mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-03 22:53:44 +08:00
402 lines
12 KiB
Python
402 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import hashlib
|
|
import math
|
|
from typing import Any
|
|
|
|
NAVIGATION_DESTINATION_KEY = "NavDestination"
|
|
RECENT_DESTINATIONS_KEY = "ApiCache_NavDestinations"
|
|
FAVORITE_DESTINATIONS_KEY = "FavoriteDestinations"
|
|
NAV_INSTRUCTION_STATE_KEY = "NavInstructionState"
|
|
NAV_INSTRUCTION_COLLAPSED_KEY = "NavInstructionCollapsed"
|
|
|
|
RECENT_DESTINATIONS_LIMIT = 10
|
|
|
|
|
|
def _coerce_float(value: Any) -> float | None:
|
|
try:
|
|
parsed = float(value)
|
|
except (OverflowError, TypeError, ValueError):
|
|
return None
|
|
return parsed if math.isfinite(parsed) else None
|
|
|
|
|
|
def _json_value(raw_value: Any, default: Any) -> Any:
|
|
if isinstance(raw_value, (list, dict)):
|
|
return raw_value
|
|
if isinstance(raw_value, bytes):
|
|
raw_value = raw_value.decode("utf-8", errors="replace")
|
|
if not raw_value:
|
|
return default
|
|
try:
|
|
return json.loads(raw_value)
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
|
|
def _param_get(params: Any, key: str, default: Any = "") -> Any:
|
|
try:
|
|
return params.get(key, encoding="utf-8", default=default)
|
|
except TypeError:
|
|
try:
|
|
return params.get(key, default=default)
|
|
except TypeError:
|
|
return params.get(key)
|
|
|
|
|
|
def _text(value: Any) -> str:
|
|
if isinstance(value, bytes):
|
|
return value.decode("utf-8", errors="replace")
|
|
return str(value or "")
|
|
|
|
|
|
def normalize_destination_payload(payload: Any) -> dict[str, Any] | None:
|
|
if not isinstance(payload, dict):
|
|
return None
|
|
|
|
name = str(payload.get("place_name") or payload.get("name") or "").strip()
|
|
latitude = _coerce_float(payload.get("latitude"))
|
|
longitude = _coerce_float(payload.get("longitude"))
|
|
|
|
if not name or latitude is None or longitude is None:
|
|
return None
|
|
|
|
return {
|
|
"name": name,
|
|
"place_name": name,
|
|
"latitude": latitude,
|
|
"longitude": longitude,
|
|
}
|
|
|
|
|
|
def parse_destination_json(raw_value: str | bytes | dict[str, Any] | None) -> dict[str, Any] | None:
|
|
if not raw_value:
|
|
return None
|
|
|
|
payload = _json_value(raw_value, None)
|
|
if payload is None:
|
|
return None
|
|
|
|
return normalize_destination_payload(payload)
|
|
|
|
|
|
def _favorite_destination_id(payload: dict[str, Any]) -> str:
|
|
"""Keep the exact favorite ID formula used by Galaxy's existing backend."""
|
|
raw = f"{payload.get('longitude')},{payload.get('latitude')}|{payload.get('routeId') or ''}|{payload.get('name') or ''}"
|
|
return hashlib.sha1(raw.encode()).hexdigest()
|
|
|
|
|
|
def favorite_destination_id(payload: dict[str, Any]) -> str:
|
|
return _favorite_destination_id(payload)
|
|
|
|
|
|
def favorite_payload_for_galaxy(destination: dict[str, Any]) -> dict[str, Any]:
|
|
favorite = dict(destination)
|
|
favorite.setdefault("routeId", "main")
|
|
return favorite
|
|
|
|
|
|
def normalize_favorite_destination(payload: Any) -> dict[str, Any] | None:
|
|
if not isinstance(payload, dict):
|
|
return None
|
|
|
|
name = str(payload.get("name") or payload.get("place_name") or "").strip()
|
|
latitude = _coerce_float(payload.get("latitude"))
|
|
longitude = _coerce_float(payload.get("longitude"))
|
|
if not name or latitude is None or longitude is None:
|
|
return None
|
|
|
|
normalized = dict(payload)
|
|
normalized.update({"name": name, "latitude": latitude, "longitude": longitude})
|
|
normalized["id"] = str(payload.get("id") or _favorite_destination_id(payload))
|
|
return normalized
|
|
|
|
|
|
def load_favorite_destinations(raw_value: Any) -> list[dict[str, Any]]:
|
|
payload = _json_value(raw_value, [])
|
|
if not isinstance(payload, list):
|
|
return []
|
|
return [
|
|
normalized
|
|
for entry in payload
|
|
if (normalized := normalize_favorite_destination(entry)) is not None
|
|
]
|
|
|
|
|
|
def ordered_favorite_destinations(favorites: list[dict[str, Any]], limit: int | None = None) -> list[dict[str, Any]]:
|
|
normalized = [
|
|
favorite
|
|
for entry in favorites
|
|
if (favorite := normalize_favorite_destination(entry)) is not None
|
|
]
|
|
|
|
def order_key(favorite: dict[str, Any]) -> tuple[bool, bool, str]:
|
|
return (
|
|
not bool(favorite.get("is_home")),
|
|
not bool(favorite.get("is_work")),
|
|
str(favorite.get("name") or "").casefold(),
|
|
)
|
|
|
|
ordered = sorted(normalized, key=order_key)
|
|
return ordered if limit is None else ordered[:max(0, limit)]
|
|
|
|
|
|
def same_destination(left: dict[str, Any] | None, right: dict[str, Any] | None) -> bool:
|
|
if not left or not right:
|
|
return False
|
|
try:
|
|
left_latitude = _coerce_float(left.get("latitude"))
|
|
right_latitude = _coerce_float(right.get("latitude"))
|
|
left_longitude = _coerce_float(left.get("longitude"))
|
|
right_longitude = _coerce_float(right.get("longitude"))
|
|
if (
|
|
left_latitude is None or right_latitude is None or
|
|
left_longitude is None or right_longitude is None
|
|
):
|
|
return False
|
|
return bool(
|
|
abs(left_latitude - right_latitude) <= 1e-6 and
|
|
abs(left_longitude - right_longitude) <= 1e-6
|
|
)
|
|
except (TypeError, ValueError):
|
|
return False
|
|
|
|
|
|
def routing_configured(params: Any) -> bool:
|
|
return bool(_text(_param_get(params, "MapboxSecretKey", "")).strip())
|
|
|
|
|
|
def set_navigation_destination(
|
|
params: Any,
|
|
payload: Any,
|
|
*,
|
|
skip_if_same: bool = False,
|
|
) -> dict[str, Any] | None:
|
|
destination = normalize_destination_payload(payload)
|
|
if destination is None:
|
|
return None
|
|
|
|
if skip_if_same:
|
|
current = parse_destination_json(_param_get(params, NAVIGATION_DESTINATION_KEY, ""))
|
|
if same_destination(current, destination):
|
|
return destination
|
|
|
|
raw_recent_destinations = _param_get(params, RECENT_DESTINATIONS_KEY, "[]")
|
|
if isinstance(raw_recent_destinations, (list, dict)):
|
|
raw_recent_destinations = json.dumps(raw_recent_destinations)
|
|
recent_destinations = update_recent_destinations(raw_recent_destinations, destination)
|
|
params.put(NAVIGATION_DESTINATION_KEY, json.dumps(destination))
|
|
params.put(RECENT_DESTINATIONS_KEY, recent_destinations)
|
|
return destination
|
|
|
|
|
|
def favorite_matches_target(
|
|
favorite: dict[str, Any],
|
|
target: dict[str, Any],
|
|
*,
|
|
allow_route_id_only: bool = False,
|
|
) -> bool:
|
|
target_id = target.get("id")
|
|
if target_id:
|
|
return bool(favorite.get("id") == target_id)
|
|
if allow_route_id_only and target.get("routeId"):
|
|
return bool(favorite.get("routeId") == target.get("routeId"))
|
|
return bool(
|
|
favorite.get("routeId") == target.get("routeId") and
|
|
favorite.get("latitude") == target.get("latitude") and
|
|
favorite.get("longitude") == target.get("longitude") and
|
|
favorite.get("name") == target.get("name")
|
|
)
|
|
|
|
|
|
def add_favorite_destination(raw_value: Any, favorite: dict[str, Any]) -> list[dict[str, Any]]:
|
|
favorites = load_favorite_destinations(raw_value)
|
|
normalized = normalize_favorite_destination(favorite)
|
|
if normalized is not None and not any(item.get("id") == normalized["id"] for item in favorites):
|
|
favorites.append(normalized)
|
|
return favorites
|
|
|
|
|
|
def remove_favorite_destination(raw_value: Any, target: dict[str, Any]) -> list[dict[str, Any]]:
|
|
favorites = load_favorite_destinations(raw_value)
|
|
return [favorite for favorite in favorites if not favorite_matches_target(favorite, target)]
|
|
|
|
|
|
def update_favorite_destination(
|
|
raw_value: Any,
|
|
target: dict[str, Any],
|
|
*,
|
|
name: str | None = None,
|
|
is_home: bool | None = None,
|
|
is_work: bool | None = None,
|
|
) -> list[dict[str, Any]] | None:
|
|
favorites = load_favorite_destinations(raw_value)
|
|
target_index = next(
|
|
(
|
|
index for index, favorite in enumerate(favorites)
|
|
if favorite_matches_target(favorite, target, allow_route_id_only=True)
|
|
),
|
|
None,
|
|
)
|
|
if target_index is None:
|
|
return None
|
|
|
|
if is_home:
|
|
for favorite in favorites:
|
|
favorite.pop("is_home", None)
|
|
if is_work:
|
|
for favorite in favorites:
|
|
favorite.pop("is_work", None)
|
|
|
|
favorite = favorites[target_index]
|
|
if name is not None:
|
|
normalized_name = str(name).strip()
|
|
if normalized_name:
|
|
favorite["name"] = normalized_name
|
|
if is_home is not None:
|
|
if is_home:
|
|
favorite["is_home"] = True
|
|
favorite.pop("is_work", None)
|
|
else:
|
|
favorite.pop("is_home", None)
|
|
if is_work is not None:
|
|
if is_work:
|
|
favorite["is_work"] = True
|
|
favorite.pop("is_home", None)
|
|
else:
|
|
favorite.pop("is_work", None)
|
|
return favorites
|
|
|
|
|
|
def normalize_recent_destination_entry(entry: Any) -> dict[str, Any] | None:
|
|
if not isinstance(entry, dict):
|
|
return None
|
|
|
|
place_name = str(entry.get("place_name") or entry.get("name") or "").strip()
|
|
if not place_name:
|
|
return None
|
|
|
|
normalized: dict[str, Any] = {"place_name": place_name}
|
|
latitude = _coerce_float(entry.get("latitude"))
|
|
longitude = _coerce_float(entry.get("longitude"))
|
|
if latitude is not None and longitude is not None:
|
|
normalized["latitude"] = latitude
|
|
normalized["longitude"] = longitude
|
|
normalized["name"] = place_name
|
|
|
|
return normalized
|
|
|
|
|
|
def load_recent_destinations(raw_value: str | bytes | None) -> list[dict[str, Any]]:
|
|
if not raw_value:
|
|
return []
|
|
|
|
try:
|
|
payload = json.loads(raw_value)
|
|
except (TypeError, ValueError, json.JSONDecodeError):
|
|
return []
|
|
|
|
if not isinstance(payload, list):
|
|
return []
|
|
|
|
normalized: list[dict[str, Any]] = []
|
|
for entry in payload:
|
|
recent = normalize_recent_destination_entry(entry)
|
|
if recent is not None:
|
|
normalized.append(recent)
|
|
return normalized
|
|
|
|
|
|
def update_recent_destinations(raw_value: str | bytes | None, destination: dict[str, Any], limit: int = RECENT_DESTINATIONS_LIMIT) -> list[dict[str, Any]]:
|
|
normalized_destination = normalize_destination_payload(destination)
|
|
if normalized_destination is None:
|
|
return load_recent_destinations(raw_value)[:limit]
|
|
|
|
current = load_recent_destinations(raw_value)
|
|
updated = [normalize_recent_destination_entry(normalized_destination)]
|
|
seen = {normalized_destination["place_name"].casefold()}
|
|
|
|
for entry in current:
|
|
place_name = entry["place_name"].casefold()
|
|
if place_name in seen:
|
|
continue
|
|
seen.add(place_name)
|
|
updated.append(entry)
|
|
if len(updated) >= limit:
|
|
break
|
|
|
|
return [entry for entry in updated if entry is not None]
|
|
|
|
|
|
class NavigationDestinationStore:
|
|
"""Shared local storage boundary for navigation destinations and favorites."""
|
|
|
|
def __init__(self, params: Any, params_memory: Any | None = None):
|
|
self.params = params
|
|
self.params_memory = params_memory or params
|
|
|
|
def active_destination(self) -> dict[str, Any] | None:
|
|
return parse_destination_json(_param_get(self.params, NAVIGATION_DESTINATION_KEY, ""))
|
|
|
|
def recent_destinations(self) -> list[dict[str, Any]]:
|
|
raw_value = _param_get(self.params, RECENT_DESTINATIONS_KEY, "[]")
|
|
if isinstance(raw_value, (list, dict)):
|
|
raw_value = json.dumps(raw_value)
|
|
return load_recent_destinations(raw_value)
|
|
|
|
def favorite_destinations(self) -> list[dict[str, Any]]:
|
|
raw_value = _param_get(self.params, FAVORITE_DESTINATIONS_KEY, "[]")
|
|
favorites = load_favorite_destinations(raw_value)
|
|
raw_payload = _json_value(raw_value, [])
|
|
if isinstance(raw_payload, list) and any(
|
|
isinstance(entry, dict) and not entry.get("id") for entry in raw_payload
|
|
):
|
|
self.params.put(FAVORITE_DESTINATIONS_KEY, favorites)
|
|
return favorites
|
|
|
|
def set_destination(
|
|
self,
|
|
payload: Any,
|
|
*,
|
|
skip_if_same: bool = False,
|
|
) -> dict[str, Any] | None:
|
|
return set_navigation_destination(self.params, payload, skip_if_same=skip_if_same)
|
|
|
|
def clear_navigation(self) -> bool:
|
|
collapsed_supported = True
|
|
for params, key in (
|
|
(self.params, NAVIGATION_DESTINATION_KEY),
|
|
(self.params_memory, NAV_INSTRUCTION_STATE_KEY),
|
|
(self.params_memory, NAV_INSTRUCTION_COLLAPSED_KEY),
|
|
):
|
|
try:
|
|
params.remove(key)
|
|
except Exception:
|
|
if key == NAV_INSTRUCTION_COLLAPSED_KEY:
|
|
collapsed_supported = False
|
|
return collapsed_supported
|
|
|
|
def add_favorite(self, favorite: dict[str, Any]) -> list[dict[str, Any]]:
|
|
updated = add_favorite_destination(_param_get(self.params, FAVORITE_DESTINATIONS_KEY, "[]"), favorite)
|
|
self.params.put(FAVORITE_DESTINATIONS_KEY, updated)
|
|
return updated
|
|
|
|
def remove_favorite(self, favorite: dict[str, Any]) -> list[dict[str, Any]]:
|
|
updated = remove_favorite_destination(_param_get(self.params, FAVORITE_DESTINATIONS_KEY, "[]"), favorite)
|
|
self.params.put(FAVORITE_DESTINATIONS_KEY, updated)
|
|
return updated
|
|
|
|
def update_favorite(self, favorite: dict[str, Any], **changes: Any) -> list[dict[str, Any]] | None:
|
|
updated = update_favorite_destination(
|
|
_param_get(self.params, FAVORITE_DESTINATIONS_KEY, "[]"),
|
|
favorite,
|
|
**changes,
|
|
)
|
|
if updated is not None:
|
|
self.params.put(FAVORITE_DESTINATIONS_KEY, updated)
|
|
return updated
|
|
|
|
def routing_configured(self) -> bool:
|
|
return routing_configured(self.params)
|