Spaces:
Running on Zero
Running on Zero
Commit ·
1add89c
1
Parent(s): c552106
new cities added
Browse files- .gitignore +11 -0
- app.py +14 -1
- data/cities/cities_manifest.json +90 -0
- src/discoverroute/config.py +26 -3
- src/discoverroute/pipeline.py +4 -2
- src/discoverroute/routing/area.py +41 -6
- src/discoverroute/ui/shell.py +27 -2
.gitignore
CHANGED
|
@@ -31,5 +31,16 @@ test_output.log
|
|
| 31 |
# They are committed for the Hugging Face Space so it needs no runtime download.
|
| 32 |
# If they exceed Space size limits, switch to git-lfs (see PROGRESS.md).
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
# Inference trace logs (Open Trace) — written at runtime, pushed to HF Dataset
|
| 35 |
logs/
|
|
|
|
| 31 |
# They are committed for the Hugging Face Space so it needs no runtime download.
|
| 32 |
# If they exceed Space size limits, switch to git-lfs (see PROGRESS.md).
|
| 33 |
|
| 34 |
+
# Secondary city cores now live in the HF dataset
|
| 35 |
+
# (build-small-hackathon/discoverroute-cities) and are pulled + pre-warmed at
|
| 36 |
+
# boot — so DON'T commit newly baked cores into the repo (keeps the image lean).
|
| 37 |
+
# London/Barcelona/New York predate this and stay tracked; this only ignores new
|
| 38 |
+
# untracked cores (San Francisco, Tokyo, Mumbai, Shanghai, Berlin, …).
|
| 39 |
+
data/cities/*_walk.graphml
|
| 40 |
+
data/cities/*_pois.parquet
|
| 41 |
+
!data/cities/london_*
|
| 42 |
+
!data/cities/barcelona_*
|
| 43 |
+
!data/cities/newyork_*
|
| 44 |
+
|
| 45 |
# Inference trace logs (Open Trace) — written at runtime, pushed to HF Dataset
|
| 46 |
logs/
|
app.py
CHANGED
|
@@ -59,7 +59,7 @@ def suggest(query: str = "") -> list:
|
|
| 59 |
def plan(start: str, dest: str, mode: str = "walk", budget: float = 0.5,
|
| 60 |
vibe: str = "", adventurousness: float = 0.3,
|
| 61 |
prefer_green: float = 0.0, prefer_quiet: float = 0.0,
|
| 62 |
-
profile: str = "") -> dict:
|
| 63 |
"""Plan a route and return everything the custom frontend renders."""
|
| 64 |
try:
|
| 65 |
profile_obj = json.loads(profile) if profile else {}
|
|
@@ -70,6 +70,7 @@ def plan(start: str, dest: str, mode: str = "walk", budget: float = 0.5,
|
|
| 70 |
start_query=start, dest_query=dest, mode=mode, budget=budget, vibe=vibe,
|
| 71 |
adventurousness=adventurousness, prefer_green=prefer_green,
|
| 72 |
prefer_quiet=prefer_quiet, profile=profile_obj, n_alternatives=N_ALTERNATIVES,
|
|
|
|
| 73 |
)
|
| 74 |
|
| 75 |
if result.error:
|
|
@@ -160,6 +161,18 @@ def warmup() -> None:
|
|
| 160 |
print("[warmup] routing graph + POIs ready", flush=True)
|
| 161 |
except Exception as exc: # noqa: BLE001
|
| 162 |
print(f"[warmup] graph FAILED: {exc}", flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
try:
|
| 164 |
from discoverroute.interpret import embed
|
| 165 |
embed.vibe_to_affinity("quiet green wander")
|
|
|
|
| 59 |
def plan(start: str, dest: str, mode: str = "walk", budget: float = 0.5,
|
| 60 |
vibe: str = "", adventurousness: float = 0.3,
|
| 61 |
prefer_green: float = 0.0, prefer_quiet: float = 0.0,
|
| 62 |
+
profile: str = "", city: str = "") -> dict:
|
| 63 |
"""Plan a route and return everything the custom frontend renders."""
|
| 64 |
try:
|
| 65 |
profile_obj = json.loads(profile) if profile else {}
|
|
|
|
| 70 |
start_query=start, dest_query=dest, mode=mode, budget=budget, vibe=vibe,
|
| 71 |
adventurousness=adventurousness, prefer_green=prefer_green,
|
| 72 |
prefer_quiet=prefer_quiet, profile=profile_obj, n_alternatives=N_ALTERNATIVES,
|
| 73 |
+
city=city,
|
| 74 |
)
|
| 75 |
|
| 76 |
if result.error:
|
|
|
|
| 161 |
print("[warmup] routing graph + POIs ready", flush=True)
|
| 162 |
except Exception as exc: # noqa: BLE001
|
| 163 |
print(f"[warmup] graph FAILED: {exc}", flush=True)
|
| 164 |
+
|
| 165 |
+
# Pre-warm the secondary cities: pull each from the HF dataset (if not already
|
| 166 |
+
# local) and load it into memory NOW, at boot, so the first user to pick a city
|
| 167 |
+
# waits 0 s — and request-time stays fully offline (files are local by then).
|
| 168 |
+
from discoverroute import config
|
| 169 |
+
from discoverroute.routing import area as area_mod
|
| 170 |
+
for slug in config.PREWARM_CITIES:
|
| 171 |
+
try:
|
| 172 |
+
area_mod._city_area(slug) # downloads if needed + loads + caches
|
| 173 |
+
print(f"[warmup] city ready: {slug}", flush=True)
|
| 174 |
+
except Exception as exc: # noqa: BLE001 - one bad city must not block boot
|
| 175 |
+
print(f"[warmup] city {slug} skipped: {exc}", flush=True)
|
| 176 |
try:
|
| 177 |
from discoverroute.interpret import embed
|
| 178 |
embed.vibe_to_affinity("quiet green wander")
|
data/cities/cities_manifest.json
CHANGED
|
@@ -52,5 +52,95 @@
|
|
| 52 |
"build_date": "2026-06-13",
|
| 53 |
"source": "OpenStreetMap",
|
| 54 |
"license": "ODbL \u2014 \u00a9 OpenStreetMap contributors"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
}
|
| 56 |
}
|
|
|
|
| 52 |
"build_date": "2026-06-13",
|
| 53 |
"source": "OpenStreetMap",
|
| 54 |
"license": "ODbL \u2014 \u00a9 OpenStreetMap contributors"
|
| 55 |
+
},
|
| 56 |
+
"sanfrancisco": {
|
| 57 |
+
"label": "San Francisco",
|
| 58 |
+
"center": [
|
| 59 |
+
37.788,
|
| 60 |
+
-122.4075
|
| 61 |
+
],
|
| 62 |
+
"bbox": [
|
| 63 |
+
-122.44387424116717,
|
| 64 |
+
37.75905120318436,
|
| 65 |
+
-122.37112575883283,
|
| 66 |
+
37.81694879681563
|
| 67 |
+
],
|
| 68 |
+
"tz": "America/Los_Angeles",
|
| 69 |
+
"poi_count": 3802,
|
| 70 |
+
"build_date": "2026-06-14",
|
| 71 |
+
"source": "OpenStreetMap",
|
| 72 |
+
"license": "ODbL \u2014 \u00a9 OpenStreetMap contributors"
|
| 73 |
+
},
|
| 74 |
+
"tokyo": {
|
| 75 |
+
"label": "Tokyo",
|
| 76 |
+
"center": [
|
| 77 |
+
35.6762,
|
| 78 |
+
139.7653
|
| 79 |
+
],
|
| 80 |
+
"bbox": [
|
| 81 |
+
139.72991277516684,
|
| 82 |
+
35.64725120318437,
|
| 83 |
+
139.80068722483315,
|
| 84 |
+
35.705148796815635
|
| 85 |
+
],
|
| 86 |
+
"tz": "Asia/Tokyo",
|
| 87 |
+
"poi_count": 10051,
|
| 88 |
+
"build_date": "2026-06-14",
|
| 89 |
+
"source": "OpenStreetMap",
|
| 90 |
+
"license": "ODbL \u2014 \u00a9 OpenStreetMap contributors"
|
| 91 |
+
},
|
| 92 |
+
"mumbai": {
|
| 93 |
+
"label": "Mumbai",
|
| 94 |
+
"center": [
|
| 95 |
+
18.922,
|
| 96 |
+
72.8347
|
| 97 |
+
],
|
| 98 |
+
"bbox": [
|
| 99 |
+
72.80431189708523,
|
| 100 |
+
18.893051203184367,
|
| 101 |
+
72.86508810291477,
|
| 102 |
+
18.950948796815634
|
| 103 |
+
],
|
| 104 |
+
"tz": "Asia/Kolkata",
|
| 105 |
+
"poi_count": 413,
|
| 106 |
+
"build_date": "2026-06-14",
|
| 107 |
+
"source": "OpenStreetMap",
|
| 108 |
+
"license": "ODbL \u2014 \u00a9 OpenStreetMap contributors"
|
| 109 |
+
},
|
| 110 |
+
"shanghai": {
|
| 111 |
+
"label": "Shanghai",
|
| 112 |
+
"center": [
|
| 113 |
+
31.234,
|
| 114 |
+
121.481
|
| 115 |
+
],
|
| 116 |
+
"bbox": [
|
| 117 |
+
121.4473812266571,
|
| 118 |
+
31.205051203184368,
|
| 119 |
+
121.51461877334289,
|
| 120 |
+
31.262948796815635
|
| 121 |
+
],
|
| 122 |
+
"tz": "Asia/Shanghai",
|
| 123 |
+
"poi_count": 2071,
|
| 124 |
+
"build_date": "2026-06-14",
|
| 125 |
+
"source": "OpenStreetMap",
|
| 126 |
+
"license": "ODbL \u2014 \u00a9 OpenStreetMap contributors"
|
| 127 |
+
},
|
| 128 |
+
"berlin": {
|
| 129 |
+
"label": "Berlin",
|
| 130 |
+
"center": [
|
| 131 |
+
52.517,
|
| 132 |
+
13.3889
|
| 133 |
+
],
|
| 134 |
+
"bbox": [
|
| 135 |
+
13.341661332878164,
|
| 136 |
+
52.48805120318437,
|
| 137 |
+
13.436138667121835,
|
| 138 |
+
52.54594879681564
|
| 139 |
+
],
|
| 140 |
+
"tz": "Europe/Berlin",
|
| 141 |
+
"poi_count": 10810,
|
| 142 |
+
"build_date": "2026-06-14",
|
| 143 |
+
"source": "OpenStreetMap",
|
| 144 |
+
"license": "ODbL \u2014 \u00a9 OpenStreetMap contributors"
|
| 145 |
}
|
| 146 |
}
|
src/discoverroute/config.py
CHANGED
|
@@ -87,11 +87,34 @@ def corridor_halfwidth_m(budget: float) -> float:
|
|
| 87 |
CITY_DATA_DIR = DATA_DIR / "cities"
|
| 88 |
CITIES_MANIFEST_PATH = CITY_DATA_DIR / "cities_manifest.json"
|
| 89 |
CITIES = {
|
| 90 |
-
"london":
|
| 91 |
-
"barcelona":
|
| 92 |
-
"newyork":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
}
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
def city_graph_path(slug: str) -> Path:
|
| 97 |
return CITY_DATA_DIR / f"{slug}_walk.graphml"
|
|
|
|
| 87 |
CITY_DATA_DIR = DATA_DIR / "cities"
|
| 88 |
CITIES_MANIFEST_PATH = CITY_DATA_DIR / "cities_manifest.json"
|
| 89 |
CITIES = {
|
| 90 |
+
"london": {"label": "London", "center": (51.5118, -0.1230), "radius_m": 3200, "tz": "Europe/London"},
|
| 91 |
+
"barcelona": {"label": "Barcelona", "center": (41.3870, 2.1700), "radius_m": 3200, "tz": "Europe/Madrid"},
|
| 92 |
+
"newyork": {"label": "New York", "center": (40.7560, -73.9845), "radius_m": 3200, "tz": "America/New_York"},
|
| 93 |
+
"sanfrancisco": {"label": "San Francisco", "center": (37.7880, -122.4075), "radius_m": 3200, "tz": "America/Los_Angeles"},
|
| 94 |
+
"tokyo": {"label": "Tokyo", "center": (35.6762, 139.7653), "radius_m": 3200, "tz": "Asia/Tokyo"},
|
| 95 |
+
"mumbai": {"label": "Mumbai", "center": (18.9220, 72.8347), "radius_m": 3200, "tz": "Asia/Kolkata"},
|
| 96 |
+
"shanghai": {"label": "Shanghai", "center": (31.2340, 121.4810), "radius_m": 3200, "tz": "Asia/Shanghai"},
|
| 97 |
+
"berlin": {"label": "Berlin", "center": (52.5170, 13.3889), "radius_m": 3200, "tz": "Europe/Berlin"},
|
| 98 |
}
|
| 99 |
|
| 100 |
+
# Secondary city cores are hosted as a HF *dataset* (just a folder of files), not
|
| 101 |
+
# committed into this repo — so the Space/app image stays lean and scales past a
|
| 102 |
+
# handful of cities. Each `<slug>_walk.graphml` + `<slug>_pois.parquet` is pulled
|
| 103 |
+
# on demand (public repo => no token needed) and cached into CITY_DATA_DIR, after
|
| 104 |
+
# which the normal on-disk path (city_graph_path/city_pois_path) just works.
|
| 105 |
+
CITIES_DATASET_REPO = os.environ.get(
|
| 106 |
+
"DISCOVERROUTE_CITIES_REPO", "build-small-hackathon/discoverroute-cities"
|
| 107 |
+
)
|
| 108 |
+
# Cities to download + load into memory at boot ("pre-warm") so the first user to
|
| 109 |
+
# pick one waits 0 s. Default: every configured city. Boot cost is paid once,
|
| 110 |
+
# before any request, and keeps request-time fully offline (files already local).
|
| 111 |
+
# Override with a comma-separated slug list, e.g. "london,newyork,tokyo".
|
| 112 |
+
PREWARM_CITIES = [
|
| 113 |
+
s.strip() for s in os.environ.get(
|
| 114 |
+
"DISCOVERROUTE_PREWARM_CITIES", ",".join(CITIES)
|
| 115 |
+
).split(",") if s.strip() in CITIES
|
| 116 |
+
]
|
| 117 |
+
|
| 118 |
|
| 119 |
def city_graph_path(slug: str) -> Path:
|
| 120 |
return CITY_DATA_DIR / f"{slug}_walk.graphml"
|
src/discoverroute/pipeline.py
CHANGED
|
@@ -56,11 +56,12 @@ def plan_route(
|
|
| 56 |
prefer_quiet: float = 0.0,
|
| 57 |
profile: dict | None = None,
|
| 58 |
n_alternatives: int = 1,
|
|
|
|
| 59 |
) -> PlanResult:
|
| 60 |
"""Plan a route + log one plan-level trace row. Never raises for user errors."""
|
| 61 |
result = _plan_route_impl(
|
| 62 |
start_query, dest_query, mode, budget, vibe, adventurousness,
|
| 63 |
-
prefer_green, prefer_quiet, profile, n_alternatives,
|
| 64 |
)
|
| 65 |
try: # one Open-Trace / Field-Notes row per call; never break a route
|
| 66 |
from discoverroute.interpret.affinity import source_of
|
|
@@ -93,6 +94,7 @@ def _plan_route_impl(
|
|
| 93 |
prefer_quiet: float = 0.0,
|
| 94 |
profile: dict | None = None,
|
| 95 |
n_alternatives: int = 1,
|
|
|
|
| 96 |
) -> PlanResult:
|
| 97 |
"""Plan a route. Returns a PlanResult; never raises for user-facing errors."""
|
| 98 |
# Validate & clamp inputs up front (an unknown mode would silently route at
|
|
@@ -110,7 +112,7 @@ def _plan_route_impl(
|
|
| 110 |
# Pick the area: Paris is pre-baked/instant; any other city is fetched
|
| 111 |
# live from OSM (only the box spanning the two endpoints).
|
| 112 |
area = area_mod.resolve_area(
|
| 113 |
-
start, end, label=_city_label(start_query, dest_query))
|
| 114 |
graph = area.graph
|
| 115 |
plain = g.plain_route(graph, *start, *end, mode=mode)
|
| 116 |
except RouteError as exc:
|
|
|
|
| 56 |
prefer_quiet: float = 0.0,
|
| 57 |
profile: dict | None = None,
|
| 58 |
n_alternatives: int = 1,
|
| 59 |
+
city: str = "",
|
| 60 |
) -> PlanResult:
|
| 61 |
"""Plan a route + log one plan-level trace row. Never raises for user errors."""
|
| 62 |
result = _plan_route_impl(
|
| 63 |
start_query, dest_query, mode, budget, vibe, adventurousness,
|
| 64 |
+
prefer_green, prefer_quiet, profile, n_alternatives, city,
|
| 65 |
)
|
| 66 |
try: # one Open-Trace / Field-Notes row per call; never break a route
|
| 67 |
from discoverroute.interpret.affinity import source_of
|
|
|
|
| 94 |
prefer_quiet: float = 0.0,
|
| 95 |
profile: dict | None = None,
|
| 96 |
n_alternatives: int = 1,
|
| 97 |
+
city: str = "",
|
| 98 |
) -> PlanResult:
|
| 99 |
"""Plan a route. Returns a PlanResult; never raises for user-facing errors."""
|
| 100 |
# Validate & clamp inputs up front (an unknown mode would silently route at
|
|
|
|
| 112 |
# Pick the area: Paris is pre-baked/instant; any other city is fetched
|
| 113 |
# live from OSM (only the box spanning the two endpoints).
|
| 114 |
area = area_mod.resolve_area(
|
| 115 |
+
start, end, label=_city_label(start_query, dest_query), city=city)
|
| 116 |
graph = area.graph
|
| 117 |
plain = g.plain_route(graph, *start, *end, mode=mode)
|
| 118 |
except RouteError as exc:
|
src/discoverroute/routing/area.py
CHANGED
|
@@ -173,17 +173,41 @@ def _in_bbox(pt, bbox) -> bool:
|
|
| 173 |
|
| 174 |
|
| 175 |
def available_cities() -> list[str]:
|
| 176 |
-
"""Slugs of pre-baked cities whose data is
|
| 177 |
return [s for s in config.CITIES
|
| 178 |
if config.city_graph_path(s).exists() and config.city_pois_path(s).exists()]
|
| 179 |
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
@functools.lru_cache(maxsize=8)
|
| 182 |
def _city_area(slug: str) -> Area:
|
| 183 |
-
"""Load a pre-baked city
|
| 184 |
from discoverroute.routing import graph as g
|
| 185 |
|
| 186 |
spec = config.CITIES[slug]
|
|
|
|
| 187 |
graph = ox.load_graphml(config.city_graph_path(slug))
|
| 188 |
df = pd.read_parquet(config.city_pois_path(slug))
|
| 189 |
origin = tuple(spec["center"])
|
|
@@ -244,15 +268,26 @@ def _build_ondemand(bbox, label: str) -> Area:
|
|
| 244 |
|
| 245 |
|
| 246 |
def resolve_area(start: tuple[float, float], end: tuple[float, float],
|
| 247 |
-
label: str = "") -> Area:
|
| 248 |
"""Pick the right area for an A→B request.
|
| 249 |
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
|
|
|
| 253 |
"""
|
| 254 |
from discoverroute.routing.graph import RouteError
|
| 255 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
# 1) Paris — full pre-baked city (instant, offline).
|
| 257 |
if config.in_paris(*start) and config.in_paris(*end):
|
| 258 |
return _paris_area()
|
|
|
|
| 173 |
|
| 174 |
|
| 175 |
def available_cities() -> list[str]:
|
| 176 |
+
"""Slugs of pre-baked cities whose data is present on disk (incl. pulled)."""
|
| 177 |
return [s for s in config.CITIES
|
| 178 |
if config.city_graph_path(s).exists() and config.city_pois_path(s).exists()]
|
| 179 |
|
| 180 |
|
| 181 |
+
def ensure_city_files(slug: str) -> None:
|
| 182 |
+
"""Make a city's graph + POI parquet exist locally, pulling from the HF
|
| 183 |
+
dataset if absent. Downloads into CITY_DATA_DIR so every existing on-disk
|
| 184 |
+
code path (city_graph_path/available_cities) then works unchanged. The repo
|
| 185 |
+
is public, so no token is needed; the file is cached after the first pull.
|
| 186 |
+
"""
|
| 187 |
+
targets = [config.city_graph_path(slug), config.city_pois_path(slug)]
|
| 188 |
+
if all(p.exists() for p in targets):
|
| 189 |
+
return
|
| 190 |
+
from huggingface_hub import hf_hub_download
|
| 191 |
+
config.CITY_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
| 192 |
+
for path in targets:
|
| 193 |
+
if path.exists():
|
| 194 |
+
continue
|
| 195 |
+
t0 = time.time()
|
| 196 |
+
hf_hub_download(
|
| 197 |
+
repo_id=config.CITIES_DATASET_REPO, repo_type="dataset",
|
| 198 |
+
filename=path.name, local_dir=str(config.CITY_DATA_DIR),
|
| 199 |
+
)
|
| 200 |
+
logger.info("pulled %s from %s in %.1fs",
|
| 201 |
+
path.name, config.CITIES_DATASET_REPO, time.time() - t0)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
@functools.lru_cache(maxsize=8)
|
| 205 |
def _city_area(slug: str) -> Area:
|
| 206 |
+
"""Load a pre-baked city as an offline Area, pulling its files if needed."""
|
| 207 |
from discoverroute.routing import graph as g
|
| 208 |
|
| 209 |
spec = config.CITIES[slug]
|
| 210 |
+
ensure_city_files(slug)
|
| 211 |
graph = ox.load_graphml(config.city_graph_path(slug))
|
| 212 |
df = pd.read_parquet(config.city_pois_path(slug))
|
| 213 |
origin = tuple(spec["center"])
|
|
|
|
| 268 |
|
| 269 |
|
| 270 |
def resolve_area(start: tuple[float, float], end: tuple[float, float],
|
| 271 |
+
label: str = "", city: str = "") -> Area:
|
| 272 |
"""Pick the right area for an A→B request.
|
| 273 |
|
| 274 |
+
An explicit ``city`` slug (from the UI picker) wins outright — the pre-baked
|
| 275 |
+
core is loaded (pulled from the dataset if needed). Otherwise both endpoints
|
| 276 |
+
inside Paris => the pre-baked Paris area; both inside one cached city core =>
|
| 277 |
+
that core; anything else is fetched just-big-enough live from OSM. Endpoints
|
| 278 |
+
too far apart for an on-demand box are rejected with a friendly RouteError.
|
| 279 |
"""
|
| 280 |
from discoverroute.routing.graph import RouteError
|
| 281 |
|
| 282 |
+
# 0) Explicit city pick (the UI dropdown) — load that core, full stop.
|
| 283 |
+
slug = (city or "").strip().lower()
|
| 284 |
+
if slug:
|
| 285 |
+
if slug == "paris":
|
| 286 |
+
return _paris_area()
|
| 287 |
+
if slug in config.CITIES:
|
| 288 |
+
return _city_area(slug)
|
| 289 |
+
# Unknown slug => ignore and fall through to inference (never crash a route).
|
| 290 |
+
|
| 291 |
# 1) Paris — full pre-baked city (instant, offline).
|
| 292 |
if config.in_paris(*start) and config.in_paris(*end):
|
| 293 |
return _paris_area()
|
src/discoverroute/ui/shell.py
CHANGED
|
@@ -82,6 +82,7 @@ body{ font-family:'DM Sans',ui-sans-serif,system-ui,sans-serif; color:var(--dr-i
|
|
| 82 |
/* a leading pin/flag glyph for the start & destination fields */
|
| 83 |
.field-wrap{ position:relative; }
|
| 84 |
.field-wrap > input[type=text]{ padding-left:34px; }
|
|
|
|
| 85 |
.field-wrap::before{ content:attr(data-glyph); position:absolute; left:12px; top:50%;
|
| 86 |
transform:translateY(-50%); font-size:14px; pointer-events:none; z-index:2; line-height:1; }
|
| 87 |
|
|
@@ -314,6 +315,21 @@ def _vibe_chips() -> str:
|
|
| 314 |
return f'<div class="vibe-chips" id="dr-vibe-chips">{chips}</div>'
|
| 315 |
|
| 316 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
def _left_panel() -> str:
|
| 318 |
return f"""
|
| 319 |
<aside class="left-panel" id="left-panel">
|
|
@@ -337,6 +353,15 @@ def _left_panel() -> str:
|
|
| 337 |
{_vibe_chips()}
|
| 338 |
</div>
|
| 339 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
<div class="dr-control combo">
|
| 341 |
<label class="dr-label" for="dr-start">Start</label>
|
| 342 |
<div class="field-wrap" data-glyph="📍">
|
|
@@ -344,8 +369,7 @@ def _left_panel() -> str:
|
|
| 344 |
value="Place de la République, Paris">
|
| 345 |
</div>
|
| 346 |
<div class="combo-list" id="dr-start-list"></div>
|
| 347 |
-
<div class="dr-help">
|
| 348 |
-
(e.g. "British Museum, London"), not a street address.</div>
|
| 349 |
</div>
|
| 350 |
|
| 351 |
<div class="dr-control combo">
|
|
@@ -781,6 +805,7 @@ async function plan() {
|
|
| 781 |
adventurousness: +$("dr-adv-i").value,
|
| 782 |
prefer_green: +$("dr-green-i").value, prefer_quiet: +$("dr-quiet-i").value,
|
| 783 |
profile: JSON.stringify(readProfile()),
|
|
|
|
| 784 |
});
|
| 785 |
renderResult((r.data && r.data[0]) || {});
|
| 786 |
} catch (e) {
|
|
|
|
| 82 |
/* a leading pin/flag glyph for the start & destination fields */
|
| 83 |
.field-wrap{ position:relative; }
|
| 84 |
.field-wrap > input[type=text]{ padding-left:34px; }
|
| 85 |
+
.field-wrap > select.dr-field{ padding-left:34px; cursor:pointer; }
|
| 86 |
.field-wrap::before{ content:attr(data-glyph); position:absolute; left:12px; top:50%;
|
| 87 |
transform:translateY(-50%); font-size:14px; pointer-events:none; z-index:2; line-height:1; }
|
| 88 |
|
|
|
|
| 315 |
return f'<div class="vibe-chips" id="dr-vibe-chips">{chips}</div>'
|
| 316 |
|
| 317 |
|
| 318 |
+
# (slug, label) for the city picker: Paris (the always-on default) then every
|
| 319 |
+
# configured core. The chosen slug is sent to /plan and wins area resolution.
|
| 320 |
+
_CITY_CHOICES = [("paris", "Paris")] + [
|
| 321 |
+
(slug, spec["label"]) for slug, spec in config.CITIES.items()
|
| 322 |
+
]
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def _city_options() -> str:
|
| 326 |
+
opts = "".join(
|
| 327 |
+
f'<option value="{slug}"{" selected" if slug == "paris" else ""}>{label}</option>'
|
| 328 |
+
for slug, label in _CITY_CHOICES
|
| 329 |
+
)
|
| 330 |
+
return opts
|
| 331 |
+
|
| 332 |
+
|
| 333 |
def _left_panel() -> str:
|
| 334 |
return f"""
|
| 335 |
<aside class="left-panel" id="left-panel">
|
|
|
|
| 353 |
{_vibe_chips()}
|
| 354 |
</div>
|
| 355 |
|
| 356 |
+
<div class="dr-control">
|
| 357 |
+
<label class="dr-label" for="dr-city">City</label>
|
| 358 |
+
<div class="field-wrap" data-glyph="🌍">
|
| 359 |
+
<select id="dr-city" class="dr-field">{_city_options()}</select>
|
| 360 |
+
</div>
|
| 361 |
+
<div class="dr-help">Pick the city to explore — Start & Destination should be
|
| 362 |
+
places within it.</div>
|
| 363 |
+
</div>
|
| 364 |
+
|
| 365 |
<div class="dr-control combo">
|
| 366 |
<label class="dr-label" for="dr-start">Start</label>
|
| 367 |
<div class="field-wrap" data-glyph="📍">
|
|
|
|
| 369 |
value="Place de la République, Paris">
|
| 370 |
</div>
|
| 371 |
<div class="combo-list" id="dr-start-list"></div>
|
| 372 |
+
<div class="dr-help">Try a landmark (e.g. "British Museum"), not a street address.</div>
|
|
|
|
| 373 |
</div>
|
| 374 |
|
| 375 |
<div class="dr-control combo">
|
|
|
|
| 805 |
adventurousness: +$("dr-adv-i").value,
|
| 806 |
prefer_green: +$("dr-green-i").value, prefer_quiet: +$("dr-quiet-i").value,
|
| 807 |
profile: JSON.stringify(readProfile()),
|
| 808 |
+
city: $("dr-city").value,
|
| 809 |
});
|
| 810 |
renderResult((r.data && r.data[0]) || {});
|
| 811 |
} catch (e) {
|