From e893638d61c8cc9dbe53957f9625d2cd496e58a8 Mon Sep 17 00:00:00 2001 From: Antek Date: Wed, 10 Dec 2025 14:14:15 +0100 Subject: [PATCH] feat: implement application entry point and main loop --- src/rail_network_graph/__main__.py | 28 +++++ src/rail_network_graph/app.py | 182 +++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 src/rail_network_graph/__main__.py create mode 100644 src/rail_network_graph/app.py diff --git a/src/rail_network_graph/__main__.py b/src/rail_network_graph/__main__.py new file mode 100644 index 0000000..8f32eff --- /dev/null +++ b/src/rail_network_graph/__main__.py @@ -0,0 +1,28 @@ +import logging +import sys + +from rail_network_graph.app import App +from rail_network_graph.logging_config import configure_logging + + +def main() -> int: + """ + Initialize logging, start the application, + and return its exit code. + + :return: Exit code returned by the application. + """ + # Configure logging + configure_logging() + log = logging.getLogger(__name__) + log.debug("Logging configured") + + log.info("Starting ConnectionMap application") + + app = App() + return app.run() + + +if __name__ == "__main__": + # Return the exit code to the operating system + sys.exit(main()) diff --git a/src/rail_network_graph/app.py b/src/rail_network_graph/app.py new file mode 100644 index 0000000..4f23d21 --- /dev/null +++ b/src/rail_network_graph/app.py @@ -0,0 +1,182 @@ +import logging + +import customtkinter as ctk +import pandas as pd + +from rail_network_graph import config +from rail_network_graph.data_processing.data_loader import load_data +from rail_network_graph.data_processing.data_processor import count_platforms_per_station, process +from rail_network_graph.graphs.stations_graph import StationsGraph +from rail_network_graph.GUI.gui_creator import GUICreator +from rail_network_graph.GUI.map.map_plotter import MapPlotter + +log = logging.getLogger(__name__) + + +class App: + """ + Main application that loads GTFS data, builds the stations graph, + and initializes the CustomTkinter-based GUI. + """ + + def __init__(self) -> None: + log.debug("Initializing application") + self.root: ctk.CTk | None = None + self.graph: StationsGraph | None = None + self.merged_data: pd.DataFrame | None = None + self.stop_to_station: dict[str, str] | None = None + self.coords: dict[str, tuple[float, float]] | None = None + self.names: dict[str, str] | None = None + self.platform_count: dict[str, int] | None = None + self.stops_data: pd.DataFrame | None = None + + @staticmethod + def _load_and_process_data() -> ( + tuple[ + pd.DataFrame, dict[str, str], dict[str, tuple[float, float]], dict[str, str], dict[str, int], pd.DataFrame + ] + ): + """ + Load GTFS data from disk and run the preprocessing pipeline. + + This step merges stops, stop times, trips and routes into a station-level + representation and computes helper mappings and statistics. + + :return: Tuple containing: + - merged_data: Processed DataFrame used to build the stations graph. + - stop_to_station: Mapping from stop_id to parent station_id. + - coords: Mapping of station_id to (latitude, longitude). + - names: Mapping of station_id to human-readable station name. + - platform_count: Mapping of station_id to number of platforms. + - stops_data: Raw GTFS stops DataFrame. + """ + log.info("Loading GTFS data from %s", config.GTFS_DATA_PATH) + stops, stop_times, trips, routes = load_data(config.GTFS_DATA_PATH) + log.debug("Processing GTFS data") + merged, stop_to_station, coords, names = process(stops, stop_times, trips, routes) + platform_count = count_platforms_per_station(stop_to_station) + log.info("Data loaded and processed") + return merged, stop_to_station, coords, names, platform_count, stops + + @staticmethod + def _create_graph(merged_data: pd.DataFrame, stop_to_station: dict[str, str]) -> StationsGraph: + """ + Create the StationsGraph representing the rail network. + + :param merged_data: Preprocessed DataFrame describing stop-to-stop connections. + :param stop_to_station: Mapping from stop_id to parent station_id. + :return: Initialized StationsGraph instance. + """ + log.debug("Creating station graph") + graph = StationsGraph(merged_data, stop_to_station) + log.info( + "Stations graph created (nodes: %s, edges: %s)", + len(getattr(graph, "stations", [])), + len(getattr(graph, "filtered_edges", [])), + ) + return graph + + @staticmethod + def _configure_ctk() -> None: + """ + Configure global CustomTkinter appearance settings. + + Currently, sets the application appearance mode to "dark". + + :return: None + """ + log.debug("Configuring CustomTkinter (dark mode)") + ctk.set_appearance_mode("dark") + + @staticmethod + def _create_gui( + graph: StationsGraph, + merged_data: pd.DataFrame, + coords: dict[str, tuple[float, float]], + names: dict[str, str], + platform_count: dict[str, int], + stops_data: pd.DataFrame, + ) -> ctk.CTk: + """ + Create and initialize the main CustomTkinter window and all GUI components. + + :param graph: StationsGraph instance representing the rail network. + :param merged_data: Preprocessed GTFS DataFrame used by GUI components. + :param coords: Mapping of station_id to geographic coordinates (lat, lon). + :param names: Mapping of station_id to human-readable station names. + :param platform_count: Mapping of station_id to number of platforms. + :param stops_data: Raw GTFS stops DataFrame for detailed stop information. + :return: Root CTk window ready to start the main event loop. + """ + log.debug("Creating main CTk window") + root = ctk.CTk() + root.title("RailNetworkGraph") + + root.protocol("WM_DELETE_WINDOW", root.quit) + + try: + root.iconbitmap(config.APP_ICON_PATH) + log.debug("Application icon set: %s", config.APP_ICON_PATH) + except Exception as exc: + log.warning("Failed to set application icon (%s): %s", config.APP_ICON_PATH, exc) + + log.debug("Initializing MapPlotter") + plotter = MapPlotter( + axis=None, + station_coords=coords, + station_names=names, + filtered_edges=list(graph.filtered_edges), + platform_count_by_station=platform_count, + geojson_path=config.VOIVODESHIP_BORDER_PATH, + ) + + log.debug("Building GUI through GUICreator") + GUICreator( + root_window=root, + map_plotter=plotter, + station_coords=coords, + station_names=names, + merged_data=merged_data, + stops_data=stops_data, + graph=graph, + ) + + log.info("GUI created") + return root + + def run(self) -> int: + """ + Run the main application event loop. + + Starts the CustomTkinter mainloop and blocks until the window is closed. + + :return: Exit code, 0 if the application terminated normally. + """ + log.info("Starting application") + + self._configure_ctk() + + ( + self.merged_data, + self.stop_to_station, + self.coords, + self.names, + self.platform_count, + self.stops_data, + ) = self._load_and_process_data() + + self.graph = self._create_graph(self.merged_data, self.stop_to_station) + + self.root = self._create_gui( + graph=self.graph, + merged_data=self.merged_data, + coords=self.coords, + names=self.names, + platform_count=self.platform_count, + stops_data=self.stops_data, + ) + + log.info("Starting CTk mainloop") + self.root.mainloop() + log.info("Application terminated") + return 0