diff --git a/prog/communication.py b/prog/communication.py
index ea79b6f95fb1d3bd39c226305dc234d9d16a91cc..d67e30f66e6fc26672a5c0c196fb09fb5d5ea1a4 100644
--- a/prog/communication.py
+++ b/prog/communication.py
@@ -212,9 +212,6 @@ def server_exec():
         return l
     return decorator
 
-dt_pinf = datetime.datetime(2100, 1, 1, tzinfo=local_timezone)
-dt_minf = datetime.datetime(2000, 1, 1, tzinfo=local_timezone)
-
 def path_to_dt(path):
     path = path.split('.')[0]
     path = path.replace('/', '-')
diff --git a/prog/data_utils.py b/prog/data_utils.py
index c600d49c892bd6bbbf0cdc05e9d3d17c9d1ca4a2..2ffa9deed15f08fb43d923cf76f855611321087c 100644
--- a/prog/data_utils.py
+++ b/prog/data_utils.py
@@ -203,6 +203,7 @@ class TripHistory:
     def add_preprocessed_data(self, data):
         for x in data["history"]:
             self.history.append(HistoryPointFromPreprocessed(x))
+            self.history.sort(key=lambda hp: hp.first_captured)
 
 
 
diff --git a/prog/main.py b/prog/main.py
index 696283a621021266a6f32a9eccd56ad6532746d7..7554ea46703febc0cab9655cc4ad1a465b03f03b 100755
--- a/prog/main.py
+++ b/prog/main.py
@@ -26,6 +26,8 @@ import sys, os
 import pprint
 import gtfs
 
+import math
+
 
 window_title_prefix = "PID realtime"
 
@@ -37,46 +39,56 @@ crs_wgs=QgsCoordinateReferenceSystem(4326)
 crs_wgs_mercator=QgsCoordinateReferenceSystem(3857)
 crs_transform = QgsCoordinateTransform(crs_wgs, crs_wgs_mercator, QgsProject.instance())
 
-live_subcribes = set()
+live_subscribers = set()
 live_data = None
+live_data_by_trip_id = None
 live_dt = None
 
+def error(s):
+    eprint("ERROR:", s)
+
 fetch_live_task: asyncio.Task = None
 
 async def fetch_live():
-    global live_data, live_dt
+    global live_data, live_dt, live_data_by_trip_id
     eprint("Hi from fetch_live")
     c = await get_communication()
     r = await c.get_last_data()
     live_data = await unzip_parse(r[1])
+    live_data_by_trip_id = {x["properties"]["trip"]["gtfs"]["trip_id"]: x for x in live_data["features"]}
     live_dt = r[0]
-    for f in live_subcribes:
-        asyncio.create_task(f(live_dt, live_data))
+    for f in live_subscribers:
+        asyncio.create_task(f(live_dt, live_data, live_data_by_trip_id))
 
     while True:
         eprint("Start wait next")
         r = await c.wait_next_data(live_dt)
         eprint("Done wait next")
         live_data = await unzip_parse(r[1])
+        live_data_by_trip_id = {x["properties"]["trip"]["gtfs"]["trip_id"]: x for x in live_data["features"]}
         live_dt = r[0]
-        for f in live_subcribes:
-            asyncio.create_task(f(live_dt, live_data))
+        for f in live_subscribers:
+            asyncio.create_task(f(live_dt, live_data, live_data_by_trip_id))
 
 
 def spawn_or_remove_fetch_live_task():
     global live_data, live_dt, fetch_live_task
     eprint("spawn_or_remove_fetch_live_task")
-    if fetch_live_task and len(live_subcribes) == 0:
+    if fetch_live_task and len(live_subscribers) == 0:
         fetch_live_task.cancel()
         fetch_live_task = None
         live_data = None
         live_dt = None
-    if not fetch_live_task and len(live_subcribes):
+    if not fetch_live_task and len(live_subscribers):
         fetch_live_task = asyncio.create_task(catch_all(fetch_live()))
 
 
 
 class ClickableLabel(QLabel):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.setStyleSheet ("color: blue; text-decoration: underline;");
+
     clicked = pyqtSignal()
     def mouseReleaseEvent(self, event):
         super(ClickableLabel, self).mouseReleaseEvent(event)
@@ -108,6 +120,7 @@ async def gtfs_default_data_getter(date, filename):
 
 gtfs.default_data_getter = gtfs_default_data_getter
 data_utils.get_communication = get_communication
+graph.get_communication = get_communication
 
 def layer_updated(layer):
     prov = layer.dataProvider()
@@ -157,7 +170,7 @@ class TimeControllToolBar(QToolBar):
         wnd.shown_data_changed_signal.connect(self.time_changed)
 
     def time_changed(self):
-        self._time_label.setText(self.wnd.current_data_capture_time.strftime("%Y-%m-%d %H:%M:%S") if self.wnd.current_data_capture_time else "")
+        self._time_label.setText(format_dt_to_user(self.wnd.current_data_capture_time) if self.wnd.current_data_capture_time else "")
 
 
 class FilterWidget(QWidget):
@@ -255,15 +268,15 @@ class MainWind(QMainWindow):
         self._tmp2 = QPushButton()
         self._tmp2.clicked.connect(self.tmp2)
 
-        dockWidget = QDockWidget("Dock Widget")
-        dockWidget.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
-        dockWidget.setWidget(self._tmp)
-        self.addDockWidget(Qt.LeftDockWidgetArea, dockWidget)
+        # dockWidget = QDockWidget("Dock Widget")
+        # dockWidget.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
+        # dockWidget.setWidget(self._tmp)
+        # self.addDockWidget(Qt.LeftDockWidgetArea, dockWidget)
 
-        dockWidget = QDockWidget("Dock Widget")
-        dockWidget.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
-        dockWidget.setWidget(self._tmp2)
-        self.addDockWidget(Qt.LeftDockWidgetArea, dockWidget)
+        # dockWidget = QDockWidget("Dock Widget")
+        # dockWidget.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
+        # dockWidget.setWidget(self._tmp2)
+        # self.addDockWidget(Qt.LeftDockWidgetArea, dockWidget)
 
         self._time_controll = TimeControllToolBar(self)
         toolbar = self.addToolBar(self._time_controll)
@@ -284,13 +297,15 @@ class MainWind(QMainWindow):
 
     @asyncSlot()
     async def tmp2(self):
-        dt = datetime.datetime(2024, 7, 4, 8, 0, 0)
+        dt = datetime.datetime(2024, 7, 16, 8, 0, 0)
         g = gtfs.for_date(dt)
         await g.load_trips()
