Journey: add regional-only filter and service notices
- connection_type setting (all/regional/long_distance) restricts EFA trip requests via inclMOT_N params, so ICE/IC/EC can be filtered out for commutes that only care about regional/local transit. - Surface leg.infos[].infoLinks titles (real disruption/outage notices from EFA) on each journey row, marked with a red warning icon.
This commit is contained in:
1 parent
d5757a00df
commit
06b90ffe6c
3 files changed
+62
-6
No files matched your search
+49
-5
@@ -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
|
||||
|
||||
|
||||
+4
-1
@@ -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:
|
||||
|
||||
@@ -52,6 +52,15 @@
|
||||
<label for="update_minutes">Update every (minutes)</label>
|
||||
<input type="number" id="update_minutes" name="update_minutes" min="1" value="{{ settings.update_minutes or 2 }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="connection_type">Connections</label>
|
||||
<select id="connection_type" name="connection_type">
|
||||
<option value="all" {{ "selected" if settings.connection_type == "all" or not settings.connection_type }}>All</option>
|
||||
<option value="regional" {{ "selected" if settings.connection_type == "regional" }}>Regional only (no ICE/IC/EC)</option>
|
||||
<option value="long_distance" {{ "selected" if settings.connection_type == "long_distance" }}>Long-distance only (ICE/IC/EC)</option>
|
||||
</select>
|
||||
<p class="hint">"Regional only" hides ICE/IC/EC long-distance trains -- just S-Bahn, regional trains, buses, trams etc.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit">Save</button>
|
||||
|
||||
Reference in new issue
Block a user