From ddef1be220d3c0194a39cd825b65d6c31906f8c0 Mon Sep 17 00:00:00 2001 From: Seppl Date: Sun, 24 Aug 2025 22:32:24 +0200 Subject: [PATCH] generate website --- app.py | 58 +++++++ main.py | 296 +++++------------------------------- pyproject.toml | 2 + src/image.py | 238 +++++++++++++++++++++++++++++ src/{api.py => iris_api.py} | 32 ++++ static/images/favicon.ico | Bin 0 -> 34494 bytes static/styles.css | 199 ++++++++++++++++++++++++ static/update_time.js | 171 +++++++++++++++++++++ templates/base.html | 40 +++++ tt_to_image.py | 37 +++++ uv.lock | 124 +++++++++++++++ 11 files changed, 937 insertions(+), 260 deletions(-) create mode 100644 app.py create mode 100644 src/image.py rename src/{api.py => iris_api.py} (86%) create mode 100644 static/images/favicon.ico create mode 100644 static/styles.css create mode 100644 static/update_time.js create mode 100644 templates/base.html create mode 100644 tt_to_image.py diff --git a/app.py b/app.py new file mode 100644 index 0000000..6c3b581 --- /dev/null +++ b/app.py @@ -0,0 +1,58 @@ +from flask import Flask, render_template, url_for, jsonify, send_file +from datetime import datetime +from pathlib import Path + +from src.iris_api import Timetable + +def compile_javascript(base_path = "static"): + js_file_ptrs = list(Path(base_path).glob("*.js")) + js_files = [] + + print(js_file_ptrs) + + for file in js_file_ptrs: + print(file.name) + fname = f"{file.name}" + js_files.append(url_for('static', filename=fname)) + + return js_files + +eva = 8002377 + +app = Flask(__name__) + +@app.route("/favicon.ico") +def favicon(): + return send_file("static/images/favicon.ico") + +@app.route("/") +def main_page(): + # print(compile_javascript()) + + return render_template( + 'base.html', + title="testin", + station="Gröbenzell", + current_time_str=datetime.now().strftime(r"%H:%M:%S"), + js_files=compile_javascript() + ) + +# TODO: add a text entry field to change the EVA +# @app.route("/setup") +# def setup(): +# return "" + +@app.route("/update", methods=['POST']) +def update(): + tt = Timetable(eva) + + tt.get_stops() + + table_data = tt.str_stops() + # print(table_data[0]) + + + result = {'status': 'success'} + # result.update(data) + result["table_data"] = table_data + return jsonify(result) \ No newline at end of file diff --git a/main.py b/main.py index 8b43248..432b863 100644 --- a/main.py +++ b/main.py @@ -1,271 +1,47 @@ -from dataclasses import dataclass, field -from src.api import Timetable, Stop - +from flask import Flask import fire -import cv2 -from PIL import ImageFont, ImageDraw, Image -import numpy as np -import time +from htpy import body, h1, head, html, title, table, tr, td, div, script -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"}, -} +app = Flask(__name__) -@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), - } +def run_server(debug=False): + app.run(debug=debug) + +@app.route("/") +def main_page(): + return build_page(None) + +def build_page(tt, debug=True): + _ = tt + from datetime import datetime + + lines = ( + ("S1", "somewhere", datetime.now().strftime(r"%H:%M:%S"), 4, "3 min"), + ("S2", "else", datetime.now().strftime(r"%H:%M:%S"), 4, "3 min"), ) - text_colors: dict = field( - default_factory=lambda: { - "title": "#899194", - "column_title": "#555A5C", - "default": "#899194" - } - ) - margins: tuple = (60, 60, 60, 60) # t, b, l, r + retval = html[ + head[title["today's menu"]], + body[ + div(style="clear: both")[ + h1(style="float: left")["Gröbenzell"], + h1(style="float: right", id="clock")[datetime.now().strftime(r"%H:%M:%S")], + ], + table[ + (tr[ + (td[element] for element in line) + ] for line in lines) + ], + ] + ] + retval = str(retval) -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 debug: + print(retval) + return retval if __name__ == "__main__": - fire.Fire(main) + fire.Fire(run_server) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0619329..1749755 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,8 @@ dependencies = [ "cachetools>=6.1.0", "datetime>=5.5", "fire>=0.7.1", + "flask>=3.1.2", + "htpy>=25.8.1", "opencv-contrib-python>=4.12.0.88", "opencv-python>=4.12.0.88", "pillow>=11.3.0", diff --git a/src/image.py b/src/image.py new file mode 100644 index 0000000..34b9e0d --- /dev/null +++ b/src/image.py @@ -0,0 +1,238 @@ +from dataclasses import dataclass, field +from iris_api import Timetable, Stop + +import cv2 +from PIL import ImageFont, ImageDraw, Image +import numpy as np + +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 \ No newline at end of file diff --git a/src/api.py b/src/iris_api.py similarity index 86% rename from src/api.py rename to src/iris_api.py index 902bbe6..e58ce42 100644 --- a/src/api.py +++ b/src/iris_api.py @@ -62,6 +62,26 @@ class Stop: else: return "" + def to_dict(self): + return { + "id": self.id, + "line": self.line, + "departure_time": self.departure_time.timestamp(), + "old_departure_time": self.old_departure_time.timestamp(), + "departure_delay": 0 if self.departure_delay is None else self.departure_delay, + "departure_canceled": self.departure_canceled, + "departure_cancellation_time": -1 if self.departure_cancellation_time is None else self.departure_cancellation_time.timestamp(), + "platform": self.platform, + "path": self.path, + "destination": self.destination, + "origin": "" if self.origin is None else self.origin, + "arrival_time": -1 if self.arrival_time is None else self.arrival_time.timestamp(), + "old_arrival_time": -1 if self.old_arrival_time is None else self.old_arrival_time.timestamp(), + "arrival_delay": 0 if self.arrival_delay is None else self.arrival_delay, + "arrival_canceled": self.arrival_canceled, + "arrival_cancellation_time": -1 if self.arrival_cancellation_time is None else self.arrival_cancellation_time.timestamp(), + } + def __repr__(self): if not self.departure_canceled: return f"{self.line} to {self.destination}: {self.departure_time.strftime(r"%Y-%m-%d %H:%M")}{f" ({self.departure_delay:+d})" if self.departure_delay is not None else ""} @ platform {self.platform}" @@ -90,6 +110,18 @@ class Timetable: self.station = changes['timetable']['@station'] return changes['timetable']['s'] + + def str_stops(self): + stops = [] + for stop in self.stops: + stop_dict = stop.to_dict() + stop_dict["until_departure"] = stop_dict["departure_time"] - self.timestamp.timestamp() + stop_dict["until_arrival"] = stop_dict["arrival_time"] - self.timestamp.timestamp() + stop_dict["timestamp"] = self.timestamp.timestamp() + stops.append(stop_dict) + return stops + + def fetch_actual(self, stop: Stop): changes = self.get_changes() for event in changes: diff --git a/static/images/favicon.ico b/static/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b2c4e69df4023651ef8a5f8bfe1c45a4c63b0b22 GIT binary patch literal 34494 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 x) { + table.deleteRow(table.rows.length - 1); + } + + // Ensure each row has y cells + for (let row of table.rows) { + while (row.cells.length < y) { + row.insertCell(); + } + while (row.cells.length > y) { + row.deleteCell(row.cells.length - 1); + } + } + } + + function formatWithSign(num) { + if (num > 0) { + return `+ ${num}`; + } else if (num < 0) { + return `- ${Math.abs(num)}`; + } else { + return ""; + } + } + + function cssClassExists(className) { + return Array.from(document.styleSheets).some(sheet => { + try { + return Array.from(sheet.cssRules).some(rule => + rule.selectorText === `.${className}` + ); + } catch (e) { + // Ignore CORS-restricted stylesheets + return false; + } + }); + } + + function redraw_table(data, table) { + // const res = JSON.parse(result) + const columns = { + "line": 0, + "destination": 1, + "departure_time": 2, + "departure_delay": 3, + "until_departure": 4 + } + + // console.log(data) + + ensureTableSize(table, Math.min(data.length, 8), 5); + + for (let i = 0; i < Math.min(data.length, 8); i++) { + const row = table.rows[i]; + // row.classList.add("departure-table-row") + // row.style.height = `$10%`; + const train = Object.entries(data[i]); + for (let [key, value] of train) { + if (key in columns) { + const cell = row.cells[columns[key]]; // Get the cell once + cell.classList.add(key); // Add class based on column name + if (key == "line") { + // cell.textContent = String(value); + const lineClass = `line-${value}`; + const appliedClass = cssClassExists(lineClass) ? lineClass : "line-default"; + cell.innerHTML = `${value}`; + const pill = document.querySelector('.line-pill'); + + const cellHeight = cell.clientHeight; + const cellWidth = cell.clientWidth; + + // Set maximum sizes dynamically + pill.style.maxWidth = `${cellHeight * 2}px`; + pill.style.maxHeight = `${cellWidth * 0.5}px`; + } + if (key == "destination") { + dest = String(value); + dest = dest.replace(/\s*\(.*?\)\s*/g, "") + .replace(/^München\s*/i, "") + .replace(/^[\s-]+|[\s-]+$/g, ""); + cell.textContent = dest; + } + if (key == "departure_time") { + if (train["canceled"]) { + cell.textContent = "Fällt aus :("; + cell.classList.add("train-canceled"); + } else { + const date = new Date(value*1000); + cell.textContent = date.toLocaleTimeString("de-DE", + { + hour: "2-digit", + minute: "2-digit" + } + ); + } + } + if (key == "departure_delay") { + if (train["canceled"]) { + cell.textContent = ""; + } else { + cell.textContent = formatWithSign(value); + if (value >= 5) { + cell.classList.add("delay-high"); + } else if (value < 0) { + cell.classList.add("delay-neg"); + } + } + } + if (key == "until_departure") { + if (train["canceled"]) { + cell.textContent = ""; + } else { + const minutes = Math.round(value/60); + cell.textContent = String(minutes) + " min"; + if (minutes <= 6) { + cell.classList.add("until-low"); + } else if (minutes <= 10) { + cell.classList.add("until-medium"); + } + } + } + // cell.innerHTML.classList.add("scrolling-text") + } + } + } + // document.querySelector('.departure-table') + // .style.setProperty('--row-count', table.rows.length); + } + + function updateTable( table ){ + fetch('/update', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + // TODO: pass number of rows we want + }) + .then(response => response.json()) + .then(result => { + // console.log(result); + redraw_table(result.table_data, table); + // const res = JSON.parse(result); + // console.log(res.table_data); + // for (const elem of res.table_data) { + // console.log(elem); + // } + }); + } + + var clockElement = document.getElementById( "clock" ); + var tableElement = document.getElementById( "departures" ); + + setInterval(function () { + updateClock( clockElement ); + updateTable( tableElement); + }, 1000); + +}()); + diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..ce3ee47 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,40 @@ + + + + Abfahrten {{ station }} + + + + + + +
+ + + + + + +
+ +
+
+
+ + {% for file in js_files %} + + {% endfor %} + diff --git a/tt_to_image.py b/tt_to_image.py new file mode 100644 index 0000000..da470cb --- /dev/null +++ b/tt_to_image.py @@ -0,0 +1,37 @@ +from iris_api import Timetable + +from src.image import create_image + +import fire + +import cv2 +import time + +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) diff --git a/uv.lock b/uv.lock index 13316b3..526f07c 100644 --- a/uv.lock +++ b/uv.lock @@ -10,6 +10,8 @@ dependencies = [ { name = "cachetools" }, { name = "datetime" }, { name = "fire" }, + { name = "flask" }, + { name = "htpy" }, { name = "opencv-contrib-python" }, { name = "opencv-python" }, { name = "pillow" }, @@ -23,6 +25,8 @@ requires-dist = [ { name = "cachetools", specifier = ">=6.1.0" }, { name = "datetime", specifier = ">=5.5" }, { name = "fire", specifier = ">=0.7.1" }, + { name = "flask", specifier = ">=3.1.2" }, + { name = "htpy", specifier = ">=25.8.1" }, { name = "opencv-contrib-python", specifier = ">=4.12.0.88" }, { name = "opencv-python", specifier = ">=4.12.0.88" }, { name = "pillow", specifier = ">=11.3.0" }, @@ -31,6 +35,15 @@ requires-dist = [ { name = "xmltodict", specifier = ">=0.14.2" }, ] +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + [[package]] name = "cachetools" version = "6.1.0" @@ -80,6 +93,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + [[package]] name = "datetime" version = "5.5" @@ -105,6 +139,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/4c/93d0f85318da65923e4b91c1c2ff03d8a458cbefebe3bc612a6693c7906d/fire-0.7.1-py3-none-any.whl", hash = "sha256:e43fd8a5033a9001e7e2973bab96070694b9f12f2e0ecf96d4683971b5ab1882", size = 115945, upload-time = "2025-08-16T20:20:22.87Z" }, ] +[[package]] +name = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, +] + +[[package]] +name = "htpy" +version = "25.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/bc/58f4b835e1ffec9441aa92f97d3ce38148fff7e46507e05f2436098db6dc/htpy-25.8.1.tar.gz", hash = "sha256:ba4ac83307cbcbf5cb2b08111f75bb978693ede1624bac75da01208882b3c48d", size = 287611, upload-time = "2025-08-15T11:34:23.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/27/67b73c2399e1f9a27d4fa8ed6f0506302b927afc5e9c4d0e2c27ceeb0520/htpy-25.8.1-py3-none-any.whl", hash = "sha256:184dc98cb39e1157fcc66ce930e5dad222955b2d97c33612277a79d7b5d8ded3", size = 18945, upload-time = "2025-08-15T11:34:22.07Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -114,6 +177,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -126,6 +210,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -325,6 +437,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, ] +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, +] + [[package]] name = "xmltodict" version = "0.14.2"