-        await g.load_stops_for_all_trips()
+        #await g.load_stops_for_all_trips()
         trips = [Trip(t.id, dt.date()) for t in g.trips_by_routes["L22"] if t.direction == 1]
         ths = {t.trip_id: TripHistory(t) for t in trips}
 
+        main_th = ths['22_28258_240701']
+
         c = await get_communication()
         #dts = await c.list_realtime_data(datetime.datetime.combine(dt.date(), datetime.time(8,0,0)), datetime.datetime.combine(dt.date(), datetime.time(9,0,0)))
         #for dt in dts:
@@ -306,7 +321,7 @@ class MainWind(QMainWindow):
         #                ths[trip_id].add_history_point(dt, dato)
         #        break
 
-        data = await c.get_preprocessed_data(dt, "22")
+        data, source_timestamps = await c.get_preprocessed_data(dt, "22")
         print("P1")
         data = await unzip_parse(data)
         print("P3")
@@ -319,16 +334,15 @@ class MainWind(QMainWindow):
 
 
 
-        for th in ths.values():
-            await th.load_stops()
+        await main_th.load_stops(await c.gtfs_get_stop_times(datetime.datetime.combine(main_th.trip.date, datetime.time()), main_th.trip.trip_id)) # HACK: Date is not CBOR supported type
+        #for th in ths.values():
+            #await th.load_stops()
 
         print("P5")
 
-        main_th = list(ths.values())[51]
-        history_graph = graph.HistoryGraph(main_th.stops)
-        print("P6")
-        for th in ths.values():
-            history_graph.add_trip(th)
+        history_graph = graph.HistoryGraph(main_th.stops, "22", dt-datetime.timedelta(hours=2), dt)
+
+        history_graph.add_trip(main_th)
         print("P7")
         history_graph.show()
         tmp_windows[history_graph]=history_graph
@@ -367,9 +381,10 @@ class MainWind(QMainWindow):
         c = await get_communication()
         r = await c.get_next_data(self.current_data_capture_time)
         if r is None:
-            ...
+            error("Couldn't move to next data, you are on the last data.")
         else:
             data = await unzip_parse(r[1])
+            await gtfs.for_date(r[0]).load_stops()
             self.show_data(data, r[0])
 
     @asyncSlot()
@@ -378,9 +393,10 @@ class MainWind(QMainWindow):
         c = await get_communication()
         r = await c.get_prev_data(self.current_data_capture_time)
         if r is None:
-            ...
+            error("Couldn't move to previous data, you are on the first data.")
         else:
             data = await unzip_parse(r[1])
+            await gtfs.for_date(r[0]).load_stops()
             self.show_data(data, r[0])
 
     @asyncSlot()
@@ -399,27 +415,39 @@ class MainWind(QMainWindow):
 
     async def start_live(self):
         if live_data is not None:
+            await gtfs.for_date(live_dt).load_stops()
             self.show_data(live_data, live_dt)
-        live_subcribes.add(self.live_data_arrived)
+        live_subscribers.add(self.live_data_arrived)
         spawn_or_remove_fetch_live_task()
 
     async def stop_live(self):
-        live_subcribes.remove(self.live_data_arrived)
+        live_subscribers.remove(self.live_data_arrived)
         spawn_or_remove_fetch_live_task()
 
 
 
-    async def live_data_arrived(self, dt, data):
+    async def live_data_arrived(self, dt, data, _):
         if self.showing_live:
+            await gtfs.for_date(dt).load_stops()
             self.show_data(data, dt)
         else:
             pprint("live data arrived but we are not in live mode")
 
 
+    async def load_data_near(self, dt):
+        c = await get_communication()
+        dts = await c.list_realtime_data(dt-datetime.timedelta(seconds=20), dt+datetime.timedelta(seconds=20))
+        if not len(dts):
+            error("No data near {format_dt_to_user(dt)}")
+        else:
+            near_dt = min(dts, key=lambda x: abs((x - dt).total_seconds()))
+            await self.load_data(near_dt)
+
     async def load_data(self, dt):
         await self.set_live(False)
         c = await get_communication()
         data = await unzip_parse(await c.get_data(dt))
+        await gtfs.for_date(dt).load_stops()
         self.show_data(data, dt)
 
     def show_data(self, data, capture_time):
@@ -439,9 +467,12 @@ class MainWind(QMainWindow):
         self.show_parsed_data()
 
     def show_parsed_data(self):
-        feats = []
+        g = gtfs.for_date_cache.get(self.current_data_capture_time.date(), None)
+        stops = g.stops or {} if g else {}
         current_filtered_data = [tp for tp in self.current_data if self._filter.filter(tp)]
 
+        feats = []
+
         self.current_filtered_data = current_filtered_data
 
         layer_fields = self.data_layer.fields()
@@ -460,7 +491,13 @@ class MainWind(QMainWindow):
             else:
                 feat["vehicle_type"] = str(dato_vehicle_type["description_cs"])
             feat["oldness"] = (self.current_data_capture_time - tp.origin_timestamp).total_seconds()
-            # feat["rotation"] 
+            last_stop = stops.get(tp.json["properties"]["last_position"]["last_stop"]["id"], None)
+            next_stop = stops.get(tp.json["properties"]["last_position"]["next_stop"]["id"], None)
+            if last_stop and next_stop:
+                lat = next_stop.lat - last_stop.lat
+                lon = next_stop.lon - last_stop.lon
+                atan = math.atan2(lat, lon*data_utils.lon_muntiplicator)/math.pi*180
+                feat["rotation"] = (360 - atan) % 360
             feats.append(feat)
 
         self.data_prov.truncate()
@@ -708,17 +745,126 @@ class TripPointSelector(QWidget):
 
 tmp_windows = {}
 
+class BoldLabel(QLabel):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        f = self.font()
+        f.setBold(True)
+        self.setFont(f)
+
+class TimeLabel(ClickableLabel):
+    def __init__(self, mwnd, dt=None):
+        super().__init__()
+        self.set_dt(dt)
+        self.clicked.connect(self.on_click)
+        self.mwnd = mwnd
+
+    def set_dt(self, dt):
+        if dt:
+            self.setText(format_dt_to_user(dt))
+        self.dt = dt
+
+
+    @asyncSlot()
+    async def on_click(self):
+        if self.dt:
+            await self.mwnd.load_data_near(self.dt)
+
+
+
 class TripPointWindow(QWidget):
     def __init__(self, tp, mwnd):
         super().__init__()
         self.mwnd = mwnd
         self._layout = QVBoxLayout()
