diff --git a/screens/journey.py b/screens/journey.py index 7e74008..ffeaa17 100644 --- a/screens/journey.py +++ b/screens/journey.py @@ -58,6 +58,23 @@ RED = (200, 0, 0) BLACK = (0, 0, 0) MUTED = (110, 110, 110) +# EFA's inclMOT_N flags select which transport modes a trip request is +# allowed to use -- N is an index into EFA's internal mode table, not the +# "class" value seen in responses. Long-distance rail (IC/EC/ICE) sits at +# 14-16; everything else usable for a regional/local commute (regional +# trains, S-Bahn, buses, trams, etc.) is the rest. Ported from db_info's +# _build_mot_params()/_build_produktgattungen(). +_MOT_LONG_DISTANCE = {f"inclMOT_{i}": "true" for i in (14, 15, 16)} +_MOT_REGIONAL = {f"inclMOT_{i}": "true" for i in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 17, 20)} + + +def _mot_params(connection_type: str) -> dict: + if connection_type == "regional": + return {"includedMeans": "checkbox", **_MOT_REGIONAL} + if connection_type == "long_distance": + return {"includedMeans": "checkbox", **_MOT_LONG_DISTANCE} + return {} + def _in_bounds(lat, lon, bounds) -> bool: if bounds is None: @@ -85,6 +102,7 @@ class JourneyScreen(Screen): "destination_query": "", "destination_label": "", "destination_lat": None, "destination_lon": None, "results": 3, "update_minutes": 2, + "connection_type": "all", } def get_poll_interval(self, settings: dict) -> int: @@ -150,7 +168,8 @@ class JourneyScreen(Screen): except (TypeError, ValueError): results = 3 - params = self._build_params(o_lat, o_lon, d_lat, d_lon, results) + connection_type = settings.get("connection_type") or "all" + params = self._build_params(o_lat, o_lon, d_lat, d_lon, results, connection_type) for source in EFA_SOURCES: if not (_in_bounds(o_lat, o_lon, source["bounds"]) and _in_bounds(d_lat, d_lon, source["bounds"])): @@ -172,9 +191,9 @@ class JourneyScreen(Screen): return None - def _build_params(self, o_lat, o_lon, d_lat, d_lon, results) -> dict: + def _build_params(self, o_lat, o_lon, d_lat, d_lon, results, connection_type: str = "all") -> dict: now = datetime.now().astimezone() - return { + params = { "outputFormat": "rapidJSON", "language": "de", "type_origin": "coord", @@ -192,6 +211,8 @@ class JourneyScreen(Screen): "lineRestriction": "400", "serverInfo": "0", } + params.update(_mot_params(connection_type)) + return params def _parse_journeys(self, data: dict, results: int): journeys = [] @@ -211,6 +232,7 @@ class JourneyScreen(Screen): "line": transportation.get("name") or "?", "origin": leg.get("origin") or {}, "destination": leg.get("destination") or {}, + "infos": leg.get("infos") or [], }) if not legs: @@ -228,6 +250,18 @@ class JourneyScreen(Screen): delay_min = int((dep_estimated - dep_planned).total_seconds() // 60) if dep_estimated else 0 arr_actual = arr_estimated or arr_planned + # Service notices (disruptions, elevator outages, etc.) come as + # leg.infos[].infoLinks[].title -- collect titles across all legs + # of this journey, deduped, since the same notice often repeats + # on every leg it affects. + notice_titles = [] + for leg in legs: + for info in leg["infos"]: + for link in info.get("infoLinks") or []: + title = link.get("title") or link.get("subtitle") + if title and title not in notice_titles: + notice_titles.append(title) + # EFA returns UTC timestamps (Z suffix) -- astimezone() with no # args converts to the system's local timezone before display. # Without this, times showed 2h behind (CEST is UTC+2), making @@ -239,6 +273,7 @@ class JourneyScreen(Screen): "lines": [leg["line"] for leg in legs], "transfers": len(legs) - 1, "platform": (first["origin"].get("properties") or {}).get("platformName") or "", + "notice": "; ".join(notice_titles), }) return journeys or None @@ -300,8 +335,17 @@ class JourneyScreen(Screen): detail_parts.append(f"{journey['transfers']}x umsteigen") if journey["platform"]: detail_parts.append(f"Gl. {journey['platform']}") - detail_line = truncate_to_width(draw, " · ".join(detail_parts), detail_font, width - 2 * pad) - draw.text((pad, top_line_y + time_font.size * 1.25), detail_line, font=detail_font, fill=MUTED) + if journey["notice"]: + detail_parts.append(journey["notice"]) + + detail_y = top_line_y + time_font.size * 1.25 + detail_x = pad + if journey["notice"]: + draw.text((detail_x, detail_y), "⚠", font=detail_font, fill=RED) + detail_x += text_width(draw, "⚠", detail_font) + scale * 0.015 + + detail_line = truncate_to_width(draw, " · ".join(detail_parts), detail_font, width - pad - detail_x) + draw.text((detail_x, detail_y), detail_line, font=detail_font, fill=MUTED) return image diff --git a/web/app.py b/web/app.py index 20b464b..03efba4 100644 --- a/web/app.py +++ b/web/app.py @@ -141,8 +141,11 @@ def create_app(config, screens: dict, scheduler) -> Flask: except (TypeError, ValueError): results = 3 update_minutes = max(1, int(request.form.get("update_minutes", 2) or 2)) + connection_type = request.form.get("connection_type", "all") + if connection_type not in ("all", "regional", "long_distance"): + connection_type = "all" - updates = {"results": results, "update_minutes": update_minutes} + updates = {"results": results, "update_minutes": update_minutes, "connection_type": connection_type} journey_screen = screens["journey"] if origin_query: diff --git a/web/templates/settings_journey.html b/web/templates/settings_journey.html index 1bf6ff4..81b3377 100644 --- a/web/templates/settings_journey.html +++ b/web/templates/settings_journey.html @@ -52,6 +52,15 @@ +
"Regional only" hides ICE/IC/EC long-distance trains -- just S-Bahn, regional trains, buses, trams etc.
+