Files
bahnhofstafel-puller/main.py
2025-08-23 22:28:04 +02:00

272 lines
8.0 KiB
Python

from dataclasses import dataclass, field
from src.api import Timetable, Stop
import fire
import cv2
from PIL import ImageFont, ImageDraw, Image
import numpy as np
import time
badge_colors = {
"S1": {"fill": "#18bae7", "text": "#C6D3D7"},
"S2": {"fill": "#74b72b", "text": "#C6D3D7"},
"S3": {"fill": "#941d81", "text": "#C6D3D7"},
"S4": {"fill": "#e20b1c", "text": "#C6D3D7"},
"S5": {"fill": "#005280", "text": "#C6D3D7"},
"S6": {"fill": "#008c59", "text": "#C6D3D7"},
"S7": {"fill": "#892f24", "text": "#C6D3D7"},
"S8": {"fill": "#0d0d11", "text": "#eeaa00"},
"S20": {"fill": "#e8526d", "text": "#C6D3D7"},
}
@dataclass
class ImageSetup:
width: int = 1920
height: int = 1080
bg_color: str = "#282a35"
fonts: dict = field(
default_factory=lambda: {
"title": ("./fonts/Rubik-Bold.ttf", 80),
"badge": ("./fonts/Rubik-SemiBold.ttf", 70),
"column_title": ("./fonts/Rubik-Regular.ttf", 50),
"default": ("./fonts/Rubik-Regular.ttf", 60),
}
)
text_colors: dict = field(
default_factory=lambda: {
"title": "#899194",
"column_title": "#555A5C",
"default": "#899194"
}
)
margins: tuple = (60, 60, 60, 60) # t, b, l, r
def denormalize(coords, setup: ImageSetup, use_margins=True):
(x, y) = coords
margins = setup.margins if use_margins else (0, 0, 0, 0)
return (
int(x * (setup.width - margins[2] - margins[3]) + margins[2]),
int(y * (setup.height - margins[0] - margins[1]) + margins[0]),
)
def create_image(tt: Timetable, setup: ImageSetup | None = None):
if setup is None:
setup = ImageSetup()
# img = np.zeros((setup.height, setup.width, 3), dtype=np.uint8)
img = Image.new("RGBA", (setup.width, setup.height), setup.bg_color)
# # fill with background color
# for i, c in enumerate(hex2color(setup.bg_color)):
# img[:, :, i] = int(c * 255)
### setup done ###
draw = ImageDraw.Draw(img)
title_font = ImageFont.truetype(*setup.fonts["title"])
ctitle_font = ImageFont.truetype(*setup.fonts["column_title"])
badge_font = ImageFont.truetype(*setup.fonts["badge"])
default_font = ImageFont.truetype(*setup.fonts["default"])
draw.text(
xy=denormalize((0, 0), setup),
text=tt.station,
font=title_font,
anchor="la",
fill=setup.text_colors["title"]
)
draw.text(
xy=denormalize((0.815, 0), setup),
text=tt.timestamp.strftime(r"%H:%M:%S"),
font=title_font,
anchor="la",
fill=setup.text_colors["title"]
)
line_badge_size = (2*setup.fonts["badge"][1], setup.fonts["badge"][1])
line_height = setup.fonts["badge"][1]*1.75
xy_start = denormalize((0,0.15), setup)
dest_x = denormalize((0.12, 0), setup)[0]
dep_x = denormalize((0.5, 0), setup)[0]
delay_x = denormalize((0.6, 0), setup)[0]
max_dest_len = dep_x - dest_x - 60
until_x = denormalize((0.5+0.5-0.12, 0), setup)[0]
# col_title_y = denormalize((0, 0.15), setup)[1]
# draw.text(
# xy=(dest_x, col_title_y),
# text="Richtung",
# font=ctitle_font,
# anchor="lm",
# fill=setup.text_colors["column_title"],
# )
# draw.text(
# xy=(dep_x, col_title_y),
# text="Zeit",
# font=ctitle_font,
# anchor="lm",
# fill=setup.text_colors["column_title"],
# )
# draw.text(
# xy=(until_x, col_title_y),
# text="fährt in",
# font=ctitle_font,
# anchor="lm",
# fill=setup.text_colors["column_title"],
# )
current_xys_line_badge = [*xy_start, xy_start[0]+line_badge_size[0], xy_start[1]+line_badge_size[1]]
for stop in tt.stops[:min(tt.min_stop_count, len(tt.stops), 7)]:
stop: Stop = stop
draw.rounded_rectangle(
current_xys_line_badge,
radius=min(current_xys_line_badge[2]-current_xys_line_badge[0], current_xys_line_badge[3]-current_xys_line_badge[1])//2,
fill=badge_colors[stop.line]["fill"],
outline=badge_colors[stop.line]["fill"],
width=10
)
center_xy = ((current_xys_line_badge[0]+current_xys_line_badge[2])//2, (current_xys_line_badge[1]+current_xys_line_badge[3])//2)
draw.text(
xy=center_xy,
text=stop.line,
font=badge_font,
anchor="mm",
fill=badge_colors[stop.line]["text"],
)
dest_text :str = stop.destination
dest_text = dest_text.replace("München-", "")
dest_text = dest_text.replace("München ", "")
paren_index = dest_text.find("(")
if paren_index != -1:
dest_text = dest_text[:paren_index]
cutoff = 0
while default_font.getlength(dest_text) > max_dest_len:
dest_text = (dest_text[:len(dest_text)-cutoff+1]+ ("." if cutoff > 0 else "")).replace(" .", "")
cutoff += 1
dep_text = stop.departure_time.strftime(r"%H:%M")
time_until = round((stop.departure_time - tt.timestamp).total_seconds()/60)
until_text = f"{time_until:d} min"
until_color = setup.text_colors["default"]
delay_color = setup.text_colors["column_title"]
dep_color = setup.text_colors["default"]
dest_color = setup.text_colors["default"]
delay_text = ""
if stop.departure_delay is not None:
delay_text = f"{"+" if stop.departure_delay >= 0 else "-"} {abs(stop.departure_delay):d}"
if stop.departure_delay > 5:
delay_color = "#c8683f"
elif stop.departure_delay < 0:
delay_color = "#48c144"
if time_until <= 6:
until_color = "#c8683f"
elif 6 < time_until <= 10:
until_color = "#b79b46"
if stop.departure_canceled:
dep_color = "#c83f4e"
dest_color = "#c83f4e"
dep_text = "Fällt aus :("
until_text = "----"
delay_text = ""
draw.text(
xy=(dest_x, center_xy[1]),
text=dest_text,
font=default_font,
anchor="lm",
fill=dest_color,
)
draw.text(
xy=(dep_x, center_xy[1]),
text=dep_text,
font=default_font,
anchor="lm",
fill=dep_color,
)
draw.text(
xy=(delay_x, center_xy[1]),
text=delay_text,
font=ctitle_font,
anchor="lm",
fill=delay_color,
)
draw.text(
xy=(until_x, center_xy[1]),
text=until_text,
font=default_font,
anchor="lm",
fill=until_color,
)
current_xys_line_badge[1] += line_height
current_xys_line_badge[3] += line_height
# if gif is not None:
# gif = Image.open(gif)
# gif.seek(gif_frame % gif.n_frames)
# frame = gif.convert("RGBA")
# frame_size = frame.size
# frame_anchor = denormalize((1, 1), setup, use_margins=False)
# img.paste(frame, (frame_anchor[0]-frame_size[0], frame_anchor[1]-frame_size[1]), frame)
# last step, convert from rgb to bgr
img = img.convert("RGB")
img = np.array(img)
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
return img
def main(eva=None):
eva = 8002377 if eva is None else eva
# eva = 8098263
# eva = 8004158
cv2.namedWindow("fs", cv2.WND_PROP_FULLSCREEN)
cv2.setWindowProperty("fs", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
# cv2.moveWindow("fs", -1920, 0)
tt = Timetable(eva=eva)
while True:
start = time.time()
tt.get_stops()
img = create_image(tt)
cv2.imshow("fs", img)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
elapsed = time.time() - start
time.sleep(max(0, 1.0 - elapsed))
cv2.destroyAllWindows()
if __name__ == "__main__":
fire.Fire(main)