+
+        self._info_layout = QGridLayout()
+        self._layout.addLayout(self._info_layout)
+        i = 0
+
+        def add_info_widgets(title, val=None):
+            nonlocal i
+            self._info_layout.addWidget(title, i, 0)
+            if val:
+                self._info_layout.addWidget(val, i, 1)
+            i += 1
+
+        self._info_capture_time = TimeLabel(self.mwnd)
+        add_info_widgets(QLabel("Capture time"), self._info_capture_time)
+        self._info_first_capture_time = TimeLabel(self.mwnd)
+        add_info_widgets(QLabel("First capture time"), self._info_first_capture_time)
+        self._info_last_capture_time = TimeLabel(self.mwnd)
+        add_info_widgets(QLabel("Last capture time"), self._info_last_capture_time)
+        self._info_origin_time = TimeLabel(self.mwnd)
+        add_info_widgets(QLabel("Origin time"), self._info_origin_time)
+
+        add_info_widgets(BoldLabel("Trip"))
+        self._info_headsig = QLabel()
+        add_info_widgets(QLabel("Headsign"), self._info_headsig)
+        self._info_trip_id = QLabel()
+        add_info_widgets(QLabel("Trip ID"), self._info_trip_id)
+        self._info_trip_name = QLabel()
+        add_info_widgets(QLabel("Trip name"), self._info_trip_name)
+        self._info_route = QLabel()
+        add_info_widgets(QLabel("Route"), self._info_route)
+        self._info_agency = QLabel()
+        add_info_widgets(QLabel("Agency"), self._info_agency)
+        self._info_sequence = QLabel()
+        add_info_widgets(QLabel("Sequence"), self._info_sequence)
+        self._info_vehicle_type = QLabel()
+        add_info_widgets(QLabel("Vehicle"), self._info_vehicle_type)
+        self._info_start_timestamp = TimeLabel(self.mwnd)
+        add_info_widgets(QLabel("Start timestamp"), self._info_start_timestamp)
+        self._info_start_timestamp = TimeLabel(self.mwnd)
+        add_info_widgets(QLabel("Start "), self._info_start_timestamp)
+
+        def add_stop_info():
+            class StopInfo:
+                ...
+            obj = StopInfo()
+            obj.name = QLabel()
+            add_info_widgets(QLabel("Name"), obj.name)
+            obj.id = QLabel()
+            add_info_widgets(QLabel("Id"), obj.id)
+            obj.arrival = TimeLabel(self.mwnd)
+            add_info_widgets(QLabel("Arrival"), obj.arrival)
+            obj.departure = TimeLabel(self.mwnd)
+            add_info_widgets(QLabel("Departure"), obj.departure)
+            return obj
+
+
+        add_info_widgets(BoldLabel("OpenAPI Last stop"))
+        self._info_last_stop = add_stop_info()
+        add_info_widgets(BoldLabel("OPpenAPI Next stop"))
+        self._info_next_stop = add_stop_info()
+
+        add_info_widgets(BoldLabel("OpenAPI delay"))
+        self._info_openapi_delay_arrival = QLabel()
+        add_info_widgets(QLabel("Last stop arrival"), self._info_openapi_delay_arrival)
+        self._info_openapi_delay_departure = QLabel()
+        add_info_widgets(QLabel("Last stop departure"), self._info_openapi_delay_departure)
+        self._info_openapi_delay = QLabel()
+        add_info_widgets(QLabel("Now"), self._info_openapi_delay)
+
+        add_info_widgets(BoldLabel("Shape & position"))
+        self._info_state = QLabel()
+        add_info_widgets(QLabel("State"), self._info_state)
+        self._info_shape_dist = QLabel()
+        add_info_widgets(QLabel("Shape dist"), self._info_shape_dist)
+        self._info_openapi_shape_dist = QLabel()
+        add_info_widgets(QLabel("OpenAPI shape dist"), self._info_openapi_shape_dist)
+
+
+
+        #self._info_rotation = QLabel()
+        #add_info_widgets(QLabel("Rotation"), self._info_rotation)
+
+        del i
+
         self.setLayout(self._layout)
         self.tp = tp
-        self._tmp = SmallTripPointLabel(tp)
-        self._layout.addWidget(self._tmp)
         self.setLayout(self._layout)
-        self.setWindowTitle(f"{window_title_prefix} - trip point TODO")
+        self.setWindowTitle(f"{window_title_prefix} - trip point")
         self.show()
         tmp_windows[self] = self
 
@@ -726,16 +872,93 @@ class TripPointWindow(QWidget):
         self._layout.addWidget(self._plot_history_button)
         self._plot_history_button.clicked.connect(self.plot_history)
 
-        self._json = QLabel(pprint.pformat(tp.json))
-        self._layout.addWidget(self._json)
+        self._show_json = QPushButton("Show json")
+        self._layout.addWidget(self._show_json)
+        self._show_json.clicked.connect(self.show_json)
+
+
+        self.fill_data()
+
+    def show_json(self):
+        w = QTextEdit()
+        w.setAcceptRichText(False)
+        w.setText(pprint.pformat(self.tp.json))
+        w.resize(600,700);
+        w.setWindowTitle(f"{window_title_prefix} - raw json")
+        tmp_windows[w] = w
+        w.show()
+
+
+    @asyncSlot()
+    async def fill_data(self):
+        tp = self.tp
+
+        self._info_origin_time.set_dt(tp.origin_timestamp)
+        self._info_capture_time.set_dt(tp.capture_time)
+        #self._info_origin_time.setText(self._info_origin_time.text() + (tp.capture_time - tp.origin_timestamp).total_seconds()} s before capture")
+
+        self._info_openapi_delay.setText(format_delay_s(tp.openapi_delay["actual"]))
+        self._info_openapi_delay_arrival.setText(format_delay_s(tp.openapi_delay["last_stop_arrival"]))
+        self._info_openapi_delay_departure.setText(format_delay_s(tp.openapi_delay["last_stop_departure"]))
+
+        self._info_start_timestamp.set_dt(tp.start_timestamp)
+
+        trip_json = tp.json["properties"]["trip"]
+        self._info_trip_id.setText(tp.trip.trip_id)
+        self._info_trip_name.setText(trip_json["gtfs"]["trip_short_name"] or "")
+        self._info_headsig.setText(trip_json["gtfs"]["trip_headsign"])
+        self._info_route.setText(trip_json["origin_route_name"] or trip_json["gtfs"]["route_short_name"])
+        self._info_vehicle_type.setText((trip_json["vehicle_type"]["description_en"] if trip_json["vehicle_type"] else "undef")
+                + (" ❄️" if trip_json["air_conditioned"] else "")
+                + (" 🔌" if trip_json["usb_chargers"] else "")
+                + (" ♿" if trip_json["wheelchair_accessible"] else "")
+                                        )
+        self._info_sequence.setText(str(trip_json["sequence_id"]))
+
+        self._info_openapi_shape_dist.setText(str(tp.openapi_shape_dist_traveled))
+
+        self._info_agency.setText(str(trip_json['agency_name']['scheduled']) + " / " + str(trip_json['agency_name']['real']))
+
+        self._info_state.setText(tp.state_position)
+
+
+
+
+
+        g = gtfs.for_date(tp.origin_timestamp)
+        await g.load_stops()
+
+        def fill_stop(json, obj):
+            if json is not None:
+                stop = g.stops.get(json["id"], None)
+                obj.id.setText(json["id"])
+                obj.name.setText(stop.name if stop else "")
+                obj.arrival.set_dt((datetime.datetime.fromisoformat(json["arrival_time"])))
+                obj.departure.set_dt(datetime.datetime.fromisoformat(json["departure_time"]))
+            else:
+                obj.name.setText("")
+                obj.arrival.set_dt(None)
+                obj.departure.set_dt(None)
+
+        fill_stop(tp.json["properties"]["last_position"]["last_stop"], self._info_last_stop)
+        fill_stop(tp.json["properties"]["last_position"]["next_stop"], self._info_next_stop)
+
+        #stops = g.stops
+        #last_stop = stops.get(tp.json["properties"]["last_position"]["last_stop"]["id"], None)
+        #next_stop = stops.get(tp.json["properties"]["last_position"]["next_stop"]["id"], None)
+        #if last_stop and next_stop:
+            #lat = next_stop.lat - last_stop.lat
+            #lon = next_stop.lon - last_stop.lon
+            #atan = math.atan2(lat, lon*data_utils.lon_muntiplicator)/math.pi*180
+            #rotation = - atan
+            #self._info_rotation.setText(f"{lat} {lon} -> {atan} -> {rotation}")
 
     @asyncSlot()
     async def plot_history(self):
         history_widget = self.mwnd.add_trip_history_widget(self.tp.trip)
