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", ""))