Add location search and polish running route planner

This commit is contained in:
Peter Stockings
2026-10-03 10:01:16 +10:00
parent 52943cd05b
commit 5ede1a01d2
7 changed files with 420 additions and 43 deletions
+54
View File
@@ -5,6 +5,7 @@ import json
import math
from collections import Counter
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
@@ -12,6 +13,59 @@ class PlannerError(ValueError):
pass
def search_locations(data, key):
if not isinstance(data, dict) or not isinstance(data.get("text"), str):
raise PlannerError("Enter a place name or address.")
text = data["text"].strip()
if not 3 <= len(text) <= 200:
raise PlannerError("Use 3–200 characters to search for a location.")
if not key:
raise PlannerError("Location search is not connected yet.")
params = {"text": text, "size": 5}
if data.get("focus") is not None:
focus = point(data["focus"])
params.update({"focus.point.lon": focus[0], "focus.point.lat": focus[1]})
req = Request(
"https://api.openrouteservice.org/geocode/search?" + urlencode(params),
headers={"Authorization": key, "Accept": "application/json"},
)
try:
with urlopen(req, timeout=8) as response:
raw = response.read(1_000_001)
if len(raw) > 1_000_000:
raise PlannerError("Location search returned too much data. Try a more specific place.")
result = json.loads(raw)
except HTTPError as error:
message = (
"Location search is busy. Try again shortly."
if error.code == 429
else ("Location search is unavailable. You can still select a point on the map.")
)
raise PlannerError(message) from None
except (URLError, TimeoutError, OSError, json.JSONDecodeError):
raise PlannerError(
"Location search could not connect. Try selecting a point on the map."
) from None
if not isinstance(result, dict) or not isinstance(result.get("features"), list):
raise PlannerError("Location search returned an incomplete response.")
locations = []
for feature in result["features"][:5]:
if not isinstance(feature, dict):
continue
geometry, props = feature.get("geometry"), feature.get("properties")
if not isinstance(geometry, dict) or not isinstance(props, dict):
continue
label = props.get("label") or props.get("name")
if geometry.get("type") != "Point" or not isinstance(label, str):
continue
try:
coordinates = point(geometry.get("coordinates"))
except PlannerError:
continue
locations.append({"label": label[:300], "coordinates": coordinates})
return locations
def number(value, low, high, label):
if isinstance(value, bool):
raise PlannerError(f"Choose a valid {label}.")