-        await history_widget.load(self.tp.capture_time-datetime.timedelta(hours=0.4), self.tp.capture_time)
         await history_widget.load_gtfs_shape()
         await history_widget.load_stops()
-        history_widget.redraw_all()
+        await history_widget.load(self.tp.capture_time-datetime.timedelta(hours=0.1), self.tp.capture_time)
 
         #await self.mwnd.plot_gtfs_shape(self.tp)
         #await self.mwnd.plot_history(self.tp, self.tp.capture_time-datetime.timedelta(minutes=30), self.tp.capture_time)
@@ -752,6 +975,120 @@ class LayerWidget(QWidget):
         self._layout.addWidget(self._title)
 
 
+class LoadRangeWidget(QWidget):
+    def __init__(self, mwnd, route_id):
+        super().__init__()
+        self.route_id = route_id
+        self.cb_clear = lambda: None
+        self.cb_add_preprocessed_data = lambda data, dt_from, dt_to: None
+        self.cb_data_changed = lambda: None
+        self.cb_add_live_data = lambda: error("Live not supported")
+
+        self._layout = QVBoxLayout()
+        self.setLayout(self._layout)
+        self._times_layout = QGridLayout()
+        self._layout.addLayout(self._times_layout)
+
+        self.dt_from = self.dt_to = None
+
+        self.live = False
+        self._dt_from = TimeLabel(mwnd)
+        self._dt_to = TimeLabel(mwnd)
+        self._times_layout.addWidget(QLabel("Loaded from:"), 0, 0)
+        self._times_layout.addWidget(self._dt_from, 0, 1)
+        self._times_layout.addWidget(QLabel("Loaded to:"), 1, 0)
+        self._times_layout.addWidget(self._dt_to, 1, 1)
+
+        self._buttons = QToolBar()
+        self._layout.addWidget(self._buttons)
+        self._load_previous = QAction(QIcon(":images/themes/default/temporal_navigation/rewindToStart.svg"), "Load previous")
+        self._load_previous.triggered.connect(self.load_prev)
+        self._buttons.addAction(self._load_previous)
+        self._reload = QAction(QIcon(":images/themes/default/mActionReload.svg"), "Reload all")
+        self._buttons.addAction(self._reload)
+        self._reload.triggered.connect(self.select_and_reload)
+        self._load_next = QAction(QIcon(":images/themes/default/temporal_navigation/skipToEnd.svg"), "Load next")
+        self._buttons.addAction(self._load_next)
+        self._load_next.triggered.connect(self.load_next)
+        self._start_live = QAction(QIcon(":images/themes/default/temporal_navigation/forward.svg"), "Load to now and start live updates")
+        self._buttons.addAction(self._start_live)
+        self._start_live.triggered.connect(self.start_live)
+        self._stop_live = QAction(QIcon(":images/themes/default/temporal_navigation/pause.svg"), "Stop live updates")
+        self._buttons.addAction(self._stop_live)
+        self._stop_live.triggered.connect(self.stop_live)
+
+    def update_dt_labels(self):
+        self._dt_from.set_dt(self.dt_from)
+        self._dt_to.set_dt(self.dt_to)
+
+    async def clear(self, dt_from, dt_to):
+        self.dt_from = self.dt_to = None
+        self.update_dt_labels()
+        await self.cb_clear()
+
+    @asyncSlot()
+    async def load_prev(self):
+        await self.load(self.dt_from-datetime.timedelta(minutes=15), self.dt_from-datetime.timedelta(seconds=20))
+
+    @asyncSlot()
+    async def load_next(self):
+        await self.load(self.dt_to+datetime.timedelta(seconds=10), self.dt_to+datetime.timedelta(minutes=15))
+
+    def select_and_reload(self):
+        ...
+
+    @asyncSlot()
+    async def start_live(self):
+        if not self.live:
+            if (datetime.datetime.now(local_timezone) - self.dt_to).total_seconds() > 60*60:
+                error("Last data is too old to start live")
+                return
+            await self.load(self.dt_to, datetime.datetime.now(local_timezone), send_changed=False)
+            if (datetime.datetime.now(local_timezone) - self.dt_to).total_seconds() > 30:
+                error("Couldn't fetch data just before now, couldn't continue with live.")
+                return
+            self.live = True
+            live_subscribers.add(self.live_data_arrived)
+            spawn_or_remove_fetch_live_task()
+            if live_data:
+                await self.cb_add_live_data(live_dt, live_data, live_data_by_trip_id)
+            await self.cb_data_changed()
+
+    def stop_live(self):
+        if self.live:
+            self.live = False
+            live_subscribers.remove(self.live_data_arrived)
+            spawn_or_remove_fetch_live_task()
+
+    async def live_data_arrived(self, dt, data, data_by_trip_id):
+        if self.live:
+            await self.cb_add_live_data(dt, data, data_by_trip_id)
+            self.dt_to = dt
+            self.update_dt_labels()
+        else:
+            pprint("live data arrived but we are not in live mode")
+
+    async def load(self, dt_from, dt_to, strict=False, send_changed=True):
+        c = await get_communication()
+        dt = dt_from.replace(second=0, microsecond=0, minute=0)
+        while dt < dt_to:
+            data, source_timestamps = await c.get_preprocessed_data(dt, self.route_id)
+            data = await unzip_parse(data)
+            await self.cb_add_preprocessed_data(data, dt_minf, dt_pinf)
+            if len(source_timestamps):
+                print(self.dt_from, self.dt_to)
+                print(source_timestamps[0], source_timestamps[-1])
+                self.dt_from = min(source_timestamps[0], self.dt_from) if self.dt_from else source_timestamps[0]
+                self.dt_to = max(source_timestamps[-1], self.dt_to) if self.dt_to else source_timestamps[-1]
+                print(self.dt_from, self.dt_to)
+                self.update_dt_labels()
+            dt += datetime.timedelta(hours=1)
+        if send_changed:
+            await self.cb_data_changed()
+
+
+
+
 class TripHistoryWidget(QWidget):
     def __init__(self, trip, mwnd):
         super().__init__()
