diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 20caa74a0..b512a8eba 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -344,6 +344,8 @@ class LatControlTorque(LatControl): elif kia_carnival_active: friction_threshold = get_kia_carnival_friction_threshold(CS.vEgo, setpoint, desired_lateral_jerk) friction_scale *= get_kia_carnival_friction_center_fade_scale(setpoint, CS.vEgo) + elif tucson_4th_gen_active: + friction_threshold = get_tucson_4th_gen_friction_threshold(CS.vEgo, setpoint, desired_lateral_jerk) elif self.is_silverado: ff *= silverado_center_taper elif volt_plexy_test_active: diff --git a/selfdrive/controls/lib/latcontrol_vehicle_tunes.py b/selfdrive/controls/lib/latcontrol_vehicle_tunes.py index 8cbf91941..472b63b58 100644 --- a/selfdrive/controls/lib/latcontrol_vehicle_tunes.py +++ b/selfdrive/controls/lib/latcontrol_vehicle_tunes.py @@ -351,6 +351,7 @@ TUCSON_4TH_GEN_CENTER_TAPER_LAT = 0.28 TUCSON_4TH_GEN_CENTER_TAPER_LAT_WIDTH = 0.055 TUCSON_4TH_GEN_CENTER_TAPER_SPEED_MAX = 14.0 TUCSON_4TH_GEN_CENTER_TAPER_SPEED_WIDTH = 1.5 +TUCSON_4TH_GEN_FRICTION_THRESHOLD_GAIN = 0.22 KIA_FORTE_BASE_LAT_ACCEL_FACTOR_MULT = 1.05 KIA_FORTE_FF_REDUCTION_LEFT = 0.05 @@ -1645,12 +1646,24 @@ def get_kia_carnival_friction_center_fade_scale(desired_lateral_accel: float, v_ return 1.0 - (KIA_CARNIVAL_FRICTION_CENTER_FADE_MAX * speed_weight * center_weight) -def get_tucson_4th_gen_center_taper_scale(desired_lateral_accel: float, v_ego: float) -> float: +def _tucson_4th_gen_center_weights(desired_lateral_accel: float, v_ego: float) -> tuple[float, float]: speed_weight = _sigmoid((TUCSON_4TH_GEN_CENTER_TAPER_SPEED_MAX - v_ego) / TUCSON_4TH_GEN_CENTER_TAPER_SPEED_WIDTH) center_weight = _sigmoid((TUCSON_4TH_GEN_CENTER_TAPER_LAT - abs(desired_lateral_accel)) / TUCSON_4TH_GEN_CENTER_TAPER_LAT_WIDTH) + return speed_weight, center_weight + + +def get_tucson_4th_gen_center_taper_scale(desired_lateral_accel: float, v_ego: float) -> float: + speed_weight, center_weight = _tucson_4th_gen_center_weights(desired_lateral_accel, v_ego) return 1.0 - (TUCSON_4TH_GEN_CENTER_TAPER_MAX * speed_weight * center_weight) +def get_tucson_4th_gen_friction_threshold(v_ego: float, desired_lateral_accel: float = 0.0, + desired_lateral_jerk: float = 0.0) -> float: + del desired_lateral_jerk + speed_weight, center_weight = _tucson_4th_gen_center_weights(desired_lateral_accel, v_ego) + return get_hkg_canfd_base_friction_threshold(v_ego) * (1.0 + TUCSON_4TH_GEN_FRICTION_THRESHOLD_GAIN * speed_weight * center_weight) + + def _kia_forte_sigmoid(x: float) -> float: return _sigmoid(x) diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py index faf0f064b..2e806b88a 100755 --- a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py +++ b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @@ -64,6 +64,7 @@ A_CHANGE_COSTS = [200, 195, 180, 170] # Reverted to original 200 at low speeds LEAD_FILTER_TIME_LOW = 0.8 # Under 40 mph: Fast response for city emergency braking LEAD_FILTER_TIME_HIGH = 1.2 # Over 40 mph: Faster response to prevent highway gaps SPEED_FILTER_THRESHOLD = 40 * CV.MPH_TO_MS # 40 mph threshold +DUPLICATE_VISION_LEAD_FILTER_TIME = 0.15 # DISTANCE ADAPTATION STRENGTH (How much penalties increase when close to lead) # [City, Urban Hwy, Rural Hwy, High Speed] @@ -382,6 +383,8 @@ class LongitudinalMpc: self.current_filter_time = LEAD_FILTER_TIME_LOW self.lead_a_filter = FirstOrderFilter(0.0, self.current_filter_time, self.dt) self.lead_v_filter = FirstOrderFilter(0.0, self.current_filter_time, self.dt) + self.duplicate_lead_a_filters = [FirstOrderFilter(0.0, 0.0, self.dt, initialized=False) for _ in range(2)] + self.duplicate_lead_v_filters = [FirstOrderFilter(0.0, 0.0, self.dt, initialized=False) for _ in range(2)] # Slew-limited filter factor to avoid abrupt 0.50↔1.00 jumps self.filter_time_factor = 1.0 self.prev_filter_time_factor = 1.0 @@ -423,6 +426,9 @@ class LongitudinalMpc: self.time_linearization = 0.0 self.time_integrator = 0.0 self.x0 = np.zeros(X_DIM) + for lead_filter in (*self.duplicate_lead_a_filters, *self.duplicate_lead_v_filters): + lead_filter.x = 0.0 + lead_filter.initialized = False self.set_weights() def set_cost_weights(self, cost_weights, constraint_cost_weights): @@ -469,7 +475,6 @@ class LongitudinalMpc: self.lead_a_filter = FirstOrderFilter(current_a, self.current_filter_time, self.dt) self.lead_v_filter = FirstOrderFilter(current_v, self.current_filter_time, self.dt) self.prev_filter_time = self.current_filter_time - # Adaptive jerk factors for distance with interp scaling dist_factor = 1.0 + self.current_dist_adapt * (20.0 / max(lead_dist, 5.0)) acceleration_jerk *= dist_factor @@ -477,7 +482,6 @@ class LongitudinalMpc: speed_jerk *= dist_factor # Scene complexity adjustment based on model uncertainty - prev_filter_time_factor = getattr(self, 'prev_filter_time_factor', 1.0) # Target factor from uncertainty if uncertainty <= 0.45: tgt_factor = 1.0 @@ -496,10 +500,13 @@ class LongitudinalMpc: tgt_factor = max(tgt_factor, float(filter_time_factor_floor)) # Slew-limit changes to avoid step-wise filter jumps - max_step = self.slew_per_sec * self.dt - delta = np.clip(tgt_factor - self.filter_time_factor, -max_step, max_step) - self.filter_time_factor += float(delta) - filter_time_factor = float(self.filter_time_factor) + if panic_bypass: + # A real closing hazard must never wait for the comfort filter to unwind. + self.filter_time_factor = 0.0 + else: + max_step = self.slew_per_sec * self.dt + delta = np.clip(tgt_factor - self.filter_time_factor, -max_step, max_step) + self.filter_time_factor += float(delta) # When uncertainty is moderately elevated, allow accel but cap jerk by increasing jerk cost if 0.45 <= uncertainty < 0.60: @@ -519,6 +526,7 @@ class LongitudinalMpc: self.set_cost_weights(cost_weights, constraint_cost_weights) # Adjust filter time constants for complex scenes + filter_time_factor = float(self.filter_time_factor) if abs(filter_time_factor - getattr(self, 'prev_filter_time_factor', 1.0)) > 0.05: new_filter_time = self.current_filter_time * filter_time_factor current_a = self.lead_a_filter.x if hasattr(self.lead_a_filter, 'x') else 0.0 @@ -561,9 +569,11 @@ class LongitudinalMpc: lead_xv = np.column_stack((x_lead_traj, v_lead_traj)) return lead_xv - def process_lead(self, lead, tracking_lead=True, t_follow=None): + def process_lead(self, lead, tracking_lead=True, t_follow=None, *, lead_index=0, + smooth_duplicate_vision=False): v_ego = self.x0[1] - if lead is not None and lead.status and tracking_lead: + lead_active = lead is not None and lead.status and tracking_lead + if lead_active: x_lead = lead.dRel v_lead = lead.vLead a_lead = lead.aLeadK @@ -584,11 +594,30 @@ class LongitudinalMpc: x_lead = np.clip(x_lead, min_x_lead, 1e8) v_lead = np.clip(v_lead, 0.0, 1e8) a_lead = np.clip(a_lead, -10., 5.) - # Apply smoothing filters with interp scaling - self.lead_a_filter.update(a_lead) - self.lead_v_filter.update(v_lead) - a_lead = self.lead_a_filter.x - v_lead = self.lead_v_filter.x + if lead_active and smooth_duplicate_vision and not bool(getattr(lead, "radar", False)): + # Keep the baseline filter synchronized so leaving this narrow comfort + # path cannot introduce a state discontinuity. + self.lead_a_filter.update(a_lead) + self.lead_v_filter.update(v_lead) + + filter_time = self.current_filter_time + filter_time = max(filter_time, DUPLICATE_VISION_LEAD_FILTER_TIME) + filter_time *= self.filter_time_factor + + a_filter = self.duplicate_lead_a_filters[lead_index] + v_filter = self.duplicate_lead_v_filters[lead_index] + a_filter.update_alpha(filter_time) + v_filter.update_alpha(filter_time) + a_lead = a_filter.update(a_lead) + v_lead = v_filter.update(v_lead) + else: + # Preserve the historical planner path outside the qualified comfort scene. + self.lead_a_filter.update(a_lead) + self.lead_v_filter.update(v_lead) + a_lead = self.lead_a_filter.x + v_lead = self.lead_v_filter.x + self.duplicate_lead_a_filters[lead_index].initialized = False + self.duplicate_lead_v_filters[lead_index].initialized = False lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau, v_ego) return lead_xv @@ -806,13 +835,15 @@ class LongitudinalMpc: def update(self, radarstate, v_cruise, x, v, a, j, danger_factor, t_follow, personality=log.LongitudinalPersonality.standard, tracking_lead=True, - optional_far_lead_comfort=True): + optional_far_lead_comfort=True, smooth_duplicate_vision=False): v_ego = self.x0[1] lead_one = radarstate.leadOne lead_two = radarstate.leadTwo self.status = tracking_lead and (lead_one.status or lead_two.status) - lead_xv_0 = self.process_lead(lead_one, tracking_lead, t_follow=t_follow) - lead_xv_1 = self.process_lead(lead_two, tracking_lead, t_follow=t_follow) + lead_xv_0 = self.process_lead(lead_one, tracking_lead, t_follow=t_follow, lead_index=0, + smooth_duplicate_vision=smooth_duplicate_vision) + lead_xv_1 = self.process_lead(lead_two, tracking_lead, t_follow=t_follow, lead_index=1, + smooth_duplicate_vision=smooth_duplicate_vision) # To estimate a safe distance from a moving lead, we calculate how much stopping # distance that lead needs as a minimum. We can add that to the current distance diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 0ae2b77d7..a7b563f6f 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -2755,7 +2755,8 @@ class LongitudinalPlanner: ) # Duplicate vision tracks can share the same noisy velocity spike. Keep the # comfort path unless distance, TTC, or lead braking makes the scene urgent. - if panic_bypass and self.is_nonurgent_duplicate_vision_follow(scene_v_ego, effective_t_follow): + nonurgent_duplicate_vision_follow = self.is_nonurgent_duplicate_vision_follow(scene_v_ego, effective_t_follow) + if panic_bypass and nonurgent_duplicate_vision_follow: panic_bypass = False steady_follow_filter_floor = 0.0 @@ -2820,7 +2821,8 @@ class LongitudinalPlanner: self.mpc.update(sm['radarState'], v_cruise, x, v, a, j, sm['starpilotPlan'].dangerFactor, effective_t_follow, personality=personality, tracking_lead=lead_control_active, - optional_far_lead_comfort=True) + optional_far_lead_comfort=True, + smooth_duplicate_vision=nonurgent_duplicate_vision_follow and not panic_bypass) self.a_desired_trajectory_full = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution) self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution) diff --git a/selfdrive/controls/tests/test_latcontrol.py b/selfdrive/controls/tests/test_latcontrol.py index 3d4868ee1..39ed2c732 100644 --- a/selfdrive/controls/tests/test_latcontrol.py +++ b/selfdrive/controls/tests/test_latcontrol.py @@ -76,6 +76,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import ( get_kia_carnival_friction_center_fade_scale, get_kia_carnival_friction_threshold, get_tucson_4th_gen_center_taper_scale, + get_tucson_4th_gen_friction_threshold, get_kia_ev6_center_taper_scale, get_kia_ev6_ff_scale, get_kia_ev6_friction_scale, @@ -839,6 +840,16 @@ class TestLatControl: assert low_speed_turn > 0.98 assert high_speed_center > 0.98 + def test_tucson_4th_gen_friction_threshold_targets_low_speed_center(self): + base = get_hkg_canfd_base_friction_threshold(8.5) + low_speed_center = get_tucson_4th_gen_friction_threshold(8.5, 0.0) + low_speed_turn = get_tucson_4th_gen_friction_threshold(8.5, 0.50) + high_speed_center = get_tucson_4th_gen_friction_threshold(20.0, 0.0) + + assert low_speed_center == pytest.approx(base * 1.22, rel=0.01) + assert low_speed_turn == pytest.approx(base, rel=0.01) + assert high_speed_center == pytest.approx(get_hkg_canfd_base_friction_threshold(20.0), rel=0.01) + def test_tucson_4th_gen_default_update_path(self): controller, VM, CS, params, starpilot_toggles = self._build_torque_controller(HYUNDAI.HYUNDAI_TUCSON_4TH_GEN) CS.vEgo = 8.5 diff --git a/selfdrive/controls/tests/test_longitudinal_planner.py b/selfdrive/controls/tests/test_longitudinal_planner.py index dab1a5f01..ab6c54e56 100644 --- a/selfdrive/controls/tests/test_longitudinal_planner.py +++ b/selfdrive/controls/tests/test_longitudinal_planner.py @@ -14,7 +14,7 @@ import openpilot.selfdrive.controls.lib.longitudinal_planner as longitudinal_pla from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner, get_coast_accel, get_vehicle_min_accel, should_publish_planner_fcw -from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import soften_far_radar_lead_accel, should_trigger_planner_fcw +from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, soften_far_radar_lead_accel, should_trigger_planner_fcw from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC from openpilot.selfdrive.modeld.constants import ModelConstants, Plan @@ -35,6 +35,71 @@ def make_lead(*, status: bool, d_rel: float = 200.0, v_lead: float = 0.0, a_lead return lead +def test_mpc_duplicate_lead_filters_do_not_cross_contaminate_tracks(): + mpc = LongitudinalMpc() + mpc.set_cur_state(20.0, 0.0) + mpc.current_filter_time = 0.5 + lead_one = make_lead(status=True, d_rel=35.0, v_lead=12.0, model_prob=1.0) + lead_two = make_lead(status=True, d_rel=35.0, v_lead=28.0, model_prob=1.0) + + mpc.process_lead(lead_one, lead_index=0, smooth_duplicate_vision=True) + mpc.process_lead(lead_two, lead_index=1, smooth_duplicate_vision=True) + + assert mpc.duplicate_lead_v_filters[0].x == pytest.approx(12.0) + assert mpc.duplicate_lead_v_filters[1].x == pytest.approx(28.0) + + lead_one.vLead = 14.0 + mpc.process_lead(lead_one, lead_index=0, smooth_duplicate_vision=True) + assert 12.0 < mpc.duplicate_lead_v_filters[0].x < 14.0 + assert mpc.duplicate_lead_v_filters[1].x == pytest.approx(28.0) + + +def test_mpc_duplicate_vision_filter_damps_low_speed_velocity_noise(): + mpc = LongitudinalMpc() + mpc.set_cur_state(18.0, 0.0) + mpc.current_filter_time = 0.0 + lead = make_lead(status=True, d_rel=38.0, v_lead=17.0, model_prob=1.0) + + mpc.process_lead(lead, lead_index=0, smooth_duplicate_vision=True) + lead.vLead = 20.0 + mpc.process_lead(lead, lead_index=0, smooth_duplicate_vision=True) + + assert 17.0 < mpc.duplicate_lead_v_filters[0].x < 20.0 + + +def test_mpc_distinct_vision_lead_uses_unchanged_baseline_filter(): + mpc = LongitudinalMpc() + baseline_mpc = LongitudinalMpc() + mpc.set_cur_state(18.0, 0.0) + baseline_mpc.set_cur_state(18.0, 0.0) + mpc.current_filter_time = 0.0 + baseline_mpc.current_filter_time = 0.0 + lead = make_lead(status=True, d_rel=38.0, v_lead=17.0, model_prob=1.0) + + mpc.process_lead(lead, lead_index=0, smooth_duplicate_vision=False) + baseline_mpc.process_lead(lead) + lead.vLead = 20.0 + mpc.process_lead(lead, lead_index=0, smooth_duplicate_vision=False) + baseline_mpc.process_lead(lead) + + assert mpc.lead_v_filter.x == pytest.approx(baseline_mpc.lead_v_filter.x) + + +def test_mpc_panic_bypass_immediately_removes_duplicate_vision_filter(): + mpc = LongitudinalMpc() + mpc.set_cur_state(18.0, 0.0) + mpc.current_filter_time = 0.0 + lead = make_lead(status=True, d_rel=25.0, v_lead=17.0, model_prob=1.0) + + mpc.process_lead(lead, lead_index=0, smooth_duplicate_vision=True) + mpc.set_weights(v_ego=18.0, panic_bypass=True) + lead.vLead = 10.0 + mpc.process_lead(lead, lead_index=0, smooth_duplicate_vision=False) + + assert mpc.filter_time_factor == 0.0 + assert mpc.lead_v_filter.x == pytest.approx(10.0) + + def make_model(v_ego: float, desired_accel: float, gas_press_prob: float = 1.0, brake_press_prob: float = 0.0): model = log.ModelDataV2.new_message() model.init('leadsV3', 3) diff --git a/starpilot/system/the_galaxy/assets/components/navigation/navigation_destination.js b/starpilot/system/the_galaxy/assets/components/navigation/navigation_destination.js index 576a2cfb2..de8f9f7ff 100644 --- a/starpilot/system/the_galaxy/assets/components/navigation/navigation_destination.js +++ b/starpilot/system/the_galaxy/assets/components/navigation/navigation_destination.js @@ -4,11 +4,12 @@ import { formatMetersToHuman, formatSecondsToHuman, getCoordinatesFromSearch, + getMapboxSearchContext, getRoutes, removeRouteFromMap, getOrdinalSuffix, highlightRoute, -} from "./navigation_utilities.js"; +} from "./navigation_utilities.js?v=nav-search-context-2"; import { Modal } from "/assets/components/modal.js"; function sha1hex(str) { @@ -231,6 +232,7 @@ const state = reactive({ fetched: false, initialized: false, isMetric: true, + language: "", lastPosition: undefined, loadingRoute: false, mapboxPublic: undefined, @@ -249,6 +251,27 @@ const searchFieldState = reactive({ value: "" }); export function NavDestination() { + function getSearchContext(query) { + const browserLanguages = typeof navigator === "undefined" + ? [] + : (navigator.languages || [navigator.language]); + const context = getMapboxSearchContext(query, state.lastPosition, [state.language, ...browserLanguages]); + if (state.lastPosition) { + context.proximity = `${state.lastPosition.longitude},${state.lastPosition.latitude}`; + } + return context; + } + + function getMapboxSuggestParams(query) { + return new URLSearchParams({ + access_token: state.mapboxPublic, + session_token: sessionToken, + q: query, + limit: 4, + ...getSearchContext(query), + }); + } + function areRoutesEqual(a, b) { return a?.routeHash && b?.routeHash && a.routeHash === b.routeHash; } @@ -368,6 +391,7 @@ export function NavDestination() { state.amap1Key = data.amap1Key?.trim() || ""; state.amap2Key = data.amap2Key?.trim() || ""; state.isMetric = data.isMetric ?? true; + state.language = data.language?.trim() || ""; const hasMapbox = !!state.mapboxPublic && !!state.mapboxSecret; const hasAMap = !!state.amap1Key && !!state.amap2Key; state.missingKeys = !hasMapbox; @@ -422,15 +446,7 @@ export function NavDestination() { state.confirmedRoute = null; state.suggestions = "[]"; if (state.searchProvider === "mapbox") { - const params = new URLSearchParams({ - access_token: state.mapboxPublic, - session_token: sessionToken, - q: val, - limit: 4 - }); - if (state.lastPosition) { - params.set("proximity", `${state.lastPosition.longitude},${state.lastPosition.latitude}`); - } + const params = getMapboxSuggestParams(val); const res = await fetch(`https://api.mapbox.com/search/searchbox/v1/suggest?${params}`); const data = await res.json(); state.suggestions = JSON.stringify(data.suggestions); @@ -589,15 +605,7 @@ export function NavDestination() { state.confirmedRoute = null; state.suggestions = "[]"; if (state.searchProvider === "mapbox") { - const params = new URLSearchParams({ - access_token: state.mapboxPublic, - session_token: sessionToken, - q: val, - limit: 4 - }); - if (state.lastPosition) { - params.set("proximity", `${state.lastPosition.longitude},${state.lastPosition.latitude}`); - } + const params = getMapboxSuggestParams(val); const res = await fetch(`https://api.mapbox.com/search/searchbox/v1/suggest?${params}`); const data = await res.json(); state.suggestions = JSON.stringify(data.suggestions); @@ -637,7 +645,7 @@ export function NavDestination() { const retJson = await ret.json(); coords = retJson.features[0].geometry.coordinates; } else { - coords = await getCoordinatesFromSearch(label, state.mapboxPublic); + coords = await getCoordinatesFromSearch(label, state.mapboxPublic, getSearchContext(label)); } } else { coords = [sugg.location.lng, sugg.location.lat]; diff --git a/starpilot/system/the_galaxy/assets/components/navigation/navigation_utilities.js b/starpilot/system/the_galaxy/assets/components/navigation/navigation_utilities.js index 56617f1d9..c0868f34c 100644 --- a/starpilot/system/the_galaxy/assets/components/navigation/navigation_utilities.js +++ b/starpilot/system/the_galaxy/assets/components/navigation/navigation_utilities.js @@ -14,6 +14,38 @@ export function highlightRoute(map, routes, selectedRouteId) { }); } +const JAPANESE_KANA_PATTERN = /[\u3040-\u30ff]/u; + +function usesJapaneseLanguage(language) { + return typeof language === 'string' && /^(?:main_)?ja(?:[-_]|$)/i.test(language.trim()); +} + +function isLikelyInJapan(position) { + if (!position) return false; + + const latitude = Number(position.latitude); + const longitude = Number(position.longitude); + if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return false; + + // Main islands, Ryukyu Islands, and the Izu/Ogasawara island chains. + const mainIslands = latitude >= 30 && latitude <= 45.8 && longitude >= 129.2 && longitude <= 146; + const ryukyuIslands = latitude >= 24 && latitude < 30 && longitude >= 122.8 && longitude <= 131.5; + const pacificIslands = latitude >= 20 && latitude < 30 && longitude >= 136 && longitude <= 154; + return mainIslands || ryukyuIslands || pacificIslands; +} + +export function getMapboxSearchContext(query, position, preferredLanguages = []) { + const languages = Array.isArray(preferredLanguages) ? preferredLanguages : [preferredLanguages]; + const japaneseSearch = JAPANESE_KANA_PATTERN.test(query || '') || languages.some(usesJapaneseLanguage); + const hasPosition = Number.isFinite(Number(position?.latitude)) && Number.isFinite(Number(position?.longitude)); + + if (isLikelyInJapan(position) || (japaneseSearch && !hasPosition)) { + return { language: 'ja', country: 'jp' }; + } + if (japaneseSearch) return { language: 'ja' }; + return {}; +} + function addRouteSource(map, sourceId, feature) { if (map.getSource(sourceId)) { const layerId = `route-line-${sourceId.replace('route-', '')}`; @@ -141,11 +173,14 @@ export function addRouteToMap(map, routes, start, dest, onRouteSelect, useMetric map.fitBounds([start, dest], { padding, duration: 1000 }); } -export async function getCoordinatesFromSearch(searchValue, mapboxPublic) { +export async function getCoordinatesFromSearch(searchValue, mapboxPublic, searchContext = {}) { const params = new URLSearchParams({ access_token: mapboxPublic, q: searchValue }); + if (searchContext.language) params.set('language', searchContext.language); + if (searchContext.country) params.set('country', searchContext.country); + if (searchContext.proximity) params.set('proximity', searchContext.proximity); const response = await fetch(`https://api.mapbox.com/search/geocode/v6/forward?${params.toString()}`); const data = await response.json(); - return data.features[0].geometry.coordinates; + return data.features?.[0]?.geometry?.coordinates; } export async function getRoutes(from, to, mapboxPublic) { diff --git a/starpilot/system/the_galaxy/assets/components/router.js b/starpilot/system/the_galaxy/assets/components/router.js index 9ab749503..bd702bb33 100644 --- a/starpilot/system/the_galaxy/assets/components/router.js +++ b/starpilot/system/the_galaxy/assets/components/router.js @@ -8,7 +8,7 @@ import { GalaxyPairing } from "/assets/components/tools/galaxy.js" import { Home } from "/assets/components/home/home.js" import { LongitudinalManeuvers } from "/assets/components/tools/longitudinal_maneuvers.js" import { MapsManager } from "/assets/components/tools/maps.js" -import { NavDestination } from "/assets/components/navigation/navigation_destination.js?v=nav-search-context-1" +import { NavDestination } from "/assets/components/navigation/navigation_destination.js?v=nav-search-context-2" import { NavKeys } from "/assets/components/navigation/navigation_keys.js?v=app-keys-session-1" import { RouteRecordings } from "/assets/components/recordings/dashcam_routes.js" import { SettingsView } from "/assets/components/settings.js" diff --git a/starpilot/system/the_galaxy/templates/index.html b/starpilot/system/the_galaxy/templates/index.html index f640bf892..789f93bbd 100644 --- a/starpilot/system/the_galaxy/templates/index.html +++ b/starpilot/system/the_galaxy/templates/index.html @@ -19,7 +19,7 @@ - + diff --git a/starpilot/system/the_galaxy/the_galaxy.py b/starpilot/system/the_galaxy/the_galaxy.py index e4db9072e..9db2a1bb5 100644 --- a/starpilot/system/the_galaxy/the_galaxy.py +++ b/starpilot/system/the_galaxy/the_galaxy.py @@ -4031,6 +4031,7 @@ def setup(app): "amap2Key": params.get("AMapKey2", encoding="utf8") or "", "destination": params.get("NavDestination", encoding="utf8") or "", "isMetric": params.get_bool("IsMetric"), + "language": params.get("LanguageSetting", encoding="utf8") or "", "lastPosition": { "latitude": str(last_position.get("latitude", "")), "longitude": str(last_position.get("longitude", ""))