Journey: stacked header, bus/train icons, drop gray text

- Origin/destination now on their own lines instead of one truncated
  "A -> B" line, which cut off the destination entirely on the small
  panel.
- Each leg gets a colored dot (blue = train, green = bus), using the
  panel's exact palette colors so they render solid, not dithered.
  Mode is read from EFA's product.name (confirmed live: "StadtBus"/
  "RegioBus"/"SchnellBus" vs "S-Bahn"/"Regionalzug"/"Zug").
- Detail-line text switched from dithered gray to solid black -- the
  gray was hard to read at the small font size the panel renders at.
This commit is contained in:
Honey committed 2026-07-23 10:00:36 +02:00
1 parent 06b90ffe6c
commit 080cb11501
1 file changed
+67 -22
+67 -22
View File
@@ -56,7 +56,12 @@ REQUEST_HEADERS = {
RED = (200, 0, 0)
BLACK = (0, 0, 0)
MUTED = (110, 110, 110)
# Pure panel colors (from the epd4in0e's 6-color quantization palette in
# display/waveshare_epd/epd4in0e.py) -- used for the train/bus mode dots so
# they render as solid color on the real hardware instead of dithering like
# an off-palette RGB value would.
BLUE = (0, 0, 255)
GREEN = (0, 255, 0)
# 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
@@ -228,8 +233,16 @@ class JourneyScreen(Screen):
)
if is_walk:
continue
# EFA's product.name is a reliable human-readable mode string
# ("StadtBus", "RegioBus", "SchnellBus", "Niederflurbus" for
# buses; "S-Bahn", "Regionalzug", "Zug" for trains) -- more
# robust than product.class, whose numeric values aren't a
# clean bus/train split (bus classes 5-7 sit right next to
# S-Bahn's class 1). Confirmed live against VRR.
is_bus = "bus" in (product.get("name") or "").lower()
legs.append({
"line": transportation.get("name") or "?",
"mode": "bus" if is_bus else "train",
"origin": leg.get("origin") or {},
"destination": leg.get("destination") or {},
"infos": leg.get("infos") or [],
@@ -270,7 +283,7 @@ class JourneyScreen(Screen):
"departure": dep_actual.astimezone().strftime("%H:%M"),
"delay_min": max(0, delay_min),
"arrival": arr_actual.astimezone().strftime("%H:%M") if arr_actual else "--:--",
"lines": [leg["line"] for leg in legs],
"legs": [{"name": leg["line"], "mode": leg["mode"]} for leg in legs],
"transfers": len(legs) - 1,
"platform": (first["origin"].get("properties") or {}).get("platformName") or "",
"notice": "; ".join(notice_titles),
@@ -297,11 +310,18 @@ class JourneyScreen(Screen):
pad = width * 0.04
origin = settings.get("origin_label") or settings.get("origin_query") or "?"
destination = settings.get("destination_label") or settings.get("destination_query") or "?"
header_font = load_font(size=int(scale * 0.05), bold=True)
header = truncate_to_width(draw, f"{origin} → {destination}", header_font, width - 2 * pad)
draw.text((pad, height * 0.045), header, font=header_font, fill=BLACK)
# Origin/destination stacked on their own lines rather than one
# "A -> B" line -- on the small panel a single combined line
# truncated too early to show the destination at all.
header_font = load_font(size=int(scale * 0.045), bold=True)
origin_line = truncate_to_width(draw, origin, header_font, width - 2 * pad)
dest_line = truncate_to_width(draw, f"→ {destination}", header_font, width - 2 * pad)
line_gap = header_font.size * 1.22
header_top = height * 0.035
draw.text((pad, header_top), origin_line, font=header_font, fill=BLACK)
draw.text((pad, header_top + line_gap), dest_line, font=header_font, fill=BLACK)
header_bottom = height * 0.045 + header_font.size * 1.35
header_bottom = header_top + line_gap * 2
draw.rectangle((pad, header_bottom, width - pad, header_bottom + 2), fill=BLACK)
row_top = header_bottom + height * 0.03
@@ -325,31 +345,56 @@ class JourneyScreen(Screen):
dep_w = text_width(draw, dep_text, time_font)
arrow_x = pad + dep_w + scale * 0.02
draw.text((arrow_x, top_line_y + (time_font.size - arrow_font.size) / 2), "→", font=arrow_font, fill=MUTED)
draw.text((arrow_x, top_line_y + (time_font.size - arrow_font.size) / 2), "→", font=arrow_font, fill=BLACK)
arrow_w = text_width(draw, "→", arrow_font)
draw.text((arrow_x + arrow_w + scale * 0.02, top_line_y), journey["arrival"], font=time_font, fill=BLACK)
detail_parts = [" → ".join(journey["lines"])]
if journey["transfers"] > 0:
detail_parts.append(f"{journey['transfers']}x umsteigen")
if journey["platform"]:
detail_parts.append(f"Gl. {journey['platform']}")
if journey["notice"]:
detail_parts.append(journey["notice"])
# Detail line: a colored dot per leg (blue = train, green = bus --
# pure panel colors, no dithering) followed by its line name, then
# transfer/platform/notice info. Drawn as manually-placed segments
# instead of one joined string so each leg can carry its own dot.
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
right_edge = width - pad
icon_d = detail_font.size * 0.55
cursor_x = pad
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)
def draw_segment(text, color=BLACK, avail=None):
nonlocal cursor_x
if avail is not None:
text = truncate_to_width(draw, text, detail_font, avail)
draw.text((cursor_x, detail_y), text, font=detail_font, fill=color)
cursor_x += text_width(draw, text, detail_font)
return text
for idx, leg in enumerate(journey["legs"]):
if cursor_x >= right_edge - icon_d:
break
dot_color = BLUE if leg["mode"] == "train" else GREEN
dot_top = detail_y + (detail_font.size - icon_d) / 2
draw.ellipse((cursor_x, dot_top, cursor_x + icon_d, dot_top + icon_d), fill=dot_color)
cursor_x += icon_d + scale * 0.01
shown = draw_segment(leg["name"], avail=right_edge - cursor_x)
if idx < len(journey["legs"]) - 1 and cursor_x < right_edge and shown == leg["name"]:
draw_segment(" → ", avail=right_edge - cursor_x)
trailing_parts = []
if journey["transfers"] > 0:
trailing_parts.append(f"{journey['transfers']}x umsteigen")
if journey["platform"]:
trailing_parts.append(f"Gl. {journey['platform']}")
if trailing_parts and cursor_x < right_edge:
draw_segment(" · " + " · ".join(trailing_parts), avail=right_edge - cursor_x)
if journey["notice"] and cursor_x < right_edge - icon_d:
cursor_x += scale * 0.01
draw_segment("⚠ ", color=RED)
draw_segment(journey["notice"], avail=right_edge - cursor_x)
return image
def _render_message(self, draw, width, height, scale, msg):
font = load_font(size=int(scale * 0.06))
mw = text_width(draw, msg, font)
draw.text(((width - mw) / 2, height / 2), msg, font=font, fill=MUTED)
draw.text(((width - mw) / 2, height / 2), msg, font=font, fill=BLACK)