@@ -787,6 +1124,13 @@ class TripHistoryWidget(QWidget):
         self._stops_layer = LayerWidget([self.stops_layer], "Stops layer")
         self._layout.addWidget(self._stops_layer)
 
+        self._load_range = LoadRangeWidget(self.mwnd, self.trip.trip_id.split("_")[0])
+        self._load_range.cb_add_preprocessed_data = self.add_preprocessed_data
+        self._load_range.cb_data_changed = self.redraw_all_async
+        self._load_range.cb_add_live_data = self.add_live_data
+        self._layout.addWidget(self._load_range)
+
+
         self._show_graph = QPushButton("Show graph")
         self._show_graph.clicked.connect(self.show_graph)
         self._layout.addWidget(self._show_graph)
@@ -801,8 +1145,18 @@ class TripHistoryWidget(QWidget):
         self.mwnd.map.removeLayer(self.history_points_layer)
 
 
+    async def add_preprocessed_data(self, data, dt_from, dt_to):
+        if self.trip.trip_id in data:
+            self.th.add_preprocessed_data(data[self.trip.trip_id])
+
+    async def add_live_data(self, dt, data, data_by_trip_id):
+        self.th.add_history_point(dt, data_by_trip_id[self.trip.trip_id])
+        self.redraw_all()
+
 
     async def load(self, dt_from, dt_to):
+        await self._load_range.load(dt_from, dt_to)
+        return
         c = await get_communication()
         dt = dt_from.replace(second=0, microsecond=0, minute=0)
         while dt < dt_to:
@@ -810,10 +1164,9 @@ class TripHistoryWidget(QWidget):
             print("P1")
             data = await unzip_parse(data)
             print("P3")
-            if self.trip.trip_id in data:
-                self.th.add_preprocessed_data(data[self.trip.trip_id])
             dt += datetime.timedelta(hours=1)
 
+
     async def load_stops(self):
         print("LOAD STOPS")
         c = await get_communication()
@@ -923,6 +1276,9 @@ class TripHistoryWidget(QWidget):
         self.redraw_history_layers()
         self.redraw_history_shape_mapping_layer()
 
+    async def redraw_all_async(self):
+        self.redraw_all()
+
     async def load_gtfs_shape(self):
         await self.th.load_gtfs_shape()
         self.redraw_gtfs_shape_layer()
@@ -931,7 +1287,7 @@ class TripHistoryWidget(QWidget):
     def show_graph(self):
         if self.graph is None:
             print(self.th.stops)
-            self.graph = graph.HistoryGraph(self.th.stops)
+            self.graph = graph.HistoryGraph(self.th.stops, self.th.trip.trip_id.split("_")[0])
             self.graph.closed.connect(self.clear_graph)
             self.graph.add_trip(self.th)
             self.graph.show()
diff --git a/prog/style.qml b/prog/style.qml
index 5bedb4004c41ec670bffc8b5f32f7a4d6107f5bf..a797f365828db1f211c55d4ce3ee67b4dbb7ee53 100644
--- a/prog/style.qml
+++ b/prog/style.qml
@@ -112,9 +112,9 @@
                   <Option name="transformer" type="Map">
                     <Option name="d" type="Map">
                       <Option name="exponent" value="0.57" type="double"/>
-                      <Option name="maxSize" value="2.4" type="double"/>
+                      <Option name="maxSize" value="3.5" type="double"/>
                       <Option name="maxValue" value="360" type="double"/>
-                      <Option name="minSize" value="2.4" type="double"/>
+                      <Option name="minSize" value="3.5" type="double"/>
                       <Option name="minValue" value="0" type="double"/>
                       <Option name="nullSize" value="0" type="double"/>
                       <Option name="scaleType" value="2" type="int"/>
@@ -232,9 +232,9 @@
                   <Option name="transformer" type="Map">
                     <Option name="d" type="Map">
                       <Option name="exponent" value="0.57" type="double"/>
-                      <Option name="maxSize" value="2.4" type="double"/>
+                      <Option name="maxSize" value="3.5" type="double"/>
                       <Option name="maxValue" value="360" type="double"/>
-                      <Option name="minSize" value="2.4" type="double"/>
+                      <Option name="minSize" value="3.5" type="double"/>
                       <Option name="minValue" value="0" type="double"/>
                       <Option name="nullSize" value="0" type="double"/>
                       <Option name="scaleType" value="2" type="int"/>
@@ -352,9 +352,9 @@
                   <Option name="transformer" type="Map">
                     <Option name="d" type="Map">
                       <Option name="exponent" value="0.57" type="double"/>
-                      <Option name="maxSize" value="2.4" type="double"/>
+                      <Option name="maxSize" value="3.5" type="double"/>
                       <Option name="maxValue" value="360" type="double"/>
-                      <Option name="minSize" value="2.4" type="double"/>
+                      <Option name="minSize" value="3.5" type="double"/>
                       <Option name="minValue" value="0" type="double"/>
                       <Option name="nullSize" value="0" type="double"/>
                       <Option name="scaleType" value="2" type="int"/>
@@ -472,9 +472,9 @@
                   <Option name="transformer" type="Map">
                     <Option name="d" type="Map">
                       <Option name="exponent" value="0.57" type="double"/>
-                      <Option name="maxSize" value="2.4" type="double"/>
+                      <Option name="maxSize" value="3.5" type="double"/>
                       <Option name="maxValue" value="360" type="double"/>
-                      <Option name="minSize" value="2.4" type="double"/>
+                      <Option name="minSize" value="3.5" type="double"/>
                       <Option name="minValue" value="0" type="double"/>
                       <Option name="nullSize" value="0" type="double"/>
                       <Option name="scaleType" value="2" type="int"/>
