diff --git a/screens/journey.py b/screens/journey.py index ffeaa17..6bbcfea 100644 --- a/screens/journey.py +++ b/screens/journey.py @@ -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)