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 0000000..b2c4e69 Binary files /dev/null and b/static/images/favicon.ico differ diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..66399e4 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,199 @@ +/* .header-box { + display: flex; + justify-content: space-between; /* pushes items to opposite sides + align-items: top; /* vertically center them + width: 100%; + height: 12%; + font-size: 120%; +} + +.header-box h1 { + margin: 0 2.5% 2.5% 2.5%; +} */ + +html, body { + /* margin: 0; */ + padding: 0; + height: 100%; + overflow: hidden; /* no scrollbars */ + font-size: 150%; + font-family: "Rubik", sans-serif; + font-optical-sizing: auto; + font-weight: 500; + font-style: normal; + background: #282a35; + color: #899194; +} + +.header-table-container { + width: 95%; + margin: 0 2.5% 1% 2.5%; + height: 12%; + display: flex; + flex-direction: column; + justify-content: center; +} + +.header-table { + width: 100%; + height: 100%; + border-collapse: collapse; + table-layout: fixed; + font-size: 120%; +} + +.header-station { + width: 79%; +} + +/* .header-clock { + width: 15%; +} */ + +.line { + width: 6%; + /* text-align: left; */ + /* padding: 0 0 0 0; */ + vertical-align: 25%; +} + +.destination{ + padding: 100em; +} + +.departure_time { + width: 12%; + text-align: center; +} + +.departure_delay { + width: 10%; + text-align: left; + font-size: 50%; + color: #646a6c +} + +.until_departure { + width: 16%; + text-align: right; +} + +.departure-table-container { + width: 95%; + margin: 0 2.5% 2.5% 2.5%; + height: 83%; + display: flex; + flex-direction: column; + justify-content: center; +} + +.departure-table { + width: 100%; + height: 100%; + border-collapse: collapse; + table-layout: fixed; + font-size: 200%; +} + + +.departure-table th, +.departure-table td { + padding: 0 0.25em 0 0; + /* vertical-align: middle; */ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + /* border: 1px solid #8de8fa; */ + /* height: 12.5%; */ +} + +.departure-table td:last-child, +.departure-table th:last-child { + padding-right: 0; +} + +.line-pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.2em 0.5em; /* relative padding */ + font-weight: bold; + font-size: 0.75em; + border-radius: 9999px; /* pill shape adjusts automatically */ + height: 100%; /* takes most of cell height */ + max-height: 100%; /* don’t overflow cell */ + width: 100%; + max-width: 100%; /* stay within cell */ + aspect-ratio: 2 / 1; + box-sizing: border-box; + white-space: nowrap; /* prevent text wrapping */ + overflow: hidden; /* hide overflow if text is too long */ + text-overflow: ellipsis; /* show "..." if necessary */ +} + +/* Colors for different lines */ +.line-S1 { + background-color: #14bae7; + color: #b5b5b5; +} + +.line-S2 { + background-color: #75b728; + color: #b5b5b5; +} + +.line-S3 { + background-color: #951781; + color: #b5b5b5; +} + +.line-S4 { + background-color: #e30b1b; + color: #b5b5b5; +} + +.line-S5 { + background-color: #00517f; + color: #b5b5b5; +} + +.line-S6 { + background-color: #008c58; + color: #b5b5b5; +} + +.line-S7 { + background-color: #882d22; + color: #b5b5b5; +} + +.line-S8 { + background-color: #000000; + color: #f0a901; +} + +.line-S20 { + background-color: #ea516c; + color: #b5b5b5; +} + +.line-default { + background-color: #bdc3c7; + color: #1f2831; +} + +.delay-high { + color: #dc4d21; +} + +.delay-neg { + color: #5bc812 +} + +.until-low { + color: #dc4d21 +} + +.until-medium { + color: #dc9e21 +} diff --git a/static/update_time.js b/static/update_time.js new file mode 100644 index 0000000..3e86aaf --- /dev/null +++ b/static/update_time.js @@ -0,0 +1,171 @@ +(function () { + + + function updateClock ( clock ) { + clock.innerHTML = new Date().toLocaleTimeString(); + } + + function ensureTableSize(table, x, y) { + // Adjust number of rows + while (table.rows.length < x) { + table.insertRow(); + } + while (table.rows.length > 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 @@ + + +
+
+ {{ station }}+ |
+
+ {{ current_time_str }}+ |
+