@@ -592,9 +592,9 @@
                   <Option name="transformer" type="Map">
                     <Option name="d" type="Map">
                       <Option name="exponent" value="0.57" type="double"/>
-                      <Option name="maxSize" value="2.4" type="double"/>
+                      <Option name="maxSize" value="3.5" type="double"/>
                       <Option name="maxValue" value="360" type="double"/>
-                      <Option name="minSize" value="2.4" type="double"/>
+                      <Option name="minSize" value="3.5" type="double"/>
                       <Option name="minValue" value="0" type="double"/>
                       <Option name="nullSize" value="0" type="double"/>
                       <Option name="scaleType" value="2" type="int"/>
@@ -714,9 +714,9 @@
                   <Option name="transformer" type="Map">
                     <Option name="d" type="Map">
                       <Option name="exponent" value="0.57" type="double"/>
-                      <Option name="maxSize" value="2.4" type="double"/>
+                      <Option name="maxSize" value="3.5" type="double"/>
                       <Option name="maxValue" value="360" type="double"/>
-                      <Option name="minSize" value="2.4" type="double"/>
+                      <Option name="minSize" value="3.5" type="double"/>
                       <Option name="minValue" value="0" type="double"/>
                       <Option name="nullSize" value="0" type="double"/>
                       <Option name="scaleType" value="2" type="int"/>
@@ -805,8 +805,8 @@
     </selectionSymbol>
   </selection>
   <labeling type="rule-based">
-    <rules key="{7d199803-2f52-406f-9b11-3e7f0023912b}">
-      <rule key="{47a4e9cb-3d7e-4298-95f4-ef970c2c129c}" filter="&quot;vehicle_type&quot; = 'tramvaj'" scalemindenom="1" scalemaxdenom="30000">
+    <rules key="{8a882773-ce69-43f3-aa19-73bb6eaf119f}">
+      <rule key="{1b1868c3-1b86-4642-a895-26a85c93e179}" filter="&quot;vehicle_type&quot; = 'tramvaj'" scalemindenom="1" scalemaxdenom="30000">
         <settings calloutType="simple">
           <text-style tabStopDistanceUnit="Point" forcedItalic="0" fontFamily="Open Sans" fontWordSpacing="0" fontItalic="0" fontSize="10" fontSizeMapUnitScale="3x:0,0,0,0,0,0" isExpression="0" fontStrikeout="0" forcedBold="0" capitalization="0" blendMode="0" textColor="255,1,1,255,rgb:1,0.00392156862745098,0.00392156862745098,1" textOpacity="1" fontKerning="1" fontUnderline="0" multilineHeight="1" fontWeight="50" fontLetterSpacing="0" fontSizeUnit="Point" stretchFactor="100" namedStyle="Regular" allowHtml="0" fieldName="route_short_name" legendString="Aa" textOrientation="horizontal" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" tabStopDistance="80" useSubstitutions="0" multilineHeightUnit="Percentage" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0">
             <families/>
@@ -930,7 +930,7 @@
           </callout>
         </settings>
       </rule>
-      <rule key="{54ca7404-395f-4156-91a2-79e39ff03c8f}" filter="&quot;vehicle_type&quot; = 'trolejbus'" scalemindenom="1" scalemaxdenom="30000">
+      <rule key="{efb88fc7-1970-429c-ad0a-9573696d53c6}" filter="&quot;vehicle_type&quot; = 'trolejbus'" scalemindenom="1" scalemaxdenom="30000">
         <settings calloutType="simple">
           <text-style tabStopDistanceUnit="Point" forcedItalic="0" fontFamily="Open Sans" fontWordSpacing="0" fontItalic="0" fontSize="10" fontSizeMapUnitScale="3x:0,0,0,0,0,0" isExpression="0" fontStrikeout="0" forcedBold="0" capitalization="0" blendMode="0" textColor="5,255,1,255,rgb:0.0196078431372549,1,0.00392156862745098,1" textOpacity="1" fontKerning="1" fontUnderline="0" multilineHeight="1" fontWeight="50" fontLetterSpacing="0" fontSizeUnit="Point" stretchFactor="100" namedStyle="Regular" allowHtml="0" fieldName="route_short_name" legendString="Aa" textOrientation="horizontal" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" tabStopDistance="80" useSubstitutions="0" multilineHeightUnit="Percentage" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0">
             <families/>
@@ -1054,7 +1054,7 @@
           </callout>
         </settings>
       </rule>
-      <rule key="{07420885-74ef-48a8-9b1e-749885a10eba}" filter="&quot;vehicle_type&quot; = 'autobus'" scalemindenom="1" scalemaxdenom="30000">
+      <rule key="{db20f50a-c885-4dee-bf61-c93c9092adad}" filter="&quot;vehicle_type&quot; = 'autobus'" scalemindenom="1" scalemaxdenom="30000">
         <settings calloutType="simple">
           <text-style tabStopDistanceUnit="Point" forcedItalic="0" fontFamily="Open Sans" fontWordSpacing="0" fontItalic="0" fontSize="10" fontSizeMapUnitScale="3x:0,0,0,0,0,0" isExpression="0" fontStrikeout="0" forcedBold="0" capitalization="0" blendMode="0" textColor="1,26,255,255,rgb:0.00392156862745098,0.10196078431372549,1,1" textOpacity="1" fontKerning="1" fontUnderline="0" multilineHeight="1" fontWeight="50" fontLetterSpacing="0" fontSizeUnit="Point" stretchFactor="100" namedStyle="Regular" allowHtml="0" fieldName="route_short_name" legendString="Aa" textOrientation="horizontal" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" tabStopDistance="80" useSubstitutions="0" multilineHeightUnit="Percentage" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0">
             <families/>
@@ -1178,7 +1178,7 @@
           </callout>
         </settings>
       </rule>
-      <rule key="{a2374761-8c03-4533-940e-a532a74d7da1}" filter="&quot;vehicle_type&quot; = 'smluvní spoj'" scalemindenom="1" scalemaxdenom="30000">
+      <rule key="{69bd12b9-5d68-41f7-a45e-76454bf5282b}" filter="&quot;vehicle_type&quot; = 'smluvní spoj'" scalemindenom="1" scalemaxdenom="30000">
         <settings calloutType="simple">
           <text-style tabStopDistanceUnit="Point" forcedItalic="0" fontFamily="Open Sans" fontWordSpacing="0" fontItalic="0" fontSize="10" fontSizeMapUnitScale="3x:0,0,0,0,0,0" isExpression="0" fontStrikeout="0" forcedBold="0" capitalization="0" blendMode="0" textColor="255,179,1,255,rgb:1,0.70196078431372544,0.00392156862745098,1" textOpacity="1" fontKerning="1" fontUnderline="0" multilineHeight="1" fontWeight="50" fontLetterSpacing="0" fontSizeUnit="Point" stretchFactor="100" namedStyle="Regular" allowHtml="0" fieldName="route_short_name" legendString="Aa" textOrientation="horizontal" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" tabStopDistance="80" useSubstitutions="0" multilineHeightUnit="Percentage" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0">
             <families/>
