diff --git a/screens/journey.py b/screens/journey.py index b54d8ac..7e74008 100644 --- a/screens/journey.py +++ b/screens/journey.py @@ -26,20 +26,25 @@ from screens.render_utils import load_font, text_width, truncate_to_width logger = logging.getLogger(__name__) HTTP_TIMEOUT = 10 -GEOCODE_URL = "https://geocoding-api.open-meteo.com/v1/search" +# Each EFA instance exposes both endpoints under the same base path -- +# XML_TRIP_REQUEST2 for routing, XML_STOPFINDER_REQUEST for resolving a +# place name to a stop + coordinates. Using the stopfinder from the same +# system that will answer the trip request (rather than a generic +# city-level geocoder) is what makes searching for e.g. "Münster +# Hauptbahnhof" -- a specific station, not just a city -- work at all. EFA_SOURCES = [ { "name": "vrr", - "url": "https://www.vrr.de/vrr-efa/XML_TRIP_REQUEST2", + "base": "https://www.vrr.de/vrr-efa/", # vrr.de hangs until timeout for trips outside its home region # instead of returning a fast error, so it's only queried when both # ends of the trip roughly fall within North Rhine-Westphalia. "bounds": {"lat_min": 50.3, "lat_max": 52.6, "lon_min": 5.7, "lon_max": 9.5}, }, - {"name": "bahnland-bayern", "url": "https://bahnland-bayern.de/efa/XML_TRIP_REQUEST2", "bounds": None}, - {"name": "efa", "url": "https://www.efa.de/hit-efa/XML_TRIP_REQUEST2", "bounds": None}, - {"name": "efa-bw", "url": "http://www.efa-bw.de/nvbw/XML_TRIP_REQUEST2", "bounds": None}, + {"name": "bahnland-bayern", "base": "https://bahnland-bayern.de/efa/", "bounds": None}, + {"name": "efa", "base": "https://www.efa.de/hit-efa/", "bounds": None}, + {"name": "efa-bw", "base": "http://www.efa-bw.de/nvbw/", "bounds": None}, ] # Some EFA instances reject the default requests/python User-Agent as bot @@ -91,23 +96,49 @@ class JourneyScreen(Screen): return max(1, minutes) * 60 def geocode(self, query: str): - """Resolve a place name to coordinates -- same free Open-Meteo - geocoding WeatherScreen uses, no key needed.""" - response = requests.get( - GEOCODE_URL, params={"name": query, "count": 1, "language": "de"}, timeout=HTTP_TIMEOUT - ) - response.raise_for_status() - results = response.json().get("results") or [] - if not results: - return None - top = results[0] - label_parts = [top.get("name"), top.get("admin1"), top.get("country")] - return { - "latitude": top["latitude"], - "longitude": top["longitude"], - "label": ", ".join(p for p in label_parts if p), + """Resolve a place/station name to coordinates via the same EFA + instances used for trip requests (XML_STOPFINDER_REQUEST) -- not a + generic city-level geocoder, which has no idea what "Hauptbahnhof" + means. Tries each source in order (no bounds pre-filter here, since + we don't have coordinates yet to check bounds against); returns the + first usable match.""" + params = { + "outputFormat": "rapidJSON", + "language": "de", + "type_sf": "any", + "name_sf": query, + "anyMaxSizeHitList": 5, + "coordOutputFormat": "WGS84[dd.ddddd]", } + for source in EFA_SOURCES: + try: + response = requests.get( + source["base"] + "XML_STOPFINDER_REQUEST", + params=params, headers=REQUEST_HEADERS, timeout=HTTP_TIMEOUT, + ) + response.raise_for_status() + data = response.json() + except (requests.exceptions.RequestException, ValueError) as exc: + logger.warning("Stopfinder source '%s' failed, trying next: %s", source["name"], exc) + continue + + match = self._best_location(data) + if match: + return match + + return None + + def _best_location(self, data: dict): + locations = data.get("locations") or [] + if not locations: + return None + best = next((loc for loc in locations if loc.get("isBest")), locations[0]) + coord = best.get("coord") + if not coord or len(coord) < 2: + return None + return {"latitude": coord[0], "longitude": coord[1], "label": best.get("name") or ""} + def check(self, settings: dict): o_lat, o_lon = settings.get("origin_lat"), settings.get("origin_lon") d_lat, d_lon = settings.get("destination_lat"), settings.get("destination_lon") @@ -126,7 +157,7 @@ class JourneyScreen(Screen): continue try: response = requests.get( - source["url"], params=params, headers=REQUEST_HEADERS, timeout=HTTP_TIMEOUT + source["base"] + "XML_TRIP_REQUEST2", params=params, headers=REQUEST_HEADERS, timeout=HTTP_TIMEOUT ) response.raise_for_status() data = response.json()