Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/rail_network_graph/__main__.py
Original file line number Diff line number Diff line change
@@ -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())
182 changes: 182 additions & 0 deletions src/rail_network_graph/app.py
Original file line number Diff line number Diff line change
@@ -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