@@ -1302,7 +1302,7 @@
           </callout>
         </settings>
       </rule>
-      <rule key="{f26909f7-710d-4388-8734-e5629dcc0b23}" filter="&quot;vehicle_type&quot; = 'loď'" scalemindenom="1" scalemaxdenom="30000">
+      <rule key="{a34cedcb-0bf4-467f-b8b2-bae99c2a83b3}" filter="&quot;vehicle_type&quot; = 'loď'" scalemindenom="1" scalemaxdenom="30000">
         <settings calloutType="simple">
           <text-style tabStopDistanceUnit="Point" forcedItalic="0" fontFamily="Open Sans" fontWordSpacing="0" fontItalic="0" fontSize="10" fontSizeMapUnitScale="3x:0,0,0,0,0,0" isExpression="0" fontStrikeout="0" forcedBold="0" capitalization="0" blendMode="0" textColor="0,0,0,255,rgb:0,0,0,1" textOpacity="1" fontKerning="1" fontUnderline="0" multilineHeight="1" fontWeight="50" fontLetterSpacing="0" fontSizeUnit="Point" stretchFactor="100" namedStyle="Regular" allowHtml="0" fieldName="route_short_name" legendString="Aa" textOrientation="horizontal" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" tabStopDistance="80" useSubstitutions="0" multilineHeightUnit="Percentage" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0">
             <families/>
@@ -1426,7 +1426,7 @@
           </callout>
         </settings>
       </rule>
-      <rule key="{6908df92-09fa-4869-a43f-4a290ea4f5b9}" filter="&quot;vehicle_type&quot; = 'náhradní doprava'" scalemindenom="1" scalemaxdenom="30000">
+      <rule key="{b0df926b-a2ef-4d50-bddf-819ba4f3c7b6}" filter="&quot;vehicle_type&quot; = 'náhradní doprava'" scalemindenom="1" scalemaxdenom="30000">
         <settings calloutType="simple">
           <text-style tabStopDistanceUnit="Point" forcedItalic="0" fontFamily="Open Sans" fontWordSpacing="0" fontItalic="0" fontSize="10" fontSizeMapUnitScale="3x:0,0,0,0,0,0" isExpression="0" fontStrikeout="0" forcedBold="0" capitalization="0" blendMode="0" textColor="1,26,255,255,rgb:0.00392156862745098,0.10196078431372549,1,1" textOpacity="1" fontKerning="1" fontUnderline="1" multilineHeight="1" fontWeight="50" fontLetterSpacing="0" fontSizeUnit="Point" stretchFactor="100" namedStyle="Regular" allowHtml="0" fieldName="route_short_name" legendString="Aa" textOrientation="horizontal" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" tabStopDistance="80" useSubstitutions="0" multilineHeightUnit="Percentage" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0">
             <families/>
@@ -1550,7 +1550,7 @@
           </callout>
         </settings>
       </rule>
-      <rule key="{5e30f856-b07d-4ec0-9567-b0efc34fe009}" filter="&quot;vehicle_type&quot; = 'regionální autobus'" scalemindenom="1" scalemaxdenom="40000">
+      <rule key="{2ad63a67-05e4-4ff1-964f-3d9aaacbf576}" filter="&quot;vehicle_type&quot; = 'regionální autobus'" scalemindenom="1" scalemaxdenom="40000">
         <settings calloutType="simple">
           <text-style tabStopDistanceUnit="Point" forcedItalic="0" fontFamily="Open Sans" fontWordSpacing="0" fontItalic="0" fontSize="10" fontSizeMapUnitScale="3x:0,0,0,0,0,0" isExpression="0" fontStrikeout="0" forcedBold="0" capitalization="0" blendMode="0" textColor="183,1,255,255,rgb:0.71764705882352942,0.00392156862745098,1,1" textOpacity="1" fontKerning="1" fontUnderline="0" multilineHeight="1" fontWeight="50" fontLetterSpacing="0" fontSizeUnit="Point" stretchFactor="100" namedStyle="Regular" allowHtml="0" fieldName="route_short_name" legendString="Aa" textOrientation="horizontal" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" tabStopDistance="80" useSubstitutions="0" multilineHeightUnit="Percentage" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0">
             <families/>
@@ -1674,7 +1674,7 @@
           </callout>
         </settings>
       </rule>
-      <rule key="{ef8b24d3-8d11-459a-bae3-fd3bf8cf86fa}" filter="&quot;vehicle_type&quot; = 'metro' AND &quot;route_short_name&quot; = 'A'" scalemindenom="1" scalemaxdenom="30000">
+      <rule key="{3de8a380-771c-4ca8-88a7-2078b9d3fd8d}" filter="&quot;vehicle_type&quot; = 'metro' AND &quot;route_short_name&quot; = 'A'" scalemindenom="1" scalemaxdenom="30000">
         <settings calloutType="simple">
           <text-style tabStopDistanceUnit="Point" forcedItalic="0" fontFamily="Open Sans" fontWordSpacing="0" fontItalic="0" fontSize="10" fontSizeMapUnitScale="3x:0,0,0,0,0,0" isExpression="0" fontStrikeout="0" forcedBold="0" capitalization="0" blendMode="0" textColor="255,255,255,255,rgb:1,1,1,1" textOpacity="1" fontKerning="1" fontUnderline="0" multilineHeight="1" fontWeight="50" fontLetterSpacing="0" fontSizeUnit="Point" stretchFactor="100" namedStyle="Regular" allowHtml="0" fieldName="route_short_name" legendString="Aa" textOrientation="horizontal" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" tabStopDistance="80" useSubstitutions="0" multilineHeightUnit="Percentage" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0">
             <families/>
@@ -1798,7 +1798,7 @@
           </callout>
         </settings>
       </rule>
-      <rule key="{fdf413ee-fc2a-44e3-8e0a-9c67b4a7486c}" filter="&quot;vehicle_type&quot; = 'metro' AND &quot;route_short_name&quot; = 'B'" scalemindenom="1" scalemaxdenom="30000">
+      <rule key="{2b31ecbd-a799-4e93-8352-72f66d169643}" filter="&quot;vehicle_type&quot; = 'metro' AND &quot;route_short_name&quot; = 'B'" scalemindenom="1" scalemaxdenom="30000">
         <settings calloutType="simple">
           <text-style tabStopDistanceUnit="Point" forcedItalic="0" fontFamily="Open Sans" fontWordSpacing="0" fontItalic="0" fontSize="10" fontSizeMapUnitScale="3x:0,0,0,0,0,0" isExpression="0" fontStrikeout="0" forcedBold="0" capitalization="0" blendMode="0" textColor="255,255,255,255,rgb:1,1,1,1" textOpacity="1" fontKerning="1" fontUnderline="0" multilineHeight="1" fontWeight="50" fontLetterSpacing="0" fontSizeUnit="Point" stretchFactor="100" namedStyle="Regular" allowHtml="0" fieldName="route_short_name" legendString="Aa" textOrientation="horizontal" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" tabStopDistance="80" useSubstitutions="0" multilineHeightUnit="Percentage" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0">
             <families/>
@@ -1922,7 +1922,7 @@
           </callout>
         </settings>
       </rule>
-      <rule key="{ee7358f7-0041-4be9-93f0-5076b999a471}" filter="&quot;vehicle_type&quot; = 'metro' AND &quot;route_short_name&quot; = 'C'" scalemindenom="1" scalemaxdenom="30000">
+      <rule key="{13ca571b-83ea-429f-80e5-8fbbe0850b6d}" filter="&quot;vehicle_type&quot; = 'metro' AND &quot;route_short_name&quot; = 'C'" scalemindenom="1" scalemaxdenom="30000">
         <settings calloutType="simple">
           <text-style tabStopDistanceUnit="Point" forcedItalic="0" fontFamily="Open Sans" fontWordSpacing="0" fontItalic="0" fontSize="10" fontSizeMapUnitScale="3x:0,0,0,0,0,0" isExpression="0" fontStrikeout="0" forcedBold="0" capitalization="0" blendMode="0" textColor="255,255,255,255,rgb:1,1,1,1" textOpacity="1" fontKerning="1" fontUnderline="0" multilineHeight="1" fontWeight="50" fontLetterSpacing="0" fontSizeUnit="Point" stretchFactor="100" namedStyle="Regular" allowHtml="0" fieldName="route_short_name" legendString="Aa" textOrientation="horizontal" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" tabStopDistance="80" useSubstitutions="0" multilineHeightUnit="Percentage" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0">
             <families/>
@@ -2046,7 +2046,7 @@
           </callout>
         </settings>
       </rule>
-      <rule key="{db037816-c2dc-4673-ad91-3ce901e828d4}" filter="&quot;vehicle_type&quot; = 'vlak'" scalemindenom="1" scalemaxdenom="60000">
+      <rule key="{4cc61794-bc84-4634-9052-06a4909a108e}" filter="&quot;vehicle_type&quot; = 'vlak'" scalemindenom="1" scalemaxdenom="60000">
         <settings calloutType="simple">
           <text-style tabStopDistanceUnit="Point" forcedItalic="0" fontFamily="Open Sans" fontWordSpacing="0" fontItalic="0" fontSize="10" fontSizeMapUnitScale="3x:0,0,0,0,0,0" isExpression="0" fontStrikeout="0" forcedBold="0" capitalization="0" blendMode="0" textColor="255,255,255,255,rgb:1,1,1,1" textOpacity="1" fontKerning="1" fontUnderline="0" multilineHeight="1" fontWeight="50" fontLetterSpacing="0" fontSizeUnit="Point" stretchFactor="100" namedStyle="Regular" allowHtml="0" fieldName="route_short_name" legendString="Aa" textOrientation="horizontal" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" tabStopDistance="80" useSubstitutions="0" multilineHeightUnit="Percentage" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0">
             <families/>
@@ -2170,7 +2170,7 @@
           </callout>
         </settings>
       </rule>
-      <rule key="{c107065a-c6a3-4a0c-abd7-8094885f9e0e}" filter="&quot;vehicle_type&quot; = 'vlak' AND &quot;route_short_name&quot; LIKE 'R%'" scalemindenom="1" scalemaxdenom="100000">
+      <rule key="{b1eb89fc-e57a-4699-ac6e-74476060c412}" filter="&quot;vehicle_type&quot; = 'vlak' AND &quot;route_short_name&quot; LIKE 'R%'" scalemindenom="1" scalemaxdenom="100000">
         <settings calloutType="simple">
           <text-style tabStopDistanceUnit="Point" forcedItalic="0" fontFamily="Open Sans" fontWordSpacing="0" fontItalic="0" fontSize="10" fontSizeMapUnitScale="3x:0,0,0,0,0,0" isExpression="0" fontStrikeout="0" forcedBold="0" capitalization="0" blendMode="0" textColor="255,37,52,255,rgb:1,0.14509803921568629,0.20392156862745098,1" textOpacity="1" fontKerning="1" fontUnderline="0" multilineHeight="1" fontWeight="50" fontLetterSpacing="0" fontSizeUnit="Point" stretchFactor="100" namedStyle="Regular" allowHtml="0" fieldName="route_short_name" legendString="Aa" textOrientation="horizontal" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" tabStopDistance="80" useSubstitutions="0" multilineHeightUnit="Percentage" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0">
             <families/>
diff --git a/prog/utils.py b/prog/utils.py
index 9e9f953a89d9661f5ab77e051532484ccb4caad0..5c520d435f22f9025cd7e9aa6e1e7b36e2b3effb 100644
--- a/prog/utils.py
+++ b/prog/utils.py
@@ -1,6 +1,7 @@
 import sys, os
 import traceback
 import datetime
+from typing import Optional
 
 def eprint(*args):
     print(*args, file=sys.stderr, flush=True)
@@ -15,3 +16,22 @@ async def catch_all(corutine, exitcode=None):
             exit(exitcode)
 
 local_timezone = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
+dt_pinf = datetime.datetime(2100, 1, 1, tzinfo=local_timezone)
+dt_minf = datetime.datetime(2000, 1, 1, tzinfo=local_timezone)
+
+
+def format_dt_to_user(dt: datetime.datetime) -> str:
+    return dt.strftime("%Y-%m-%d %H:%M:%S")
+
+def parse_dt_from_user(val: str) -> Optional[datetime.datetime]:
+    return datetime.datetime.strptime(val, "%Y-%m-%d %H:%M:%S").replace(tzinfo=local_timezone)
+
+def format_delay_s(s):
+    if s is None:
+        return "–"
+    if s < 0: return "- " + format_delay_s(-s)
+    if s == 0: return "on time"
+    if s < 60: return f"{s} s"
+    if s < 10*60: return f"{s//60} min {s%60} s"
+    if s < 60*60: return f"{s//60} min"
+    if s < 60*60: return f"{s//60//60} h {s//60} min"