diff --git a/pyproject.toml b/pyproject.toml index f0cb7847..b023d21a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,12 +13,20 @@ Jinja2 = ">=3.1.2" weasyprint = ">=60.2" lxml = ">=5.0.0" xdg-base-dirs = "^6.0.1" -tomlkit = "^0.12.3" +tomlkit = "^0.13.0" [tool.poetry.group.dev.dependencies] -black = ">=23.12.1" +black = {extras = ["d"], version = "^24.4.2"} mypy = ">=1.8.0" +[tool.poetry.scripts] +amtrak-gtfs = "timetable_kit.amtrak.get_gtfs:main" +amtrak-stations = "timetable_kit.amtrak.json_stations:main" +via-gtfs = "timetable_kit.via.get_gtfs:main" +greyhound-gtfs = "timetable_kit.greyhound.get_gtfs:main" +hartford-line-gtfs = "timetable_kit.hartford_line.get_gtfs:main" +maple-leaf-gtfs = "timetable_kit.maple_leaf.get_gtfs:main" + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/timetable_kit/amtrak/access.py b/timetable_kit/amtrak/access.py index 4268670a..4c809b06 100755 --- a/timetable_kit/amtrak/access.py +++ b/timetable_kit/amtrak/access.py @@ -8,8 +8,8 @@ This has very similar code to baggage.py """ -from io import StringIO # for parsing JSON import json +from io import StringIO # for parsing JSON import pandas as pd diff --git a/timetable_kit/amtrak/accessibility_check.py b/timetable_kit/amtrak/accessibility_check.py index fed4353e..7398f9cd 100755 --- a/timetable_kit/amtrak/accessibility_check.py +++ b/timetable_kit/amtrak/accessibility_check.py @@ -11,15 +11,10 @@ Requires local bad_stations.csv file: That is generated by "json_stations.py process" """ import json -from pathlib import Path from io import StringIO # Needed to parse JSON import pandas as pd -# These are mine - -from timetable_kit.file_locations import get_timetable_kit_data_home - from timetable_kit.amtrak.json_stations import ( load_stations_json, load_station_details, @@ -27,6 +22,7 @@ # To filter out that which is not a train station from timetable_kit.amtrak.station_type import is_train_station +from timetable_kit.file_locations import get_timetable_kit_data_home station_stats_dir = get_timetable_kit_data_home() / "amtrak" / "station_stats" stations_csv_path = get_timetable_kit_data_home() / "amtrak" / "stations.csv" diff --git a/timetable_kit/amtrak/agency.py b/timetable_kit/amtrak/agency.py index 54aec7e3..dcac3e9d 100644 --- a/timetable_kit/amtrak/agency.py +++ b/timetable_kit/amtrak/agency.py @@ -5,35 +5,33 @@ This holds a class for "AgencyAmtrak" intended to be used as a singleton. """ -from timetable_kit.feed_enhanced import FeedEnhanced # Mostly for typechecking - -from timetable_kit.generic_agency import Agency - -# for patch_feed -import timetable_kit.amtrak.gtfs_patches as gtfs_patches - # for patch_add_wheelchair_boarding import timetable_kit.amtrak.access as access -# for sleeper trains, which trains have checked baggage, major stations, etc -import timetable_kit.amtrak.special_data as special_data - # for whether stations have checked baggage import timetable_kit.amtrak.baggage as baggage +# for patch_feed +import timetable_kit.amtrak.gtfs_patches as gtfs_patches + # for get_station_name import timetable_kit.amtrak.json_stations as json_stations +# for get_route_name +import timetable_kit.amtrak.route_names as route_names + +# for sleeper trains, which trains have checked baggage, major stations, etc +import timetable_kit.amtrak.special_data as special_data + # for get_station_name_pretty (subroutines) import timetable_kit.text_assembly as text_assembly -from timetable_kit.text_assembly import SAFE_BR # Map from station codes to connecting service names # This is stashed in a class variable from timetable_kit.amtrak.connecting_services_data import connecting_services_dict - -# for get_route_name -import timetable_kit.amtrak.route_names as route_names +from timetable_kit.feed_enhanced import FeedEnhanced # Mostly for typechecking +from timetable_kit.generic_agency import Agency +from timetable_kit.text_assembly import SAFE_BR class AgencyAmtrak(Agency): @@ -109,10 +107,6 @@ def connecting_bus_key_sentence(self, doing_html=True) -> str: """Sentence to put in the symbol key for connecting bus services.""" return "Connecting Bus Service (can be booked through Amtrak)" - def agency_css_class(self) -> str: - """Name of a CSS class for agency-specific styling.""" - return "amtrak-special-css" - def get_route_name(self, today_feed: FeedEnhanced, route_id: str) -> str: """Given today_feed and a route_id, produce a suitalbe name for a column subheading. diff --git a/timetable_kit/amtrak/agency_cleanup.py b/timetable_kit/amtrak/agency_cleanup.py index fc4e7336..c448e506 100644 --- a/timetable_kit/amtrak/agency_cleanup.py +++ b/timetable_kit/amtrak/agency_cleanup.py @@ -71,9 +71,9 @@ def revised_amtrak_agencies(agency): ) # Edit the lookup table: # This was only needed for pre-2022 Amtrak data. - agency_lookup_table[ - 174 - ] = "Amtrak Directly Operated Thruway Bus" # Is "Amtrak" in feed + agency_lookup_table[174] = ( + "Amtrak Directly Operated Thruway Bus" # Is "Amtrak" in feed + ) agency_lookup_table[192] = "Thruway Bus Operator 192" agency_lookup_table[1206] = "Thruway Bus Operator 1206" agency_lookup_table[1207] = "Thruway Bus Operator 1207" diff --git a/timetable_kit/amtrak/baggage.py b/timetable_kit/amtrak/baggage.py index 0dc12613..e1d8ca0e 100644 --- a/timetable_kit/amtrak/baggage.py +++ b/timetable_kit/amtrak/baggage.py @@ -7,8 +7,8 @@ Requires local copy of Amtrak stations database: That local copy is generated by "json_stations.py download" """ -from io import StringIO # for parsing JSON import json +from io import StringIO # for parsing JSON import pandas as pd diff --git a/timetable_kit/amtrak/get_gtfs.py b/timetable_kit/amtrak/get_gtfs.py index 800a6498..b86c2d04 100755 --- a/timetable_kit/amtrak/get_gtfs.py +++ b/timetable_kit/amtrak/get_gtfs.py @@ -21,6 +21,10 @@ def get_gtfs_files(): return _gtfs_files +def main(): + get_gtfs_files().download_and_save() + + # MAIN PROGRAM if __name__ == "__main__": - get_gtfs_files().download_and_save() + main() diff --git a/timetable_kit/amtrak/get_wiki_stations.py b/timetable_kit/amtrak/get_wiki_stations.py index f621294e..434be29c 100755 --- a/timetable_kit/amtrak/get_wiki_stations.py +++ b/timetable_kit/amtrak/get_wiki_stations.py @@ -11,13 +11,12 @@ """ import argparse -from pathlib import Path import re from math import nan +from pathlib import Path import pandas as pd - arg_parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description="""Process the list of Amtrak stations in Wikipedia. diff --git a/timetable_kit/amtrak/gtfs_patches.py b/timetable_kit/amtrak/gtfs_patches.py index 8378af55..fed51d91 100644 --- a/timetable_kit/amtrak/gtfs_patches.py +++ b/timetable_kit/amtrak/gtfs_patches.py @@ -7,11 +7,10 @@ """ # TODO: all the Amtrak-specific stuff needs to be made object oriented in an "Amtrak object" perhaps -from timetable_kit.debug import debug_print -from timetable_kit.feed_enhanced import FeedEnhanced - # Add the wheelchair boarding information from JSON into the GTFS from timetable_kit.amtrak.access import patch_add_wheelchair_boarding +from timetable_kit.debug import debug_print +from timetable_kit.feed_enhanced import FeedEnhanced arizona_stops_list = [ # Sunset Limited @@ -116,7 +115,7 @@ def patch_hiawatha(feed: FeedEnhanced): def patch_sunset_limited(feed: FeedEnhanced): - """Patch a bug where Sunset Limited #2 has wrong departure days. + """Patch a bug where Sunset Limited #2 has wrong departure days. It departs from LAX on Su/We/Fr and not Sa/Tu/Th The bug is because it starts so late it starts on the following day in Eastern Standard Time. @@ -139,17 +138,17 @@ def patch_sunset_limited(feed: FeedEnhanced): debug_print(1, "Found #2 listed as running on Saturday, patching") new_calendar.loc[index, "sunday"] = 0 new_calendar.loc[index, "monday"] = 1 - #feed.calendar = new_calendar + # feed.calendar = new_calendar if new_calendar.loc[index, "wednesday"] == 1: # This is incorrrect. debug_print(1, "Found #2 listed as running on Tuesday, patching") new_calendar.loc[index, "wednesday"] = 0 new_calendar.loc[index, "thursday"] = 1 - #feed.calendar = new_calendar + # feed.calendar = new_calendar if new_calendar.loc[index, "friday"] == 1: # This is incorrrect. debug_print(1, "Found #2 listed as running on Thursday, patching") new_calendar.loc[index, "friday"] = 0 new_calendar.loc[index, "saturday"] = 1 - #feed.calendar = new_calendar + # feed.calendar = new_calendar feed.calendar = new_calendar diff --git a/timetable_kit/amtrak/json_stations.py b/timetable_kit/amtrak/json_stations.py index 24189c03..401a73c3 100755 --- a/timetable_kit/amtrak/json_stations.py +++ b/timetable_kit/amtrak/json_stations.py @@ -18,18 +18,14 @@ # will download Amtrak's station files into the './stations/' directory # otherwise runs test case -import sys -from pathlib import Path -from io import StringIO # Needed to parse JSON import argparse - -import requests -import json # better for the details import +import random +from io import StringIO # Needed to parse JSON +from pathlib import Path from time import sleep # Avoid slamming Amtrak's server too fast -- not needed import pandas as pd - -import random +import requests from timetable_kit.file_locations import get_timetable_kit_data_home @@ -72,6 +68,7 @@ def download_stations_json() -> str: def save_stations_json(stations_json: str): """Save Amtrak's basic stations database (json text) to a suitable file.""" + stations_json_local_path().parent.mkdir(exist_ok=True) with open(stations_json_local_path(), "w") as stations_json_local_file: print(stations_json, file=stations_json_local_file) @@ -448,8 +445,7 @@ def make_arg_parser(): return arg_parser -# MAIN PROGRAM -if __name__ == "__main__": +def main(): arg_parser = make_arg_parser() args = arg_parser.parse_args() @@ -463,3 +459,8 @@ def make_arg_parser(): download_one_station(str.upper(args.station_code)) case "process" | "p": do_station_processing() + + +# MAIN PROGRAM +if __name__ == "__main__": + main() diff --git a/timetable_kit/amtrak/station_type.py b/timetable_kit/amtrak/station_type.py index e2fd243b..e3b0c413 100755 --- a/timetable_kit/amtrak/station_type.py +++ b/timetable_kit/amtrak/station_type.py @@ -13,10 +13,10 @@ import sys # for sys.exit from io import StringIO # Needed to parse JSON -import pandas as pd # For parsing the HTML pages import lxml.html +import pandas as pd from timetable_kit.amtrak.json_stations import ( load_stations_json, @@ -24,7 +24,7 @@ ) # These are mine -from timetable_kit.debug import set_debug_level, debug_print +from timetable_kit.debug import set_debug_level # This is a map from what we might see in the web page, # to the key information in the form: diff --git a/timetable_kit/amtrak/wiki_station_cleanup.py b/timetable_kit/amtrak/wiki_station_cleanup.py index 8d192dd5..239e6d8b 100755 --- a/timetable_kit/amtrak/wiki_station_cleanup.py +++ b/timetable_kit/amtrak/wiki_station_cleanup.py @@ -9,9 +9,8 @@ get station names from Amtrak's JSON data. """ -import pandas as pd import gtfs_kit # type: ignore # Tell MyPy this has no type stubs - +import pandas as pd # Cities with multiple stations in the same city, requiring disambiguation two_station_cities = [ diff --git a/timetable_kit/compare.py b/timetable_kit/compare.py index 3d77b09b..b49576a0 100755 --- a/timetable_kit/compare.py +++ b/timetable_kit/compare.py @@ -12,7 +12,6 @@ # Mostly for agency defaulting from timetable_kit import runtime_config -from timetable_kit.runtime_config import agency # My packages: Local module imports from timetable_kit.debug import set_debug_level @@ -20,6 +19,7 @@ # To initialize the feed -- does type changes from timetable_kit.initialize import initialize_feed +from timetable_kit.runtime_config import agency # Common arguments for the command line from timetable_kit.timetable_argparse import ( @@ -28,6 +28,7 @@ add_debug_argument, ) + ### "Compare" Debugging routines to check for changes in timetable diff --git a/timetable_kit/convenience_types.py b/timetable_kit/convenience_types.py index 1c12f404..f483591a 100644 --- a/timetable_kit/convenience_types.py +++ b/timetable_kit/convenience_types.py @@ -2,12 +2,15 @@ # Part of timetable_kit # Copyright 2023 Nathanael Nerode. Licensed under GNU Affero GPL v.3 or later. """Types used for extra type-checking.""" +from __future__ import annotations # Do this after working our way through the codebase to fix all calls # from typing import NewType # Do this for now -from typing import TypeAlias, NewType, NamedTuple +from typing import TypeAlias, NamedTuple + +from pandas import DataFrame, Series # This must be a date in YYYYMMDD format # type GTFSDate = NewType("GTFSDate", str) @@ -28,3 +31,6 @@ class HtmlAndCss(NamedTuple): html_text: str css_text: str + + +Calendar = DataFrame | Series diff --git a/timetable_kit/core.py b/timetable_kit/core.py index 7fad7e8d..8dd4b5f7 100644 --- a/timetable_kit/core.py +++ b/timetable_kit/core.py @@ -16,21 +16,12 @@ from pathlib import Path, PurePath from typing import Type, Self, TypedDict -import tomlkit import pandas as pd +import tomlkit -############ -# My modules -# This (runtime_config) stores critical data supplied at runtime such as the agency subpackage to use. -from timetable_kit.time import ( - get_zonediff, # for "days" - explode_timestr, # for "days" - day_string, # for "days" - get_zone_str, # for TZ column -) - -from timetable_kit import text_presentation from timetable_kit import icons +from timetable_kit import text_presentation +from timetable_kit.convenience_types import GTFSDate #################################### # Specific functions from my modules @@ -43,13 +34,23 @@ TwoTripsError, InputError, ) -from timetable_kit.convenience_types import GTFSDate - from timetable_kit.feed_enhanced import GTFS_DAYS, FeedEnhanced # We call these repeatedly, so give them shorthand names -from timetable_kit.runtime_config import agency from timetable_kit.runtime_config import agency_singleton +from timetable_kit.styles import StyleHandler + +############ +# My modules +# This (runtime_config) stores critical data supplied at runtime such as the agency subpackage to use. +from timetable_kit.time import ( + get_zonediff, # for "days" + get_zone_str, + TimeTuple, # for TZ column +) + +# For the new HTML layout engine +from timetable_kit.timetable_class import Timetable # This is the big styler routine, lots of CSS; keep out of main namespace from timetable_kit.timetable_styling import ( @@ -63,10 +64,6 @@ stations_list_from_trip_id, ) -# For the new HTML layout engine -from timetable_kit.timetable_class import Timetable - - # Constant set for the special column names. # These should not be interpreted as trip_short_names or train numbers. special_column_names = { @@ -101,6 +98,7 @@ class TTSpec: def __init__(self: Self, aux: dict, csv: pd.DataFrame) -> None: self.aux: dict = aux self.csv: pd.DataFrame = csv + self._column_options = self._extract_column_options() self.__set_aux_defaults() # Set defaults for missing aux # Warning, this doesn't set crucial things like tt_id. @@ -127,7 +125,9 @@ def set_reference_date(self: Self, reference_date: GTFSDate | None): self.aux["reference_date"] = reference_date @classmethod - def from_files(cls: Type[Self], filename: str, input_dir: os.PathLike | str = "."): + def from_files( + cls: Type[Self], filename: str, input_dir: os.PathLike | str = "." + ) -> Type[Self]: """Load a tt-spec from files, both the aux and the CSV.""" input_dir = Path(input_dir) @@ -238,9 +238,8 @@ def augment_from_key_cell(self, *, feed: FeedEnhanced): Requires a feed. Requires that reference_date be set. - Note that this tucks on the end of the tt_spec. A "second row" for column- - options will therefore be unaffected. Other second rows may result in confusing - results. + Note that this tucks on the end of the tt_spec. A "second row" for column_options + will therefore be unaffected. Other second rows may result in confusing results. Prints the CSV. """ @@ -460,7 +459,7 @@ def get_train_specs_list(self: Self) -> list[str]: ] return train_specs_list - def extract_column_options(self: Self): + def _extract_column_options(self: Self): """If this TTSpec has column-options in row 2 of the CSV, remove that row and fill in the column_options structure. @@ -474,19 +473,18 @@ def extract_column_options(self: Self): We HAVE to reindex after removing the column_options from the CSV. """ - self.column_options: list[list[str]] + # Consider generalizing to allow column-options in other rows if str(self.csv.iloc[1, 0]).lower() not in ["column-options", "column_options"]: column_count = self.csv.shape[1] # What, there weren't any? Make a list containing blank lists: - self.column_options = [[]] * column_count + return [[]] * column_count # No column-options row, so don't delete it - return + # Now for the main version column_options_df = self.csv.iloc[1, 0:] # second row, all of it column_options_raw_list = column_options_df.to_list() column_options_nested_list = [str(i).split() for i in column_options_raw_list] - self.column_options = column_options_nested_list # Now delete row 2. # This drops by index and not by actual row number, FIXME # Thankfully they're currently the same @@ -495,8 +493,13 @@ def extract_column_options(self: Self): # We MUST reset the index so rows are numbered 0 to end without breaks # Drop old index, operate in place self.csv.reset_index(drop=True, inplace=True) - debug_print(1, "Column options separated from CSV:", self.column_options) + debug_print(1, "Column options separated from CSV:", column_options_nested_list) debug_print(6, "New CSV:", self.csv) + return column_options_nested_list + + @property + def column_options(self): + return self._column_options class _CellCodes(TypedDict, total=False): @@ -752,6 +755,7 @@ def raise_error_if_not_one_row(trips): def fill_tt_spec( spec: TTSpec, + style: StyleHandler, *, today_feed: FeedEnhanced, doing_html=False, @@ -806,7 +810,6 @@ def fill_tt_spec( debug_print(1, "Working with reference date ", reference_date) # Set correct defaults for various things not always in the aux - times_24h = bool(spec.aux.get("times_24h")) train_numbers_side_by_side = bool(spec.aux.get("train_numbers_side_by_side")) use_bus_icon_in_cells = bool(spec.aux.get("use_bus_icon_in_cells")) box_time_characters = bool(spec.aux.get("box_time_characters")) @@ -817,7 +820,6 @@ def fill_tt_spec( # Clean up the spec. spec.strip_omits() - spec.extract_column_options() # Also removes column_options from main CSV dataframe spec.augment_from_key_cell(feed=today_feed) # Expand "shorthand" specs # We have a filtered feed. We're going to have to map from tsns to trip_ids, repeatedly. @@ -1150,6 +1152,7 @@ def route_from_train_spec_local(train_spec: str) -> pd.Series: t.text.iloc[y, x] = text_presentation.style_updown( reverse, doing_html=doing_html ) + pass case [ "days" | "days-of-week", ck, @@ -1198,14 +1201,18 @@ def route_from_train_spec_local(train_spec: str) -> pd.Series: stop_tz = stop_df.iloc[0].stop_timezone zonediff = get_zonediff(stop_tz, agency_tz, reference_date) # Get the day change for the reference stop (format is explained in text_presentation) - departure = explode_timestr(timepoint.departure_time, zonediff) + departure = TimeTuple.from_gtfs_time_string( + timepoint.departure_time, zonediff + ) offset = int(departure.day) # Finally, get the calendar (must be unique) calendar = today_feed.calendar[ today_feed.calendar.service_id == my_trip.service_id ] # And fill in the actual string - daystring = day_string(calendar, offset=offset) + daystring = style.format_day_string( + calendar, offset=offset, html=doing_html + ) # TODO: add some HTML styling here t.text.iloc[y, x] = daystring # Color this cell @@ -1401,7 +1408,7 @@ def route_from_train_spec_local(train_spec: str) -> pd.Series: ) ) - calendar = None # if not use_daystring, save time + # calendar = None # if not use_daystring, save time if use_daystring: calendar = today_feed.calendar[ today_feed.calendar.service_id == my_trip.service_id @@ -1461,12 +1468,12 @@ def route_from_train_spec_local(train_spec: str) -> pd.Series: stop_tz=stop_tz, agency_tz=agency_tz, reference_date=reference_date, + style=style, doing_html=doing_html, box_time_characters=box_time_characters, reverse=reverse, two_row=two_row, use_ar_dp_str=this_column_gets_ardp, - times_24h=times_24h, use_daystring=use_daystring, calendar=calendar, long_days_box=long_days_box, @@ -1489,7 +1496,9 @@ def route_from_train_spec_local(train_spec: str) -> pd.Series: t.classes.fillna(value="", inplace=True) # debug_print(1, "Classes table:", t.classes) # This is only set if it's "True"; fill in the "False" states. - t.th.fillna(value=False, inplace=True) + with pd.option_context("future.no_silent_downcasting", True): + t.th.fillna(value=False, inplace=True) + t.th.infer_objects(copy=False) # This is only set if it's not empty; fill in the "" states. t.attributes.fillna(value="", inplace=True) # Correct default diff --git a/timetable_kit/feed_enhanced.py b/timetable_kit/feed_enhanced.py index 0f19f9de..0668e00d 100644 --- a/timetable_kit/feed_enhanced.py +++ b/timetable_kit/feed_enhanced.py @@ -11,26 +11,24 @@ It also gets rid of the shapes table, because it's huge and we don't use it. """ +import datetime # for filter_for_utilities, to get current date if necessary from collections.abc import Iterable -from typing import Type, Self, NamedTuple - +from dataclasses import dataclass from operator import not_ # Needed for bad_service_id filter +from typing import Type, Self, Optional -import datetime # for filter_for_utilities, to get current date if necessary - -from pandas import DataFrame, Series import gtfs_kit # type: ignore # Tell MyPy this has no type stubs +from pandas import DataFrame, Series # These are used to distinguish str types with special restrictions. from timetable_kit.convenience_types import GTFSDate, GTFSDay - +from timetable_kit.debug import debug_print from timetable_kit.errors import ( NoTripError, TwoTripsError, TwoStopsError, InputError, ) -from timetable_kit.debug import debug_print GTFS_DAYS = ( "monday", @@ -45,12 +43,19 @@ headers, appropriately lowercase for the column headers.""" -class DateRange(NamedTuple): +@dataclass +class DateRange: """Used to track what dates a timetable is valid for.""" latest_start_date: str earliest_end_date: str + def is_invalid(self) -> bool: + return not self.latest_start_date or not self.earliest_end_date + + def is_one_day(self) -> bool: + return self.latest_start_date == self.earliest_end_date + class FeedEnhanced(gtfs_kit.Feed): """GTFS feed class enhanced for timetable_kit use""" @@ -58,20 +63,20 @@ class FeedEnhanced(gtfs_kit.Feed): def __init__( self, dist_units: str, - agency: DataFrame | None = None, - stops: DataFrame | None = None, - routes: DataFrame | None = None, - trips: DataFrame | None = None, - stop_times: DataFrame | None = None, - calendar: DataFrame | None = None, - calendar_dates: DataFrame | None = None, - fare_attributes: DataFrame | None = None, - fare_rules: DataFrame | None = None, - shapes: DataFrame | None = None, - frequencies: DataFrame | None = None, - transfers: DataFrame | None = None, - feed_info: DataFrame | None = None, - attributions: DataFrame | None = None, + agency: Optional[DataFrame] = None, + stops: Optional[DataFrame] = None, + routes: Optional[DataFrame] = None, + trips: Optional[DataFrame] = None, + stop_times: Optional[DataFrame] = None, + calendar: Optional[DataFrame] = None, + calendar_dates: Optional[DataFrame] = None, + fare_attributes: Optional[DataFrame] = None, + fare_rules: Optional[DataFrame] = None, + shapes: Optional[DataFrame] = None, + frequencies: Optional[DataFrame] = None, + transfers: Optional[DataFrame] = None, + feed_info: Optional[DataFrame] = None, + attributions: Optional[DataFrame] = None, ) -> None: # doing it long form instead of the mildly cursed way gtfs_kit does, because IDEs choke on that super().__init__( diff --git a/timetable_kit/feed_enhanced.pyi b/timetable_kit/feed_enhanced.pyi index bc3514c2..1b2c4ae5 100644 --- a/timetable_kit/feed_enhanced.pyi +++ b/timetable_kit/feed_enhanced.pyi @@ -4,35 +4,40 @@ # Copyright 2022, 2023 Nathanael Nerode. Licensed under GNU Affero GPL v.3 or later. from collections.abc import Iterable -from typing import Type, Self, NamedTuple +from dataclasses import dataclass +from typing import Type, Self, Optional -from pandas import DataFrame, Series from gtfs_kit import Feed # type: ignore # Tell MyPy this has no type stubs +from pandas import DataFrame, Series GTFS_DAYS: tuple[str, str, str, str, str, str, str] -class DateRange(NamedTuple): +@dataclass +class DateRange: """Used to track what dates a timetable is valid for.""" latest_start_date: str earliest_end_date: str + def is_invalid(self) -> bool: ... + def is_one_day(self) -> bool: ... + class FeedEnhanced(Feed): dist_units: str - agency: DataFrame | None = None - stops: DataFrame | None = None - routes: DataFrame | None = None - trips: DataFrame | None = None - stop_times: DataFrame | None = None - calendar: DataFrame | None = None - calendar_dates: DataFrame | None = None - fare_attributes: DataFrame | None = None - fare_rules: DataFrame | None = None - shapes: DataFrame | None = None - frequencies: DataFrame | None = None - transfers: DataFrame | None = None - feed_info: DataFrame | None = None - attributions: DataFrame | None = None + agency: Optional[DataFrame] = None + stops: Optional[DataFrame] = None + routes: Optional[DataFrame] = None + trips: Optional[DataFrame] = None + stop_times: Optional[DataFrame] = None + calendar: Optional[DataFrame] = None + calendar_dates: Optional[DataFrame] = None + fare_attributes: Optional[DataFrame] = None + fare_rules: Optional[DataFrame] = None + shapes: Optional[DataFrame] = None + frequencies: Optional[DataFrame] = None + transfers: Optional[DataFrame] = None + feed_info: Optional[DataFrame] = None + attributions: Optional[DataFrame] = None @classmethod def enhance(cls: Type[Self], regular_feed: Feed) -> Self: ... diff --git a/timetable_kit/file_handling.py b/timetable_kit/file_handling.py index ab9c713e..fc528eb0 100755 --- a/timetable_kit/file_handling.py +++ b/timetable_kit/file_handling.py @@ -9,6 +9,7 @@ """ from pathlib import Path + from timetable_kit.errors import InputError diff --git a/timetable_kit/file_locations.py b/timetable_kit/file_locations.py index 6dade02a..59c60a9d 100644 --- a/timetable_kit/file_locations.py +++ b/timetable_kit/file_locations.py @@ -21,7 +21,7 @@ def get_timetable_kit_data_home(system: bool = False) -> Path: return _data_home / "timetable_kit" -def get_search_list(base: PathLike) -> list[Path]: +def get_search_list(base: PathLike | str) -> list[Path]: """Given a relative base resource type to search for (like "templates"), give a list of directories to search Typically ["~/.local/share/timetable_kit/templates","/var/lib/timetable_kit/templates"] diff --git a/timetable_kit/generic_agency/__init__.py b/timetable_kit/generic_agency/__init__.py index 60904658..72d6da35 100644 --- a/timetable_kit/generic_agency/__init__.py +++ b/timetable_kit/generic_agency/__init__.py @@ -8,10 +8,11 @@ # The Agency class type, for others to inherit from -from .agency import Agency +from .agency import Agency, AgencySingletonGetter # The singleton instance of a class, for stateful memoization from .agency import get_singleton +from ..errors import InputError # Object explaining where to find the GTFS & which can also download it diff --git a/timetable_kit/generic_agency/agency.py b/timetable_kit/generic_agency/agency.py index 6f600201..a58b93f1 100644 --- a/timetable_kit/generic_agency/agency.py +++ b/timetable_kit/generic_agency/agency.py @@ -7,17 +7,22 @@ Amtrak and others need to provide the same interface. This should be made easier by class inheritance. """ +from typing import Protocol -from timetable_kit.feed_enhanced import FeedEnhanced +# Find the HTML for a specific connecting agency's logo +from timetable_kit.connecting_services import get_connecting_service_logo_html from timetable_kit.debug import debug_print +from timetable_kit.feed_enhanced import FeedEnhanced # For text twiddling -from timetable_kit import text_assembly -from timetable_kit.text_assembly import href_wrap, and_clause, or_clause -from timetable_kit.text_assembly import SAFE_BR - -# Find the HTML for a specific connecting agency's logo -from timetable_kit.connecting_services import get_connecting_service_logo_html +from timetable_kit.text_assembly import ( + SAFE_BR, + station_name_to_single_line_text, + station_name_to_multiline_text, + href_wrap, + and_clause, + or_clause, +) # Intended to be used both directly and by subclasses @@ -327,11 +332,6 @@ def add_via_disclaimer(self, doing_html=True) -> bool: # There is probably a better way to do this. return False - def agency_css_class(self) -> str: - """Name of a CSS class for agency-specific styling.""" - # Default is blank. This generates class="". - return "" - def get_all_connecting_services(self, station_list: list[str]) -> list[str]: """Given a list of station codes, return a list of services which connect (with no duplicates)""" @@ -568,10 +568,10 @@ def get_station_name_pretty( stop_name_raw, facility_name, station_code, major ) elif doing_multiline_text: - reassemble = text_assembly.station_name_to_multiline_text + reassemble = station_name_to_multiline_text return reassemble(stop_name_raw, facility_name, station_code, major) else: - reassemble = text_assembly.station_name_to_single_line_text + reassemble = station_name_to_single_line_text return reassemble(stop_name_raw, facility_name, station_code, major) @@ -579,7 +579,12 @@ def get_station_name_pretty( _singleton = Agency() -def get_singleton(): +def get_singleton() -> Agency: """Get singleton for generic agency.""" global _singleton return _singleton + + +class AgencySingletonGetter(Protocol): + @staticmethod + def get_singleton() -> Agency: ... diff --git a/timetable_kit/get_gtfs.py b/timetable_kit/get_gtfs.py index 489c1f6c..d3be4512 100755 --- a/timetable_kit/get_gtfs.py +++ b/timetable_kit/get_gtfs.py @@ -9,19 +9,12 @@ since it's used during initalization of every /get_gtfs.py file. """ -from typing import Self - -import sys # for sys.exit +import shutil # for rmtree from os import PathLike from pathlib import Path -import shutil # for rmtree - - -from xdg_base_dirs import xdg_data_home # for where to put the files - +from typing import Self from zipfile import ZipFile - import requests from timetable_kit.file_locations import get_timetable_kit_data_home diff --git a/timetable_kit/greyhound/agency.py b/timetable_kit/greyhound/agency.py index b7976c37..23b21dba 100644 --- a/timetable_kit/greyhound/agency.py +++ b/timetable_kit/greyhound/agency.py @@ -7,12 +7,10 @@ """ from __future__ import annotations +import timetable_kit.greyhound.gtfs_patches as gtfs_patches # for patch_feed from timetable_kit.feed_enhanced import FeedEnhanced from timetable_kit.generic_agency import Agency -# for patch_feed -import timetable_kit.greyhound.gtfs_patches as gtfs_patches - class AgencyGreyhound(Agency): """Greyhound-specific code for interpreting specs and GTFS feeds.""" diff --git a/timetable_kit/greyhound/get_gtfs.py b/timetable_kit/greyhound/get_gtfs.py index 56b13e9e..a9b76db0 100755 --- a/timetable_kit/greyhound/get_gtfs.py +++ b/timetable_kit/greyhound/get_gtfs.py @@ -21,6 +21,10 @@ def get_gtfs_files(): return _gtfs_files +def main(): + get_gtfs_files().download_and_save() + + # MAIN PROGRAM if __name__ == "__main__": - get_gtfs_files().download_and_save() + main() diff --git a/timetable_kit/hartford_line/get_gtfs.py b/timetable_kit/hartford_line/get_gtfs.py index c3c3a868..7918c861 100755 --- a/timetable_kit/hartford_line/get_gtfs.py +++ b/timetable_kit/hartford_line/get_gtfs.py @@ -4,24 +4,23 @@ # Copyright 2022, 2023 Nathanael Nerode. Licensed under GNU Affero GPL v.3 or later. """Retrieve CT Rail Hartford Line's static GTFS data and merge with Amtrak's.""" -from typing import Self from pathlib import Path +from typing import Self from zipfile import ZipFile # For loading the feeds for merging import gtfs_kit # type: ignore # Tell MyPy this has no type stubs +import timetable_kit.amtrak as amtrak # for get_gtfs_files from timetable_kit.get_gtfs import ( AgencyGTFSFiles, move_old_dir, move_old_file, ) -import timetable_kit.amtrak as amtrak # for get_gtfs_files # For the merge process from timetable_kit.merge_gtfs import merge_feed, remove_stop_code_column - # GTFS seems to be at: # https://www.cttransit.com/about/developers # But it uses *numerical* service IDs 1 (disabled), 2 (weekday), 3 (weekend) @@ -173,6 +172,10 @@ def get_gtfs_files(): return _gtfs_files +def main(): + get_gtfs_files().download_and_save() + + # MAIN PROGRAM if __name__ == "__main__": - get_gtfs_files().download_and_save() + main() diff --git a/timetable_kit/initialize.py b/timetable_kit/initialize.py index 001790f3..be9ac089 100644 --- a/timetable_kit/initialize.py +++ b/timetable_kit/initialize.py @@ -8,7 +8,6 @@ # Other people's packages from pathlib import Path -import datetime import gtfs_kit # type: ignore # Tell MyPy this has no type stubs @@ -18,11 +17,11 @@ from timetable_kit.feed_enhanced import FeedEnhanced # For the Agency singleton -from timetable_kit.runtime_config import agency_singleton +from timetable_kit.timetable_class import TTConfig # INITIALIZATION CODE -def initialize_feed(gtfs, patch_the_feed: bool = True) -> FeedEnhanced: +def initialize_feed(config: TTConfig) -> FeedEnhanced: """Initialize the master_feed and related variables. Does some cleaning, and removal of the large shapes table which we don't use. @@ -30,13 +29,13 @@ def initialize_feed(gtfs, patch_the_feed: bool = True) -> FeedEnhanced: Also does agency-specific patching -- optionally. Also initializes the agency singleton from the feed *after* that. - NOTE, this is a side-effect, not great coding style, FIXME + NOTE, this is a side effect, not great coding style, FIXME - gtfs: may be a filename or a Path. + config: TTConfig instance for configuring output """ - debug_print(1, "Using GTFS file " + str(gtfs)) - gtfs_path = Path(gtfs) + debug_print(1, "Using GTFS file " + str(config.gtfs_filename)) + gtfs_path = Path(config.gtfs_filename) # The unit is only relevant if we read the shapes file; we currently don't. # Also affects display miles so default to "mi". plain_feed = gtfs_kit.read_feed(gtfs_path, dist_units="mi") @@ -58,16 +57,16 @@ def initialize_feed(gtfs, patch_the_feed: bool = True) -> FeedEnhanced: master_feed = FeedEnhanced.enhance(plain_feed) # Patch the feed in an agency-specific fashion. - if patch_the_feed: - master_feed = agency_singleton().patch_feed(master_feed) + if config.patch_the_feed: + master_feed = config.agency.patch_feed(master_feed) debug_print(1, "Feed patched, hopefully") else: # Have to patch in the wheelchair access info regardless - master_feed = agency_singleton().patch_feed_wheelchair_access_only(master_feed) + master_feed = config.agency.patch_feed_wheelchair_access_only(master_feed) debug_print(1, "Feed patched for wheelchair access only") # Initialize the singleton from the feed - # (This has side-effects, note, not pass-by-value semantics) - agency_singleton().init_from_feed(master_feed) + # (This has side effects, note, not pass-by-value semantics) + config.agency.init_from_feed(master_feed) return master_feed diff --git a/timetable_kit/list_stations.py b/timetable_kit/list_stations.py index 02cf1fc8..59908746 100755 --- a/timetable_kit/list_stations.py +++ b/timetable_kit/list_stations.py @@ -9,7 +9,7 @@ import sys # For sys.exit from timetable_kit import runtime_config # for the agency() -from timetable_kit.debug import debug_print, set_debug_level +from timetable_kit.debug import set_debug_level from timetable_kit.initialize import initialize_feed from timetable_kit.runtime_config import agency # for the agency() diff --git a/timetable_kit/load_resources.py b/timetable_kit/load_resources.py index 3946d6f2..e1dcbb08 100644 --- a/timetable_kit/load_resources.py +++ b/timetable_kit/load_resources.py @@ -37,6 +37,7 @@ get_logo_svg(filename: str) -> str get_connecting_services_csv(filename: str) -> str """ +from pathlib import Path from jinja2 import ( Environment, @@ -201,6 +202,26 @@ def get_connecting_services_csv(filename: str) -> str: return connecting_services_csv_str +def get_style_toml(filename: str) -> str: + """Load a style from either the user config directory or the default location. + The filename can just be the name or the full path. For example, "default" for the default style. + This is to enable directly passing the option from the command line. + """ + if not filename.endswith(".toml"): + filename += ".toml" + + maybe_paths = ( + Path(filename), + *(_dir / filename for _dir in get_search_list("styles")), + Path(__file__).parent / "styles" / filename, + ) + + for path in maybe_paths: + if path.exists() and path.is_file(): + with path.open() as f: + return f.read() # return the first valid entry in the hierarchy + + # TESTING if __name__ == "__main__": page_tpl = template_environment.get_template("page_standard.html") diff --git a/timetable_kit/make_spec.py b/timetable_kit/make_spec.py index 2fb70e2c..82dd661d 100755 --- a/timetable_kit/make_spec.py +++ b/timetable_kit/make_spec.py @@ -11,7 +11,7 @@ import sys # for exit import pandas as pd -from pandas import DataFrame, Series +from pandas import Series from timetable_kit import runtime_config # for the agency() from timetable_kit.debug import set_debug_level diff --git a/timetable_kit/maple_leaf/agency.py b/timetable_kit/maple_leaf/agency.py index 38cf339f..41d1eb04 100644 --- a/timetable_kit/maple_leaf/agency.py +++ b/timetable_kit/maple_leaf/agency.py @@ -6,9 +6,7 @@ This holds a class for "AgencyMapleLeaf" intended to be used as a singleton. """ import timetable_kit.text_assembly as text_assembly - from timetable_kit.amtrak import AgencyAmtrak -from timetable_kit.via import AgencyVIA # Map from station codes to connecting service names # This is stashed in a class variable @@ -16,6 +14,7 @@ # For getting VIA station codes to print them from timetable_kit.maple_leaf.station_data import amtrak_code_to_via_code +from timetable_kit.via import AgencyVIA # This should mostly be based on Amtrak. @@ -82,10 +81,6 @@ def get_all_connecting_services(self, station_list: list[str]) -> list[str]: print("Enhanced station list", enhanced_station_list) return super().get_all_connecting_services(enhanced_station_list) - def agency_css_class(self) -> str: - """Name of a CSS class for agency-specific styling.""" - return "maple-leaf-special-css" - # Establish the singleton _singleton = AgencyMapleLeaf() diff --git a/timetable_kit/maple_leaf/get_gtfs.py b/timetable_kit/maple_leaf/get_gtfs.py index 9a1d6488..1b662169 100755 --- a/timetable_kit/maple_leaf/get_gtfs.py +++ b/timetable_kit/maple_leaf/get_gtfs.py @@ -4,29 +4,19 @@ # Copyright 2023 Nathanael Nerode. Licensed under GNU Affero GPL v.3 or later. """Routines for creating a Maple Leaf GTFS from Amtrak's GTFS and VIA's GTFS.""" -import sys # for sys.exit - from typing import Self -from pathlib import Path -from zipfile import ZipFile -import pandas as pd import gtfs_kit # type: ignore # Tell MyPy this has no type stubs -# Mine -from timetable_kit.feed_enhanced import FeedEnhanced # for filtering - # For the merge from timetable_kit import amtrak from timetable_kit import via - -from timetable_kit.get_gtfs import AgencyGTFSFiles, move_old_dir, move_old_file -from timetable_kit.merge_gtfs import merge_feed, remove_stop_code_column - +from timetable_kit.feed_enhanced import FeedEnhanced # for filtering +from timetable_kit.get_gtfs import AgencyGTFSFiles, move_old_file from timetable_kit.maple_leaf.station_data import ( - amtrak_code_to_via_code, via_code_to_amtrak_code, ) +from timetable_kit.merge_gtfs import merge_feed, remove_stop_code_column def translate_via_stations_to_amtrak(via_ml_feed): @@ -170,5 +160,9 @@ def get_gtfs_files(): return _gtfs_files -if __name__ == "__main__": +def main(): get_gtfs_files().download_and_save() + + +if __name__ == "__main__": + main() diff --git a/timetable_kit/page_layout.py b/timetable_kit/page_layout.py index 111aca6f..c8175d6a 100644 --- a/timetable_kit/page_layout.py +++ b/timetable_kit/page_layout.py @@ -13,35 +13,34 @@ # Other people's packages import datetime # for getting today's date for credit on the timetable -# My packages -# We need runtime data such as the subpackage for the agency (amtrak, via, etc.) -# And we need a shorthand way to refer to it -from timetable_kit.runtime_config import agency_singleton +from timetable_kit import connecting_services +from timetable_kit import icons # The type, used for argument passing from timetable_kit.convenience_types import HtmlAndCss from timetable_kit.core import TTSpec - -from timetable_kit.time import gtfs_date_to_isoformat -from timetable_kit import text_presentation -from timetable_kit import icons -from timetable_kit import connecting_services - -from timetable_kit.debug import debug_print - +from timetable_kit.feed_enhanced import DateRange from timetable_kit.load_resources import ( get_font_css, template_environment, ) +# My packages +# We need runtime data such as the subpackage for the agency (amtrak, via, etc.) +# And we need a shorthand way to refer to it +from timetable_kit.runtime_config import agency_singleton +from timetable_kit.styles import StyleHandler +from timetable_kit.time import gtfs_date_to_isoformat +from timetable_kit.timetable_class import TTConfig + def produce_html_page( timetable_styled_html, *, + style: StyleHandler, spec: TTSpec, # for aux content (including page_id) and list of station codes - author, - start_date, - end_date, + config: TTConfig, + date_range: DateRange ) -> HtmlAndCss: """ Take the output of style_timetable_for_html -- which is mostly a table -- @@ -85,11 +84,11 @@ def produce_html_page( services_list=services_list, one_line=(not spec.aux.get("key_on_right")) ) - ### Prepare Jinja template substitution: + # Prepare Jinja template substitution: production_date_str = datetime.date.today().isoformat() - start_date_str = gtfs_date_to_isoformat(start_date) - end_date_str = gtfs_date_to_isoformat(end_date) + start_date_str = gtfs_date_to_isoformat(date_range.latest_start_date) + end_date_str = gtfs_date_to_isoformat(date_range.earliest_end_date) html_params = { "page_id": page_id, @@ -100,17 +99,15 @@ def produce_html_page( "production_date": production_date_str, "start_date": start_date_str, "end_date": end_date_str, - "author": author, + "author": config.author, "connecting_services_keys_html": connecting_services_keys_html, - "connecting_bus_key_sentence": agency_singleton().connecting_bus_key_sentence(), # "Connecting Bus Service (can be booked through Amtrak)" - "agency_css_class": spec.aux.get( - "agency_css_class", agency_singleton().agency_css_class() - ), # Used to change color of top heading & prefix with agency name - "unofficial_disclaimer": agency_singleton().unofficial_disclaimer(), # "This is unofficial" disclaimer - "always_check_disclaimer": agency_singleton().always_check_disclaimer(), # "Always check agency website" - "gtfs_data_link": agency_singleton().gtfs_data_link(), # "GTFS data" - "by_agency_with_gtfs_link": agency_singleton().by_agency_with_gtfs_link(), # for GTFS released "by Amtrak" - "add_via_disclaimer": agency_singleton().add_via_disclaimer(), # True or False, should we add the VIA disclaimer + "connecting_bus_key_sentence": config.agency.connecting_bus_key_sentence(), # "Connecting Bus Service (can be booked through Amtrak)" + "agency_css_class": style.special_css_tag, # Used to change color of top heading & prefix with agency name + "unofficial_disclaimer": config.agency.unofficial_disclaimer(), # "This is unofficial" disclaimer + "always_check_disclaimer": config.agency.always_check_disclaimer(), # "Always check agency website" + "gtfs_data_link": config.agency.gtfs_data_link(), # "GTFS data" + "by_agency_with_gtfs_link": config.agency.by_agency_with_gtfs_link(), # for GTFS released "by Amtrak" + "add_via_disclaimer": config.agency.add_via_disclaimer(), # True or False, should we add the VIA disclaimer } # Allows direct icon references in Jinja2 @@ -165,7 +162,9 @@ def produce_html_page( return result -def produce_html_file(pages: list[HtmlAndCss], *, title, for_rpa=False): +def produce_html_file( + pages: list[HtmlAndCss], *, title, for_rpa=False, agency_special_css: str = "" +): """ Take a *list* of containers output by calling produce_html_page, which are like this: html_text -- an HTML
section for a page @@ -181,6 +180,7 @@ def produce_html_file(pages: list[HtmlAndCss], *, title, for_rpa=False): # Get the CSS for styling icons (contains vertical alignment and 1em height/width) # This is used every time an icon is inserted. # This includes the CSS for all icons whether used in this timetable or not. + # TODO get a list of all connecting services and base this import off of that instead of loading everything icons_css = icons.get_css_for_all_icons() # For connecting service logos as imgs: @@ -189,19 +189,17 @@ def produce_html_file(pages: list[HtmlAndCss], *, title, for_rpa=False): # The @font-face directives: # Eventually the list of fonts should be passed in. FIXME. - fonts = ["SpartanTT"] + fonts = ["SpartanTT"] # , "Noto Sans", "Noto Sans Mono"] # It breaks Weasyprint to include references to nonexistent fonts, # So we have to make sure it only includes used fonts. # (Including nonexistent fonts works OK for rendering in Firefox, though.) - fonts_css_list = [] - for font in fonts: - fonts_css_list.append(get_font_css(font)) - font_faces_css = "".join(fonts_css_list) + font_faces_css = " ".join(get_font_css(font) for font in fonts) stylesheet_params = { "icons_css": icons_css, "logos_css": logos_css, "font_faces_css": font_faces_css, + "agency_special_css": agency_special_css, } html_file_params = { diff --git a/timetable_kit/reverse_csv.py b/timetable_kit/reverse_csv.py index 214a3175..9940006f 100755 --- a/timetable_kit/reverse_csv.py +++ b/timetable_kit/reverse_csv.py @@ -5,8 +5,8 @@ """This simply reverses the rows in a CSV file """ import sys -import pandas as pd +import pandas as pd # TO DO: do a smarter parse of this. # Keep header rows at the top, diff --git a/timetable_kit/runtime_config.py b/timetable_kit/runtime_config.py index 560d637a..246ba267 100644 --- a/timetable_kit/runtime_config.py +++ b/timetable_kit/runtime_config.py @@ -8,22 +8,23 @@ This data includes the critical choice of which agency's subpackage to use. """ import pathlib + # For sys.exit import sys -from timetable_kit.debug import debug_print +import timetable_kit.amtrak # The agencies we might need to import import timetable_kit.generic_agency -import timetable_kit.amtrak -import timetable_kit.via +import timetable_kit.greyhound import timetable_kit.hartford_line import timetable_kit.maple_leaf -import timetable_kit.greyhound +import timetable_kit.via +from timetable_kit.debug import debug_print # These will get set elsewhere, later, by initialization code. -agency_name = None -agency_package = None +agency_name: str = None +agency_package: timetable_kit.generic_agency.AgencySingletonGetter = None agency_input_dir = None timetable_kit_directory = pathlib.Path(__file__).parent diff --git a/timetable_kit/specs_amtrak/adirondack.toml b/timetable_kit/specs_amtrak/adirondack.toml index 92027c98..5b1a111a 100644 --- a/timetable_kit/specs_amtrak/adirondack.toml +++ b/timetable_kit/specs_amtrak/adirondack.toml @@ -5,4 +5,4 @@ top_text = "Adirondack service will be cancelled north of Saratoga Springs, NY b reference_date = "20240620" key_d = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/auto-train.toml b/timetable_kit/specs_amtrak/auto-train.toml index 6a26a034..27e294c2 100644 --- a/timetable_kit/specs_amtrak/auto-train.toml +++ b/timetable_kit/specs_amtrak/auto-train.toml @@ -3,4 +3,4 @@ heading = "Auto Train" aria_label = "Auto Train" reference_date = "20240620" top_text = "The Auto Train carries passengers only with a motor vehicle.

Auto Train check-in for vehicles starts at 12:30 PM and closes at 3:00 PM in both directions.
For details and up-to-date requirements, see
https://www.amtrak.com/route-content/auto-train-boarding-and-vehicle-requirements.html." -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/berkshire-flyer.toml b/timetable_kit/specs_amtrak/berkshire-flyer.toml index 323e4541..0a5bc4d9 100644 --- a/timetable_kit/specs_amtrak/berkshire-flyer.toml +++ b/timetable_kit/specs_amtrak/berkshire-flyer.toml @@ -5,4 +5,4 @@ top_text = "The Berkshire Flyer is a seasonal train service that runs on Fridays reference_date = "20240620" key_d = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/borealis.toml b/timetable_kit/specs_amtrak/borealis.toml index f7b67b5d..19f13f5b 100644 --- a/timetable_kit/specs_amtrak/borealis.toml +++ b/timetable_kit/specs_amtrak/borealis.toml @@ -8,4 +8,4 @@ key_baggage = true key_d = true key_r = true key_tz = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/california-zephyr.toml b/timetable_kit/specs_amtrak/california-zephyr.toml index be691029..1e1d0eb5 100644 --- a/timetable_kit/specs_amtrak/california-zephyr.toml +++ b/timetable_kit/specs_amtrak/california-zephyr.toml @@ -10,4 +10,4 @@ use_bus_icon_in_cells = true key_d = true key_r = true key_tz = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/capitol-corridor-weekday-eb.toml b/timetable_kit/specs_amtrak/capitol-corridor-weekday-eb.toml index 8d4f841e..78c8bc74 100644 --- a/timetable_kit/specs_amtrak/capitol-corridor-weekday-eb.toml +++ b/timetable_kit/specs_amtrak/capitol-corridor-weekday-eb.toml @@ -7,4 +7,4 @@ key_bus = true use_bus_icon_in_cells = true key_baggage = true key_d = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/capitol-corridor-weekday-wb.toml b/timetable_kit/specs_amtrak/capitol-corridor-weekday-wb.toml index 21e4cb04..8bcb5a7f 100644 --- a/timetable_kit/specs_amtrak/capitol-corridor-weekday-wb.toml +++ b/timetable_kit/specs_amtrak/capitol-corridor-weekday-wb.toml @@ -8,4 +8,4 @@ key_bus = true use_bus_icon_in_cells = true key_baggage = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/capitol-corridor-weekend-eb.toml b/timetable_kit/specs_amtrak/capitol-corridor-weekend-eb.toml index f5bfa3b8..cdec36b8 100644 --- a/timetable_kit/specs_amtrak/capitol-corridor-weekend-eb.toml +++ b/timetable_kit/specs_amtrak/capitol-corridor-weekend-eb.toml @@ -8,4 +8,4 @@ key_bus = true use_bus_icon_in_cells = true key_baggage = true key_d = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/capitol-corridor-weekend-wb.toml b/timetable_kit/specs_amtrak/capitol-corridor-weekend-wb.toml index 894d58a3..206943e7 100644 --- a/timetable_kit/specs_amtrak/capitol-corridor-weekend-wb.toml +++ b/timetable_kit/specs_amtrak/capitol-corridor-weekend-wb.toml @@ -10,4 +10,4 @@ key_bus = true use_bus_icon_in_cells = true key_baggage = true key_d = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/capitol-limited.toml b/timetable_kit/specs_amtrak/capitol-limited.toml index 1cb025c6..277da704 100644 --- a/timetable_kit/specs_amtrak/capitol-limited.toml +++ b/timetable_kit/specs_amtrak/capitol-limited.toml @@ -4,4 +4,4 @@ aria_label = "Capitol Limited" reference_date = "20240620" key_baggage = true key_tz = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/cardinal.toml b/timetable_kit/specs_amtrak/cardinal.toml index 95e5369d..4cf25e99 100644 --- a/timetable_kit/specs_amtrak/cardinal.toml +++ b/timetable_kit/specs_amtrak/cardinal.toml @@ -6,4 +6,4 @@ key_baggage = true key_d = true key_r = true key_tz = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/carolinian-piedmont-nb.toml b/timetable_kit/specs_amtrak/carolinian-piedmont-nb.toml index 20b7d3c6..c8e1863a 100644 --- a/timetable_kit/specs_amtrak/carolinian-piedmont-nb.toml +++ b/timetable_kit/specs_amtrak/carolinian-piedmont-nb.toml @@ -5,4 +5,4 @@ reference_date = "20240620" programmers_warning = "Train 80, 19, and 20 have weird variations on incoming NEC timings." key_baggage = true key_d = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/carolinian-piedmont-sb.toml b/timetable_kit/specs_amtrak/carolinian-piedmont-sb.toml index 6637cc2e..0281248f 100644 --- a/timetable_kit/specs_amtrak/carolinian-piedmont-sb.toml +++ b/timetable_kit/specs_amtrak/carolinian-piedmont-sb.toml @@ -5,4 +5,4 @@ reference_date = "20240620" programmers_warning = "Train 80, 19, and 20 have weird variations on incoming NEC timings." key_baggage = true key_d = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/cascades-nb.toml b/timetable_kit/specs_amtrak/cascades-nb.toml index 495e778b..101cd61a 100644 --- a/timetable_kit/specs_amtrak/cascades-nb.toml +++ b/timetable_kit/specs_amtrak/cascades-nb.toml @@ -4,6 +4,6 @@ aria_label = "Cascades Northbound" reference_date = "20240620" programmers_warning = "Oregon Point buses missing. Coast Starlight duplicate timetable problems." key_baggage = true -for_rpa = true + key_bus = true use_bus_icon_in_cells = true diff --git a/timetable_kit/specs_amtrak/cascades-sb.toml b/timetable_kit/specs_amtrak/cascades-sb.toml index 0edb534f..0e63e506 100644 --- a/timetable_kit/specs_amtrak/cascades-sb.toml +++ b/timetable_kit/specs_amtrak/cascades-sb.toml @@ -4,6 +4,6 @@ aria_label = "Cascades Southbound" reference_date = "20240620" programmers_warning = "Oregon Point buses missing. Coast Starlight duplicate timetable problems." key_baggage = true -for_rpa = true + key_bus = true use_bus_icon_in_cells = true diff --git a/timetable_kit/specs_amtrak/city-of-new-orleans-illini-saluki.toml b/timetable_kit/specs_amtrak/city-of-new-orleans-illini-saluki.toml index b8b42edd..6d52a562 100644 --- a/timetable_kit/specs_amtrak/city-of-new-orleans-illini-saluki.toml +++ b/timetable_kit/specs_amtrak/city-of-new-orleans-illini-saluki.toml @@ -4,4 +4,4 @@ aria_label = "City of New Orleans Illini Saluki" reference_date = "20240620" bottom_text = "Note:
Amtrak only carries passengers between Homewood, IL and Chicago if they are connecting to another Amtrak train at Chicago.
Metra provides regular service between Homewood, IL and Chicago on the Metra Electric line." key_baggage = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/coast-starlight.toml b/timetable_kit/specs_amtrak/coast-starlight.toml index 19ea15f5..b45a7e98 100644 --- a/timetable_kit/specs_amtrak/coast-starlight.toml +++ b/timetable_kit/specs_amtrak/coast-starlight.toml @@ -9,4 +9,4 @@ use_bus_icon_in_cells = true key_baggage = true key_d = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/crescent.toml b/timetable_kit/specs_amtrak/crescent.toml index ba084844..91d8b128 100644 --- a/timetable_kit/specs_amtrak/crescent.toml +++ b/timetable_kit/specs_amtrak/crescent.toml @@ -7,4 +7,4 @@ key_baggage = true key_d = true key_r = true key_tz = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/downeaster-nb.toml b/timetable_kit/specs_amtrak/downeaster-nb.toml index 4943491c..3ce35e4c 100644 --- a/timetable_kit/specs_amtrak/downeaster-nb.toml +++ b/timetable_kit/specs_amtrak/downeaster-nb.toml @@ -3,4 +3,4 @@ heading = "Downeaster Northbound" aria_label = "Downeaster Northbound" landscape = true reference_date = "20240620" -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/downeaster-sb.toml b/timetable_kit/specs_amtrak/downeaster-sb.toml index 991b3aef..8c2aa309 100644 --- a/timetable_kit/specs_amtrak/downeaster-sb.toml +++ b/timetable_kit/specs_amtrak/downeaster-sb.toml @@ -3,4 +3,4 @@ heading = "Downeaster Southbound" aria_label = "Downeaster Southbound" landscape = true reference_date = "20240620" -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/eb-cxn-rochester.toml b/timetable_kit/specs_amtrak/eb-cxn-rochester.toml index 85f9ed93..c2b08d5d 100644 --- a/timetable_kit/specs_amtrak/eb-cxn-rochester.toml +++ b/timetable_kit/specs_amtrak/eb-cxn-rochester.toml @@ -3,4 +3,4 @@ heading = "Thruway Bus Connection: Rochester, MN" aria_label = "Rochester MN Bus" reference_date = "20240620" top_text = "Groome Transportation buses provide connections from Rochester to St. Paul-Minneapolis and points west on the Empire Builder, as well as additional frequencies between LaCrosse, WI; Winona, MN; and St. Paul-Minneapolis." -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/empire-builder-borealis.toml b/timetable_kit/specs_amtrak/empire-builder-borealis.toml index e1407ea4..300e884b 100644 --- a/timetable_kit/specs_amtrak/empire-builder-borealis.toml +++ b/timetable_kit/specs_amtrak/empire-builder-borealis.toml @@ -9,4 +9,4 @@ key_baggage = true key_d = true key_r = true key_tz = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/empire-service-weekday-eb.toml b/timetable_kit/specs_amtrak/empire-service-weekday-eb.toml index b13a07d9..c4968733 100644 --- a/timetable_kit/specs_amtrak/empire-service-weekday-eb.toml +++ b/timetable_kit/specs_amtrak/empire-service-weekday-eb.toml @@ -7,4 +7,4 @@ reference_date = "20240620" key_baggage = true key_d = true all_stations_accessible = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/empire-service-weekday-wb.toml b/timetable_kit/specs_amtrak/empire-service-weekday-wb.toml index 48d9c31d..77d44f22 100644 --- a/timetable_kit/specs_amtrak/empire-service-weekday-wb.toml +++ b/timetable_kit/specs_amtrak/empire-service-weekday-wb.toml @@ -6,4 +6,4 @@ landscape = true reference_date = "20240620" key_baggage = true all_stations_accessible = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/empire-service-weekend-eb.toml b/timetable_kit/specs_amtrak/empire-service-weekend-eb.toml index a2f4f482..035eb275 100644 --- a/timetable_kit/specs_amtrak/empire-service-weekend-eb.toml +++ b/timetable_kit/specs_amtrak/empire-service-weekend-eb.toml @@ -7,4 +7,4 @@ reference_date = "20240620" key_baggage = true key_d = true all_stations_accessible = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/empire-service-weekend-wb.toml b/timetable_kit/specs_amtrak/empire-service-weekend-wb.toml index 697d98b9..6cb56a8c 100644 --- a/timetable_kit/specs_amtrak/empire-service-weekend-wb.toml +++ b/timetable_kit/specs_amtrak/empire-service-weekend-wb.toml @@ -6,4 +6,4 @@ landscape = true reference_date = "20240620" key_baggage = true all_stations_accessible = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/ethan-allen-express.toml b/timetable_kit/specs_amtrak/ethan-allen-express.toml index e1ebf825..097a85bf 100644 --- a/timetable_kit/specs_amtrak/ethan-allen-express.toml +++ b/timetable_kit/specs_amtrak/ethan-allen-express.toml @@ -6,4 +6,4 @@ key_on_right = true key_bus = true use_bus_icon_in_cells = true landscape = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/grand-canyon.toml b/timetable_kit/specs_amtrak/grand-canyon.toml index bd6d1811..4a59e613 100644 --- a/timetable_kit/specs_amtrak/grand-canyon.toml +++ b/timetable_kit/specs_amtrak/grand-canyon.toml @@ -11,4 +11,4 @@ key_r = true key_d = true key_bus = true use_bus_icon_in_cells = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/heartland-flyer.toml b/timetable_kit/specs_amtrak/heartland-flyer.toml index d874517d..843548da 100644 --- a/timetable_kit/specs_amtrak/heartland-flyer.toml +++ b/timetable_kit/specs_amtrak/heartland-flyer.toml @@ -5,4 +5,4 @@ reference_date = "20240620" top_text = "The Heartland Flyer runs from Fort Worth to Oklahoma City.
It connects with both directions of the Texas Eagle at Fort Worth.
Amtrak Thruway Bus service operated by BeeLine Express connects from Oklahoma City to Wichita,
and to Newton, where it connects with both directions of the Southwest Chief. It will wait for late trains at Newton." key_bus = true use_bus_icon_in_cells = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/hiawatha-nb.toml b/timetable_kit/specs_amtrak/hiawatha-nb.toml index 95c87847..3fa681a2 100644 --- a/timetable_kit/specs_amtrak/hiawatha-nb.toml +++ b/timetable_kit/specs_amtrak/hiawatha-nb.toml @@ -4,5 +4,5 @@ aria_label = "Hiawatha Northbound" reference_date = "20240620" top_text = "Check AmtrakHiawatha.com for additional and connecting bus service and updated schedules." key_baggage = true -for_rpa = true + transposed = true diff --git a/timetable_kit/specs_amtrak/hiawatha-sb.toml b/timetable_kit/specs_amtrak/hiawatha-sb.toml index e7ca64e7..1ceb9d8c 100644 --- a/timetable_kit/specs_amtrak/hiawatha-sb.toml +++ b/timetable_kit/specs_amtrak/hiawatha-sb.toml @@ -4,5 +4,5 @@ aria_label = "Hiawatha Southbound" reference_date = "20240620" top_text = "Check AmtrakHiawatha.com for additional and connecting bus service and updated schedules." key_baggage = true -for_rpa = true + transposed = true diff --git a/timetable_kit/specs_amtrak/keystone-service-weekday-eb.toml b/timetable_kit/specs_amtrak/keystone-service-weekday-eb.toml index 23d85a89..56078577 100644 --- a/timetable_kit/specs_amtrak/keystone-service-weekday-eb.toml +++ b/timetable_kit/specs_amtrak/keystone-service-weekday-eb.toml @@ -8,4 +8,4 @@ key_baggage = true key_d = true key_r = true compress_credits = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/keystone-service-weekday-wb.toml b/timetable_kit/specs_amtrak/keystone-service-weekday-wb.toml index 9c10877c..8f169b36 100644 --- a/timetable_kit/specs_amtrak/keystone-service-weekday-wb.toml +++ b/timetable_kit/specs_amtrak/keystone-service-weekday-wb.toml @@ -8,4 +8,4 @@ key_baggage = true key_d = true key_r = true compress_credits = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/keystone-service-weekend-eb.toml b/timetable_kit/specs_amtrak/keystone-service-weekend-eb.toml index d615bdd1..44fb64c4 100644 --- a/timetable_kit/specs_amtrak/keystone-service-weekend-eb.toml +++ b/timetable_kit/specs_amtrak/keystone-service-weekend-eb.toml @@ -8,5 +8,5 @@ key_baggage = true key_d = true key_r = true compress_credits = true -for_rpa = true + key_on_right = true diff --git a/timetable_kit/specs_amtrak/keystone-service-weekend-wb.toml b/timetable_kit/specs_amtrak/keystone-service-weekend-wb.toml index 080ef9af..f2f35954 100644 --- a/timetable_kit/specs_amtrak/keystone-service-weekend-wb.toml +++ b/timetable_kit/specs_amtrak/keystone-service-weekend-wb.toml @@ -8,5 +8,5 @@ key_baggage = true key_d = true key_r = true compress_credits = true -for_rpa = true + key_on_right = true diff --git a/timetable_kit/specs_amtrak/lake-shore-limited.toml b/timetable_kit/specs_amtrak/lake-shore-limited.toml index fd2d7e25..78946f8b 100644 --- a/timetable_kit/specs_amtrak/lake-shore-limited.toml +++ b/timetable_kit/specs_amtrak/lake-shore-limited.toml @@ -7,4 +7,4 @@ key_baggage = true key_d = true key_r = true key_tz = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/lincoln-service-missouri-river-runner.toml b/timetable_kit/specs_amtrak/lincoln-service-missouri-river-runner.toml index 7dcc5320..4fd1bfcd 100644 --- a/timetable_kit/specs_amtrak/lincoln-service-missouri-river-runner.toml +++ b/timetable_kit/specs_amtrak/lincoln-service-missouri-river-runner.toml @@ -6,4 +6,4 @@ landscape = true key_baggage = true key_r = true key_d = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/lynchburg-roanoke-nb.toml b/timetable_kit/specs_amtrak/lynchburg-roanoke-nb.toml index 39814006..00a4fd83 100644 --- a/timetable_kit/specs_amtrak/lynchburg-roanoke-nb.toml +++ b/timetable_kit/specs_amtrak/lynchburg-roanoke-nb.toml @@ -6,4 +6,4 @@ reference_date = "20240620" key_baggage = true key_d = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/lynchburg-roanoke-sb.toml b/timetable_kit/specs_amtrak/lynchburg-roanoke-sb.toml index 5d7fdf0d..5149dce2 100644 --- a/timetable_kit/specs_amtrak/lynchburg-roanoke-sb.toml +++ b/timetable_kit/specs_amtrak/lynchburg-roanoke-sb.toml @@ -6,4 +6,4 @@ reference_date = "20240620" key_baggage = true key_d = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/nec-bos-was-saturday-nb.toml b/timetable_kit/specs_amtrak/nec-bos-was-saturday-nb.toml index df37bd2e..b87cf8d4 100644 --- a/timetable_kit/specs_amtrak/nec-bos-was-saturday-nb.toml +++ b/timetable_kit/specs_amtrak/nec-bos-was-saturday-nb.toml @@ -10,4 +10,4 @@ key_on_right = true key_baggage = true key_d = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/nec-bos-was-saturday-sb.toml b/timetable_kit/specs_amtrak/nec-bos-was-saturday-sb.toml index 75c8b321..65f2e8a9 100644 --- a/timetable_kit/specs_amtrak/nec-bos-was-saturday-sb.toml +++ b/timetable_kit/specs_amtrak/nec-bos-was-saturday-sb.toml @@ -10,5 +10,5 @@ key_on_right = true key_baggage = true key_d = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/nec-bos-was-sunday-nb.toml b/timetable_kit/specs_amtrak/nec-bos-was-sunday-nb.toml index 72730e25..ae30742b 100644 --- a/timetable_kit/specs_amtrak/nec-bos-was-sunday-nb.toml +++ b/timetable_kit/specs_amtrak/nec-bos-was-sunday-nb.toml @@ -10,4 +10,4 @@ key_on_right = true key_baggage = true key_d = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/nec-bos-was-sunday-sb.toml b/timetable_kit/specs_amtrak/nec-bos-was-sunday-sb.toml index 5cdd9d77..611fee61 100644 --- a/timetable_kit/specs_amtrak/nec-bos-was-sunday-sb.toml +++ b/timetable_kit/specs_amtrak/nec-bos-was-sunday-sb.toml @@ -10,5 +10,5 @@ key_on_right = true key_baggage = true key_d = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/nec-bos-was-weekday-nb.toml b/timetable_kit/specs_amtrak/nec-bos-was-weekday-nb.toml index fcbac0d9..9aac4ab9 100644 --- a/timetable_kit/specs_amtrak/nec-bos-was-weekday-nb.toml +++ b/timetable_kit/specs_amtrak/nec-bos-was-weekday-nb.toml @@ -10,4 +10,4 @@ key_on_right = true key_baggage = true key_d = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/nec-bos-was-weekday-sb.toml b/timetable_kit/specs_amtrak/nec-bos-was-weekday-sb.toml index ab32b72f..5fa8db91 100644 --- a/timetable_kit/specs_amtrak/nec-bos-was-weekday-sb.toml +++ b/timetable_kit/specs_amtrak/nec-bos-was-weekday-sb.toml @@ -10,5 +10,5 @@ key_on_right = true key_baggage = true key_d = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/pacific-surfliner-nb.toml b/timetable_kit/specs_amtrak/pacific-surfliner-nb.toml index 8bcf2a2f..d3650d5a 100644 --- a/timetable_kit/specs_amtrak/pacific-surfliner-nb.toml +++ b/timetable_kit/specs_amtrak/pacific-surfliner-nb.toml @@ -5,4 +5,4 @@ landscape = true reference_date = "20240620" key_baggage = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/pacific-surfliner-sb.toml b/timetable_kit/specs_amtrak/pacific-surfliner-sb.toml index 3299deab..c4c31610 100644 --- a/timetable_kit/specs_amtrak/pacific-surfliner-sb.toml +++ b/timetable_kit/specs_amtrak/pacific-surfliner-sb.toml @@ -5,4 +5,4 @@ landscape = true reference_date = "20240620" key_baggage = true key_d = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/pennsylvanian.toml b/timetable_kit/specs_amtrak/pennsylvanian.toml index d3ee6c72..4c43da19 100644 --- a/timetable_kit/specs_amtrak/pennsylvanian.toml +++ b/timetable_kit/specs_amtrak/pennsylvanian.toml @@ -6,4 +6,4 @@ bottom_text = "The eastbound Capitol Limited connection is unreliable due key_baggage = true key_d = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/pere-marquette.toml b/timetable_kit/specs_amtrak/pere-marquette.toml index ba49daba..066b30fa 100644 --- a/timetable_kit/specs_amtrak/pere-marquette.toml +++ b/timetable_kit/specs_amtrak/pere-marquette.toml @@ -4,4 +4,4 @@ aria_label = "Pere Marquette" reference_date = "20240620" key_tz = true key_on_right = false -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/quincy.toml b/timetable_kit/specs_amtrak/quincy.toml index 732f320b..91067faa 100644 --- a/timetable_kit/specs_amtrak/quincy.toml +++ b/timetable_kit/specs_amtrak/quincy.toml @@ -7,4 +7,4 @@ bottom_text = "Amtrak does not carry local passengers between Chicago, La key_baggage = true key_r = true key_d = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/richmond-saturday-nb.toml b/timetable_kit/specs_amtrak/richmond-saturday-nb.toml index 79ec4764..a700dcdd 100644 --- a/timetable_kit/specs_amtrak/richmond-saturday-nb.toml +++ b/timetable_kit/specs_amtrak/richmond-saturday-nb.toml @@ -6,4 +6,4 @@ reference_date = "20240620" key_baggage = true key_d = true compress_credits = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/richmond-sunday-nb.toml b/timetable_kit/specs_amtrak/richmond-sunday-nb.toml index 162e2cab..a0ce38ed 100644 --- a/timetable_kit/specs_amtrak/richmond-sunday-nb.toml +++ b/timetable_kit/specs_amtrak/richmond-sunday-nb.toml @@ -6,4 +6,4 @@ reference_date = "20240620" key_baggage = true key_d = true compress_credits = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/richmond-weekday-nb.toml b/timetable_kit/specs_amtrak/richmond-weekday-nb.toml index 9f4bbfe2..78b5d706 100644 --- a/timetable_kit/specs_amtrak/richmond-weekday-nb.toml +++ b/timetable_kit/specs_amtrak/richmond-weekday-nb.toml @@ -7,4 +7,4 @@ bottom_text = "Carolinian #80 allows pickups at some additional stops on the NEC key_baggage = true key_d = true compress_credits = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/richmond-weekday-sb.toml b/timetable_kit/specs_amtrak/richmond-weekday-sb.toml index eb564d71..bf25a2aa 100644 --- a/timetable_kit/specs_amtrak/richmond-weekday-sb.toml +++ b/timetable_kit/specs_amtrak/richmond-weekday-sb.toml @@ -6,4 +6,4 @@ reference_date = "20240620" key_baggage = true key_r = true compress_credits = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/richmond-weekend-sb.toml b/timetable_kit/specs_amtrak/richmond-weekend-sb.toml index f53262ba..11f891ad 100644 --- a/timetable_kit/specs_amtrak/richmond-weekend-sb.toml +++ b/timetable_kit/specs_amtrak/richmond-weekend-sb.toml @@ -6,4 +6,4 @@ reference_date = "20240620" key_baggage = true key_r = true compress_credits = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/san-joaquins-nb.toml b/timetable_kit/specs_amtrak/san-joaquins-nb.toml index abb1434f..c9491b9f 100644 --- a/timetable_kit/specs_amtrak/san-joaquins-nb.toml +++ b/timetable_kit/specs_amtrak/san-joaquins-nb.toml @@ -7,4 +7,4 @@ key_baggage = true key_r = true key_d = true key_l = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/san-joaquins-sb.toml b/timetable_kit/specs_amtrak/san-joaquins-sb.toml index 863aa792..a59c50e1 100644 --- a/timetable_kit/specs_amtrak/san-joaquins-sb.toml +++ b/timetable_kit/specs_amtrak/san-joaquins-sb.toml @@ -7,4 +7,4 @@ key_baggage = true key_r = true key_d = true key_l = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/silver-service-1.toml b/timetable_kit/specs_amtrak/silver-service-1.toml index 9c0b33c8..0834151e 100644 --- a/timetable_kit/specs_amtrak/silver-service-1.toml +++ b/timetable_kit/specs_amtrak/silver-service-1.toml @@ -10,4 +10,4 @@ key_on_right = false key_baggage = true key_d = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/silver-service-2.toml b/timetable_kit/specs_amtrak/silver-service-2.toml index 3127cce9..32f0a450 100644 --- a/timetable_kit/specs_amtrak/silver-service-2.toml +++ b/timetable_kit/specs_amtrak/silver-service-2.toml @@ -9,4 +9,4 @@ key_on_right = true key_baggage = true key_d = true key_r = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/southwest-chief.toml b/timetable_kit/specs_amtrak/southwest-chief.toml index ab3af197..67d7ced6 100644 --- a/timetable_kit/specs_amtrak/southwest-chief.toml +++ b/timetable_kit/specs_amtrak/southwest-chief.toml @@ -8,4 +8,4 @@ key_d = true key_r = true key_tz = true mountain_standard_time = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/sunset-limited.toml b/timetable_kit/specs_amtrak/sunset-limited.toml index 89e4ff5f..09100fd7 100644 --- a/timetable_kit/specs_amtrak/sunset-limited.toml +++ b/timetable_kit/specs_amtrak/sunset-limited.toml @@ -7,4 +7,4 @@ key_d = true key_r = true key_tz = true mountain_standard_time = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/texas-eagle.toml b/timetable_kit/specs_amtrak/texas-eagle.toml index 0d2475a9..234e64d8 100644 --- a/timetable_kit/specs_amtrak/texas-eagle.toml +++ b/timetable_kit/specs_amtrak/texas-eagle.toml @@ -10,4 +10,4 @@ key_d = true key_r = true key_tz = true mountain_standard_time = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/vermont-to-upstate-ny.toml b/timetable_kit/specs_amtrak/vermont-to-upstate-ny.toml index 3a401b71..dbfbfebe 100644 --- a/timetable_kit/specs_amtrak/vermont-to-upstate-ny.toml +++ b/timetable_kit/specs_amtrak/vermont-to-upstate-ny.toml @@ -5,4 +5,4 @@ aria_label = "Vermont to Upstate New York" reference_date = "20240620" train_numbers_side_by_side = true compress_credits = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/vermonter-valley-flyer-weekday.toml b/timetable_kit/specs_amtrak/vermonter-valley-flyer-weekday.toml index 33613cc4..0f5f1670 100644 --- a/timetable_kit/specs_amtrak/vermonter-valley-flyer-weekday.toml +++ b/timetable_kit/specs_amtrak/vermonter-valley-flyer-weekday.toml @@ -2,4 +2,4 @@ title = "Vermonter & Valley Flyer - Weekday" heading = "Vermonter & Valley Flyer - Weekday" aria_label = "Vermonter Valley Flyer Weekday" reference_date = "20240620" -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/vermonter-valley-flyer-weekend.toml b/timetable_kit/specs_amtrak/vermonter-valley-flyer-weekend.toml index fe6a8a6a..6ca19830 100644 --- a/timetable_kit/specs_amtrak/vermonter-valley-flyer-weekend.toml +++ b/timetable_kit/specs_amtrak/vermonter-valley-flyer-weekend.toml @@ -2,4 +2,4 @@ title = "Vermonter & Valley Flyer - Weekend" heading = "Vermonter & Valley Flyer - Weekend" aria_label = "Vermonter Valley Flyer Weekend" reference_date = "20240620" -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/vermonter.toml b/timetable_kit/specs_amtrak/vermonter.toml index 10a77a19..6a04ec7a 100644 --- a/timetable_kit/specs_amtrak/vermonter.toml +++ b/timetable_kit/specs_amtrak/vermonter.toml @@ -2,4 +2,4 @@ title = "Vermonter Timetable" heading = "Vermonter" aria_label = "Vermonter" reference_date = "20240620" -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/winter-park-express.toml b/timetable_kit/specs_amtrak/winter-park-express.toml index 9d16e078..0e3b0e28 100644 --- a/timetable_kit/specs_amtrak/winter-park-express.toml +++ b/timetable_kit/specs_amtrak/winter-park-express.toml @@ -2,5 +2,5 @@ title = "Winter Park Express Timetable" heading = "Winter Park Express" aria_label = "Winter Park Express" reference_date = "20240620" -for_rpa = true + top_text = "The Winter Park Express ski train will run each Friday, Saturday, and Sunday from January 12 to March 31 2024.
It will also run on Martin Luther King Jr. Day (Monday, January 15) and Presidents’ Day (Monday, February 19).
There will be 'extra' runs on Thursday, March 14 and Thursday, March 21." diff --git a/timetable_kit/specs_amtrak/wolverine-blue-water.toml b/timetable_kit/specs_amtrak/wolverine-blue-water.toml index b5f80f81..0a926740 100644 --- a/timetable_kit/specs_amtrak/wolverine-blue-water.toml +++ b/timetable_kit/specs_amtrak/wolverine-blue-water.toml @@ -5,4 +5,4 @@ top_text="NOTE: Effective May 6 until October 18, 2024 Trains 350 and 353 will o reference_date = "20240620" programmers_warning = "Wolverine trains have identical M-F and SaSu timetables but are listed separately, so days column is overridden: recheck this when GTFS changes. Also see Amtrak track construction notice https://www.amtrak.com/alert/wolverine-trains-350-353-temporarily-suspended.html - Trains 350 and 353 will operate on Fridays, Saturdays and Sundays only. Trains will operate normally on the following holidays: Memorial Day, Monday, May 27 Juneteenth, Wednesday, June 19 Independence Day, Thursday, July 4th Labor Day, Monday, September 2" key_tz = true -for_rpa = true + diff --git a/timetable_kit/specs_amtrak/wolverine-two-trips-error-test.toml b/timetable_kit/specs_amtrak/wolverine-two-trips-error-test.toml index ff23cf6b..8e4e9679 100644 --- a/timetable_kit/specs_amtrak/wolverine-two-trips-error-test.toml +++ b/timetable_kit/specs_amtrak/wolverine-two-trips-error-test.toml @@ -4,4 +4,4 @@ aria_label = "Wolverine Blue Water" reference_date = "20240620" programmers_warning = "This is used to make sure TwoTripsError will be correctly thrown when it should be. Wolverine trains have identical M-F and SaSu timetables but are listed separately, so are useful for this." key_tz = true -for_rpa = true + diff --git a/timetable_kit/specs_hartford/hartford-line-valley-flyer-saturday-nb.toml b/timetable_kit/specs_hartford/hartford-line-valley-flyer-saturday-nb.toml index 52355677..8d900f4a 100644 --- a/timetable_kit/specs_hartford/hartford-line-valley-flyer-saturday-nb.toml +++ b/timetable_kit/specs_hartford/hartford-line-valley-flyer-saturday-nb.toml @@ -7,4 +7,4 @@ top_text = "CTrail Hartford Line trains (#64XX) are not ticketed or op bottom_text = "CTrail tickets are valid for all CTRail trains, and for Amtrak trains from Springfield to New Haven except for the Vermonter and during holiday blackout periods on Northeast Regionals.
The CTrail Hartford Line website has full schedules for services where CTrail tickets are valid, including additional Hartford-New Haven buses." landscape = true all_stations_accessible = true -for_rpa = true + diff --git a/timetable_kit/specs_hartford/hartford-line-valley-flyer-saturday-sb.toml b/timetable_kit/specs_hartford/hartford-line-valley-flyer-saturday-sb.toml index b5987328..8298b357 100644 --- a/timetable_kit/specs_hartford/hartford-line-valley-flyer-saturday-sb.toml +++ b/timetable_kit/specs_hartford/hartford-line-valley-flyer-saturday-sb.toml @@ -8,4 +8,4 @@ bottom_text = "CTrail tickets are valid for all CTRail trains, and for Amtrak tr landscape = true key_d = true all_stations_accessible = true -for_rpa = true + diff --git a/timetable_kit/specs_hartford/hartford-line-valley-flyer-sunday-nb.toml b/timetable_kit/specs_hartford/hartford-line-valley-flyer-sunday-nb.toml index 1710e57e..cdd5082b 100644 --- a/timetable_kit/specs_hartford/hartford-line-valley-flyer-sunday-nb.toml +++ b/timetable_kit/specs_hartford/hartford-line-valley-flyer-sunday-nb.toml @@ -7,4 +7,4 @@ top_text = "CTrail Hartford Line trains (#64XX) are not ticketed or op bottom_text = "CTrail tickets are valid for all CTRail trains, and for Amtrak trains from Springfield to New Haven except for the Vermonter and during holiday blackout periods on Northeast Regionals.
The CTrail Hartford Line website has full schedules for services where CTrail tickets are valid, including additional Hartford-New Haven buses." landscape = true all_stations_accessible = true -for_rpa = true + diff --git a/timetable_kit/specs_hartford/hartford-line-valley-flyer-sunday-sb.toml b/timetable_kit/specs_hartford/hartford-line-valley-flyer-sunday-sb.toml index a1ce19ee..6e26a37b 100644 --- a/timetable_kit/specs_hartford/hartford-line-valley-flyer-sunday-sb.toml +++ b/timetable_kit/specs_hartford/hartford-line-valley-flyer-sunday-sb.toml @@ -8,4 +8,4 @@ bottom_text = "CTrail tickets are valid for all CTRail trains, and for Amtrak tr landscape = true key_d = true all_stations_accessible = true -for_rpa = true + diff --git a/timetable_kit/specs_hartford/hartford-line-valley-flyer-weekday-nb.toml b/timetable_kit/specs_hartford/hartford-line-valley-flyer-weekday-nb.toml index fa24c2f1..60c75547 100644 --- a/timetable_kit/specs_hartford/hartford-line-valley-flyer-weekday-nb.toml +++ b/timetable_kit/specs_hartford/hartford-line-valley-flyer-weekday-nb.toml @@ -7,4 +7,4 @@ top_text = "CTrail Hartford Line trains (#44XX) are not ticketed or op bottom_text = "CTrail tickets are valid for all CTRail trains, and for Amtrak trains from Springfield to New Haven except for the Vermonter and during holiday blackout periods on Northeast Regionals.
The CTrail Hartford Line website has full schedules for services where CTrail tickets are valid, including additional Hartford-New Haven buses.
NOTE: Some northbound CTrail Hartford Line trains are pick-up only (no drop-off) at New Haven State Street Station." landscape = true all_stations_accessible = true -for_rpa = true + diff --git a/timetable_kit/specs_hartford/hartford-line-valley-flyer-weekday-sb.toml b/timetable_kit/specs_hartford/hartford-line-valley-flyer-weekday-sb.toml index c0b13044..495a673a 100644 --- a/timetable_kit/specs_hartford/hartford-line-valley-flyer-weekday-sb.toml +++ b/timetable_kit/specs_hartford/hartford-line-valley-flyer-weekday-sb.toml @@ -7,4 +7,4 @@ top_text = "CTrail Hartford Line trains (#44XX) are not ticketed or op bottom_text = "CTrail tickets are valid for all CTRail trains, and for Amtrak trains from Springfield to New Haven except for the Vermonter and during holiday blackout periods on Northeast Regionals.
The CTrail Hartford Line website has full schedules for services where CTrail tickets are valid, including additional Hartford-New Haven buses.
NOTE: Some southbound CTrail Hartford Line trains are drop-off only (no pick-up) at New Haven State Street Station." landscape = true all_stations_accessible = true -for_rpa = true + diff --git a/timetable_kit/specs_maple_leaf/maple-leaf.toml b/timetable_kit/specs_maple_leaf/maple-leaf.toml index 513cc754..d657a48a 100644 --- a/timetable_kit/specs_maple_leaf/maple-leaf.toml +++ b/timetable_kit/specs_maple_leaf/maple-leaf.toml @@ -8,4 +8,4 @@ key_on_right = true key_baggage = true key_r = true key_d = true -for_rpa = true + diff --git a/timetable_kit/specs_via/canadian-1.toml b/timetable_kit/specs_via/canadian-1.toml index 30e5c91c..89057cfb 100644 --- a/timetable_kit/specs_via/canadian-1.toml +++ b/timetable_kit/specs_via/canadian-1.toml @@ -8,4 +8,4 @@ key_baggage = true key_f = true key_tz = true wheelchair_advance_notice = true -for_rpa = true + diff --git a/timetable_kit/specs_via/canadian-2.toml b/timetable_kit/specs_via/canadian-2.toml index d0fb3a7c..db70abca 100644 --- a/timetable_kit/specs_via/canadian-2.toml +++ b/timetable_kit/specs_via/canadian-2.toml @@ -9,4 +9,4 @@ key_f = true key_tz = true central_standard_time = true wheelchair_advance_notice = true -for_rpa = true + diff --git a/timetable_kit/specs_via/churchill-the-pas.toml b/timetable_kit/specs_via/churchill-the-pas.toml index e2703baa..c0c6bae7 100644 --- a/timetable_kit/specs_via/churchill-the-pas.toml +++ b/timetable_kit/specs_via/churchill-the-pas.toml @@ -9,4 +9,4 @@ key_f = true key_tz = true key_missing_times = true wheelchair_advance_notice = true -for_rpa = true + diff --git a/timetable_kit/specs_via/jasper-prince-rupert.toml b/timetable_kit/specs_via/jasper-prince-rupert.toml index c1dfa352..e9437ad8 100644 --- a/timetable_kit/specs_via/jasper-prince-rupert.toml +++ b/timetable_kit/specs_via/jasper-prince-rupert.toml @@ -8,4 +8,4 @@ key_baggage = true key_f = true key_tz = true wheelchair_advance_notice = true -for_rpa = true + diff --git a/timetable_kit/specs_via/kingston-sub-nb.csv b/timetable_kit/specs_via/kingston-sub-nb.csv index 96396455..061f1b19 100644 --- a/timetable_kit/specs_via/kingston-sub-nb.csv +++ b/timetable_kit/specs_via/kingston-sub-nb.csv @@ -4,7 +4,6 @@ omit,city pair (for checking against list_trains.py),,om,om,to,tm,to,tm,om,om,to omit,"calendar-days.txt #624, #34 suspended",,,,,,,,,,,,,,,,,,,,,,, route-name,,,,,,,,,,,,,,,,,,,,,,,, days,,,OTTW,OTTW,OTTW,TRTO,OTTW,TRTO,OTTW,OTTW,OTTW,TRTO,OTTW,OTTW,OTTW,OTTW,OTTW,TRTO,OTTW,OTTW,TRTO,OTTW,TRTO,OTTW -updown,,,,,,,,,,,,,,,,,,,,,,,, TRTO,,,blank,blank,first two-row,first two-row,first two-row,first two-row,blank,blank,first two-row,first two-row,blank,first,first,blank,first,first,first,first,first,first,first,first GUIL,,,blank,blank,,,,,blank,blank,,,blank,,,blank,,,,,,,, OSHA,,,blank,blank,,,,,blank,blank,,,blank,,,blank,,,,,,,, diff --git a/timetable_kit/specs_via/kingston-sub-nb.toml b/timetable_kit/specs_via/kingston-sub-nb.toml index 37b98838..3f34f21f 100644 --- a/timetable_kit/specs_via/kingston-sub-nb.toml +++ b/timetable_kit/specs_via/kingston-sub-nb.toml @@ -9,4 +9,4 @@ key_r = true key_d = true wheelchair_advance_notice = true montreal_airport_shuttle = true -for_rpa = true + diff --git a/timetable_kit/specs_via/kingston-sub-sb.csv b/timetable_kit/specs_via/kingston-sub-sb.csv index b649f9ee..36ff43df 100644 --- a/timetable_kit/specs_via/kingston-sub-sb.csv +++ b/timetable_kit/specs_via/kingston-sub-sb.csv @@ -4,7 +4,6 @@ omit,city pair (for checking against list_trains.py),,ot,ot,mo,mt,ot,ot,mt,ot,mo omit,"calendar-days.txt: #651, 655 suspended","calendar-days.txt: 647 suspended, also 635, 637, 639",,,,,,,,,,,,,,,,,,,,,,, route-name,,,,,,,,,,,,,,,,,,,,,,,,, days,,,OTTW,OTTW,OTTW,TRTO,OTTW,OTTW,TRTO,OTTW,OTTW,OTTW,TRTO,OTTW,OTTW,OTTW,TRTO,OTTW,OTTW,OTTW,TRTO,OTTW,OTTW,TRTO,OTTW -updown,,,,,,,,,,,,,,,,,,,,,,,,, origin,,,,,,,,,,,,,,,,,,,,,,,,, MTRL,,,blank,blank,first two-row,first two-row,blank,blank,first two-row,blank,,first two-row,first two-row,blank,blank,,first two-row,blank,blank,blank,first two-row,blank,,first two-row, DORV,,,blank,blank,,,blank,blank,,blank,,,,blank,blank,,,blank,blank,blank,,blank,,, diff --git a/timetable_kit/specs_via/kingston-sub-sb.toml b/timetable_kit/specs_via/kingston-sub-sb.toml index 993740ea..c6f7bf55 100644 --- a/timetable_kit/specs_via/kingston-sub-sb.toml +++ b/timetable_kit/specs_via/kingston-sub-sb.toml @@ -9,4 +9,4 @@ key_r = true key_d = true wheelchair_advance_notice = true montreal_airport_shuttle = true -for_rpa = true + diff --git a/timetable_kit/specs_via/montreal-jonquiere.toml b/timetable_kit/specs_via/montreal-jonquiere.toml index d9cb30c1..66b4dbe0 100644 --- a/timetable_kit/specs_via/montreal-jonquiere.toml +++ b/timetable_kit/specs_via/montreal-jonquiere.toml @@ -8,4 +8,4 @@ key_baggage = true key_f = true key_tz = true wheelchair_advance_notice = true -for_rpa = true + diff --git a/timetable_kit/specs_via/montreal-senneterre.toml b/timetable_kit/specs_via/montreal-senneterre.toml index 2ee176b1..2fc24a63 100644 --- a/timetable_kit/specs_via/montreal-senneterre.toml +++ b/timetable_kit/specs_via/montreal-senneterre.toml @@ -8,4 +8,4 @@ key_baggage = true key_f = true key_tz = true wheelchair_advance_notice = true -for_rpa = true + diff --git a/timetable_kit/specs_via/ocean.toml b/timetable_kit/specs_via/ocean.toml index 27c39450..a6897682 100644 --- a/timetable_kit/specs_via/ocean.toml +++ b/timetable_kit/specs_via/ocean.toml @@ -11,4 +11,4 @@ key_tz = true atlantic_time = true wheelchair_advance_notice = true note_regarding_wheelchair = "Drummondville & Saint-Hyacinthe." -for_rpa = true + diff --git a/timetable_kit/specs_via/quebec-city-nb.csv b/timetable_kit/specs_via/quebec-city-nb.csv index 789bf493..b5701686 100644 --- a/timetable_kit/specs_via/quebec-city-nb.csv +++ b/timetable_kit/specs_via/quebec-city-nb.csv @@ -3,7 +3,6 @@ column-options,,,ardp,ardp,,,,,,ardp omit,calendar_days.txt: #624 suspended,,,,,,,26 has extra stops on Friday only,, route-name,,,,,,,,,, days,,,MTRL,MTRL,MTRL,MTRL,MTRL,MTRL,MTRL,MTRL -updown,,,,,,,,,, OTTW,,,blank,first,blank,first,first,first,first,blank CSLM,,,blank,,blank,,,,,blank ALEX,,,blank,,blank,,,,,blank diff --git a/timetable_kit/specs_via/quebec-city-nb.toml b/timetable_kit/specs_via/quebec-city-nb.toml index a22a919a..73720bf5 100644 --- a/timetable_kit/specs_via/quebec-city-nb.toml +++ b/timetable_kit/specs_via/quebec-city-nb.toml @@ -8,4 +8,4 @@ key_r = true key_d = true wheelchair_advance_notice = true montreal_airport_shuttle = true -for_rpa = true + diff --git a/timetable_kit/specs_via/quebec-city-sb.csv b/timetable_kit/specs_via/quebec-city-sb.csv index 90e66f62..33289d8c 100644 --- a/timetable_kit/specs_via/quebec-city-sb.csv +++ b/timetable_kit/specs_via/quebec-city-sb.csv @@ -2,7 +2,6 @@ column-options,,,ardp,ardp,,,, route-name,,,,,,,, days,,,MTRL,MTRL,MTRL,MTRL,MTRL,MTRL -updown,,,,,,,, omit,"VIA official schedules confusingly show trains 51 and 633, which are Montreal-Ottawa-Fallowfield.",,,,,,, omit,calendar_days.txt: #25 & #637 are suspended,,,,,,, QBEC,,,first,from Halifax,first,first,first,first diff --git a/timetable_kit/specs_via/quebec-city-sb.toml b/timetable_kit/specs_via/quebec-city-sb.toml index 616c8b9d..e3fa3b00 100644 --- a/timetable_kit/specs_via/quebec-city-sb.toml +++ b/timetable_kit/specs_via/quebec-city-sb.toml @@ -8,4 +8,4 @@ key_r = true key_d = true wheelchair_advance_notice = true montreal_airport_shuttle = true -for_rpa = true + diff --git a/timetable_kit/specs_via/sudbury-white-river.toml b/timetable_kit/specs_via/sudbury-white-river.toml index b200d87b..b886946b 100644 --- a/timetable_kit/specs_via/sudbury-white-river.toml +++ b/timetable_kit/specs_via/sudbury-white-river.toml @@ -8,4 +8,4 @@ key_baggage = true key_f = true key_tz = true wheelchair_advance_notice = true -for_rpa = true + diff --git a/timetable_kit/specs_via/sw-ontario-nb.csv b/timetable_kit/specs_via/sw-ontario-nb.csv index 05ab699e..b67c9382 100644 --- a/timetable_kit/specs_via/sw-ontario-nb.csv +++ b/timetable_kit/specs_via/sw-ontario-nb.csv @@ -4,7 +4,6 @@ omit,"suspended: #80 (before #70), #88 (last train)",,,,,,,, omit,"When the Maple Leaf has a different weekend schedule, this is 698-63. But starting in early July 2023, it doesn't any more.",,,,,,,, route-name,,,,,,,,, days,,,LNDN,TRTO,TRTO,TRTO,TRTO,TRTO,TRTO -updown,,,,,,,,, omit,"VIA Rail's timing data for NY stations is WRONG as of July 2023, so don't use it!",,,,,,,, omit,NEWY,,blank,blank,blank,blank,blank,first,blank omit,YONK,,blank,blank,blank,blank,blank,,blank diff --git a/timetable_kit/specs_via/sw-ontario-nb.toml b/timetable_kit/specs_via/sw-ontario-nb.toml index 8e5952b9..55995a32 100644 --- a/timetable_kit/specs_via/sw-ontario-nb.toml +++ b/timetable_kit/specs_via/sw-ontario-nb.toml @@ -6,4 +6,4 @@ bottom_text = "The Maple Leaf is operated jointly by VIA Rail and dict[str, Any]: + return tomlkit.parse(get_style_toml(filename)) + + def __init__(self, style_name: str = "default") -> None: + # Load default style values first, then overwrite what changes in the specified style + default_config = self._read_toml("default") + selected_config = self._read_toml(style_name) + config = recurse_update_dict(default_config, selected_config) + + match config: # now read that + # I love using this to destructure things it's so convenient + case { + "time": { + "24h": using_24h, + "am": {"style": am_style, "string": am_string}, + "pm": {"style": pm_style, "string": pm_string}, + }, + "day": {"format": day_format, "week_start": week_start}, + "css": {"tag": special_css}, + "extra": {"for_rpa": is_for_rpa}, + }: + + self._using_24h: bool = using_24h + self._am_style: str = am_style + self._pm_style: str = pm_style + self._am_string: str = am_string + self._pm_string: str = pm_string + self._day_format: dict[str, str] = ( + {"all": day_format, "some": day_format} + if isinstance(day_format, str) + else day_format + ) + self._week_start: str = week_start + self._special_css_tag: str = special_css + self._is_for_rpa: bool = is_for_rpa + + case _: + err = "Style config not valid. " + unexpected_keys = set(config.keys()) - EXPECTED_KEYS + missing_keys = EXPECTED_KEYS - set(config.keys()) + + if unexpected_keys: + err += "\nUnexpected value(s): " + err += ", ".join(unexpected_keys) + + if missing_keys: + err += "\nMissing value(s): " + err += ", ".join(missing_keys) + + err += "\nFull config: \n\n" + err += json.dumps(config, indent=2) + err += "\n" + + raise StyleHandler.InvalidStyleError(err) + + def format_time_string( + self, + gtfs_timestr: str | int | TimeTuple, + *, + tz_difference: int = 0, + html: bool = False, + use_box_spans: bool = False, + ) -> str: + """ + Given a GTFS time, make a formatted time string. + """ + html = html or use_box_spans # if we want spans we need html first + + time_stuff = TimeTuple.from_gtfs_time_string(gtfs_timestr, tz_difference) + + # these are agnostic to whether you're using 12h or 24h time + # meridiem is just the m in am/pm - I couldn't think of another variable name lol + if time_stuff.pm: + meridiem_style = self._pm_style + meridiem_string = self._pm_string + else: + meridiem_style = self._am_style + meridiem_string = self._am_string + + string = "" + + if self._using_24h: + string += f"{time_stuff.hour24: >2}:{time_stuff.min:02}" + assert len(string) == 5 + + else: + hour = time_stuff.hour12 if time_stuff.hour12 != 0 else 12 + string += f"{hour: >2}:{time_stuff.min:0>2}" + assert len(string) == 5 + string += meridiem_string + + if html: + if use_box_spans: + + if self._using_24h: + string = span_enclose( + "box-time24", + span_enclose("box-digit", string[0]), + span_enclose("box-digit", string[1]), + span_enclose("box-colon", string[2]), + span_enclose("box-digit", string[3]), + span_enclose("box-digit", string[4]), + # will correctly add nothing if there are only 5 characters + string[5:], + ) + + else: + string = span_enclose( + "box-time12", + span_enclose("box-1", string[0]), + span_enclose("box-digit", string[1]), + span_enclose("box-colon", string[2]), + span_enclose("box-digit", string[3]), + span_enclose("box-digit", string[4]), + # will correctly add nothing if there are only 5 characters + span_enclose("box-ap", string[5:]), + ) + else: + string = span_enclose( + "box-time24" if self._using_24h else "box-time12", string + ) + + # html style tags like b, i, strong, etc separated by spaces + for html_style in meridiem_style.split(): + string = f"<{html_style}>{string}" + + return string + + def format_blank_time_string( + self, + html: bool = False, + ) -> str: + if html: + if self._using_24h: + return span_enclose("box-time24", "") + else: + return span_enclose("box-time12", "") + else: + return "" + + class DaystringHandler(Protocol): + def __call__( + self, + self_: StyleHandler, + days_of_service: dict[str, int], + *, + offset: int, + html: bool, + **_, + ) -> str: ... + + _DAYS_IN_WEEK = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + ] + + _DAY_CHARS_EN = "MTWTFSS" + _DAY_CHARS_FR = "LMMJVSD" + _DAY_CHARS_NUMS = "1234567" + + def _day_string_chars( + self, + days_of_service: dict[str, int], + *, + offset: int = 0, + html: bool = False, + locale_initials: str, + **_, + ) -> str: + """ + Presents days of service in the form of days' initials, starting from the day of choice. + Out of service days are given their own class for custom styling later, + such as crossing or greying out letters. This function can be given any seven-character + string for locale_initials to be adapted to whatever language one desires. + + For CSV-only presentation, out of service day initials are presented as an underscore. + """ + week_start = self._DAYS_IN_WEEK.index(self._week_start) + days_in_week = self._DAYS_IN_WEEK[week_start:] + self._DAYS_IN_WEEK[:week_start] + days_chars = locale_initials[week_start:] + locale_initials[:week_start] + days_of_service_ordered = [days_of_service[day] for day in days_in_week] + assert len(days_of_service_ordered) == len(days_chars), "Where did the time go?" + + string = "" + for i, char in enumerate(days_chars): + is_running_today = days_of_service_ordered[ + (i - offset) % len(days_of_service_ordered) + ] + if html: + html_class = "running" if is_running_today else "absent" + string += span_enclose(f"char-day-{html_class}", char) + else: + string += char if is_running_today else "_" + + return string + + _day_string_letters_en: DaystringHandler = partial( + _day_string_chars, locale_initials=_DAY_CHARS_EN + ) + _day_string_letters_fr: DaystringHandler = partial( + _day_string_chars, locale_initials=_DAY_CHARS_FR + ) + _day_string_numbers: DaystringHandler = partial( + _day_string_chars, locale_initials=_DAY_CHARS_NUMS + ) + + def _day_string_letters_ca_bilingual( + self, + days_of_service: dict[str, int], + *, + offset: int = 0, + html: bool = False, + **_, + ) -> str: + """ + Uses the English and French day initials together for extra maple syrup. + """ + return ( + self._day_string_letters_en(self, days_of_service, offset=offset, html=html) + + ("
" if html else "\n") + + self._day_string_letters_fr( + self, days_of_service, offset=offset, html=html + ) + ) + + # This dictionary of special cases for daystring is easier to read + # than hand-writing all the if-thens. + # The special cases are ones which don't need the "rotation trick" + # (or, in the case of SaSu, where we want to *avoid* it) + _day_string_special_cases: Final[dict[tuple[...], str]] = { + (1, 1, 1, 1, 1, 1, 1): "Daily", + # Missing only one day + (1, 1, 1, 1, 1, 1, 0): "Mo-Sa", + (0, 1, 1, 1, 1, 1, 1): "Tu-Su", + (1, 0, 1, 1, 1, 1, 1): "We-Mo", + (1, 1, 0, 1, 1, 1, 1): "Th-Tu", + (1, 1, 1, 0, 1, 1, 1): "Fr-We", + (1, 1, 1, 1, 0, 1, 1): "Sa-Th", + (1, 1, 1, 1, 1, 0, 1): "Su-Fr", + # Missing two consecutive days (including Mo-Fr) + (1, 1, 1, 1, 1, 0, 0): "Mo-Fr", + (0, 1, 1, 1, 1, 1, 0): "Tu-Sa", + (0, 0, 1, 1, 1, 1, 1): "We-Su", + (1, 0, 0, 1, 1, 1, 1): "Th-Mo", + (1, 1, 0, 0, 1, 1, 1): "Fr-Tu", + (1, 1, 1, 0, 0, 1, 1): "Sa-We", + (1, 1, 1, 1, 0, 0, 1): "Su-Th", + # Missing three consecutive days + (1, 1, 1, 1, 0, 0, 0): "Mo-Th", + (0, 1, 1, 1, 1, 0, 0): "Tu-Fr", + (0, 0, 1, 1, 1, 1, 0): "We-Sa", + (0, 0, 0, 1, 1, 1, 1): "Th-Su", + (1, 0, 0, 0, 1, 1, 1): "Fr-Mo", + (1, 1, 0, 0, 0, 1, 1): "Sa-Tu", + (1, 1, 1, 0, 0, 0, 1): "Su-We", + # Missing four consecutive days + (1, 1, 1, 0, 0, 0, 0): "Mo-We", + (0, 1, 1, 1, 0, 0, 0): "Tu-Th", + (0, 0, 1, 1, 1, 0, 0): "We-Fr", + (0, 0, 0, 1, 1, 1, 0): "Th-Sa", + (0, 0, 0, 0, 1, 1, 1): "Fr-Su", + (1, 0, 0, 0, 0, 1, 1): "Sa-Mo", + (1, 1, 0, 0, 0, 0, 1): "Su-Tu", + # Only running two consecutive days + # (including SaSu, which we need to avoid SuSa in -1 offset cases) + (1, 1, 0, 0, 0, 0, 0): "MoTu", + (0, 1, 1, 0, 0, 0, 0): "TuWe", + (0, 0, 1, 1, 0, 0, 0): "WeTh", + (0, 0, 0, 1, 1, 0, 0): "ThFr", + (0, 0, 0, 0, 1, 1, 0): "FrSa", + (0, 0, 0, 0, 0, 1, 1): "SaSu", + (1, 0, 0, 0, 0, 0, 1): "SuMo", + # Only running on one day a week + (1, 0, 0, 0, 0, 0, 0): "Mo", + (0, 1, 0, 0, 0, 0, 0): "Tu", + (0, 0, 1, 0, 0, 0, 0): "We", + (0, 0, 0, 1, 0, 0, 0): "Th", + (0, 0, 0, 0, 1, 0, 0): "Fr", + (0, 0, 0, 0, 0, 1, 0): "Sa", + (0, 0, 0, 0, 0, 0, 1): "Su", + } + + # The following function and above LUT is copied from the original monolithic implementation. + # It's quite verbose, but I see no better way for it to be implemented. - Borketh + def _day_string_neroden_hyphens_2l( + self, days_of_service: dict[str, int], *, offset: int = 0, **_ + ) -> str: + """ + Return "MoWeFr" style string for days of week. + + Given a calendar DataTable which contains only a single row for a single service, + this returns a string like "Daily" or "MoWeFr" for the serviced days of the week. + + Use offset to get the string for stops which are more than 24 hours after initial + departure. Beware of time zone changes! + + I have had more requests for tweaks to this format than anything else! + """ + + # Use modulo to correct the offset to the range 0:6 + # Note timezone differences can lead to -1 offset. + # Later stations on the route lead to positive offset. + offset %= 7 + + # OK. Fast encoding version here as a list of 1s and 0s. + days_of_service_vector = [days_of_service[day] for day in self._DAYS_IN_WEEK] + + # Do the offset rotation. + def rotate_right(vec: list, n: int) -> list: + return vec[-n:] + vec[:-n] + + days_of_service_vector = rotate_right(days_of_service_vector, offset) + + # Try the lookup-table path. + try: + daystring = self._day_string_special_cases[tuple(days_of_service_vector)] + return daystring + except KeyError: + pass + + # Lookup-table path failed. + # Now we have to do it the hard way, by just patching days of the week together. + # This probably means the days of non-operation are non-consecutive (MWF or whatever). + + # Now we get tricky. We want the days of the week to line up as they cycle around the clock. + # This is kind of messy! We always use the order of the original, zero-offset day. + # That's slightly wacky for the -1 offsets -- Su is first instead of Mo -- but that is OK. + daystring = "" + if days_of_service["monday"]: + daystring += ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"][offset] + if days_of_service["tuesday"]: + daystring += ["Tu", "We", "Th", "Fr", "Sa", "Su", "Mo"][offset] + if days_of_service["wednesday"]: + daystring += ["We", "Th", "Fr", "Sa", "Su", "Mo", "Tu"][offset] + if days_of_service["thursday"]: + daystring += ["Th", "Fr", "Sa", "Su", "Mo", "Tu", "We"][offset] + if days_of_service["friday"]: + daystring += ["Fr", "Sa", "Su", "Mo", "Tu", "We", "Th"][offset] + if days_of_service["saturday"]: + daystring += ["Sa", "Su", "Mo", "Tu", "We", "Th", "Fr"][offset] + if days_of_service["sunday"]: + daystring += ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"][offset] + + if daystring == "": + raise GTFSError("No days of operation?!?") + + # Generic case + return daystring + + _day_string_formats: dict[str, DaystringHandler] = { + "hyphen-2l": _day_string_neroden_hyphens_2l, + "numbers": _day_string_numbers, + "letters-en": _day_string_letters_en, + "letters-fr": _day_string_letters_fr, + "letters-CA": _day_string_letters_ca_bilingual, + } + + def format_day_string( + self, + days_of_service: dict[str, int] | Calendar, + offset: int = 0, + html: bool = False, + ) -> str: + # normalize the days_of_service from whatever we're given + if type(days_of_service) is not dict: + days_of_service: Calendar + days_of_service_list = days_of_service.to_dict("records") + # if there are zero or duplicate service records, we error out. + if len(days_of_service_list) == 0: + raise GTFSError("daystring() can't handle an empty calendar") + if len(days_of_service_list) >= 2: + raise GTFSError( + "daystring() can't handle two calendars for service_id: ", + days_of_service_list, + ) + days_of_service: dict[str, int] = days_of_service_list[0] + + every_day_string = self._day_format["all"] # For example, "Daily" + some_days_format = self._day_format["some"] + + if every_day_string != some_days_format and all(days_of_service.values()): + return every_day_string + + return self._day_string_formats[some_days_format]( + self, days_of_service, offset=offset, html=html + ) + + @property + def special_css_tag(self) -> str: + return self._special_css_tag + + @property + def for_rpa(self) -> bool: + return self._is_for_rpa diff --git a/timetable_kit/styles/default.toml b/timetable_kit/styles/default.toml new file mode 100644 index 00000000..6645c1f9 --- /dev/null +++ b/timetable_kit/styles/default.toml @@ -0,0 +1,15 @@ +# Nathanael's Amtrak-inspired format, the default for timetable_kit +[time] +24h = false +am = {style = "", string = "A"} +pm = {style = "b", string = "P"} + +[day] +format = {all = "Daily", some = "hyphen-2l"} +week_start = "monday" + +[css] +tag = "amtrak-special-css" + +[extra] +for_rpa = true \ No newline at end of file diff --git a/timetable_kit/styles/silly_example.toml b/timetable_kit/styles/silly_example.toml new file mode 100644 index 00000000..47d03846 --- /dev/null +++ b/timetable_kit/styles/silly_example.toml @@ -0,0 +1,9 @@ +# chaos. +[time] +24h = false +am = {style = "b i small", string = "in the morning"} +pm = {style = "s u sup", string = "in the afternoon"} + +[day] +format = "letters-CA" +week_start = "wednesday" \ No newline at end of file diff --git a/timetable_kit/styles/via-tac.toml b/timetable_kit/styles/via-tac.toml new file mode 100644 index 00000000..b7eea1c7 --- /dev/null +++ b/timetable_kit/styles/via-tac.toml @@ -0,0 +1,13 @@ +# Borketh's VIA-inspired style for use by Transport Action Canada +[time] +24h = true +pm = {style = ""} + +[day] +format = "numbers" + +[css] +tag = "via-special-css" + +[extra] +for_rpa = false \ No newline at end of file diff --git a/timetable_kit/templates/agency_via.css b/timetable_kit/templates/agency_via.css index e258e529..31b9693a 100644 --- a/timetable_kit/templates/agency_via.css +++ b/timetable_kit/templates/agency_via.css @@ -8,7 +8,8 @@ VIA Rail specific CSS. {# Vary the color of the h1 heading. #} h1.via-special-css { - background-color: crimson; + background-color: #ffcc00; + color: black } /* Put this before the printed heading, but not in index lists of headings */ h1.via-special-css::before { diff --git a/timetable_kit/templates/rpa_logo.html b/timetable_kit/templates/rpa_logo.html index 24c81269..6c275b13 100644 --- a/timetable_kit/templates/rpa_logo.html +++ b/timetable_kit/templates/rpa_logo.html @@ -1,8 +1,9 @@ {# rpa_logo.html Part of timetable_kit Copyright 2022 Nathanael Nerode. Licensed under GNU Affero GPL v.3 or later. -#}
{# -#}{# -#}{# -#}{# -#}
+#} + diff --git a/timetable_kit/templates/stylesheet.css b/timetable_kit/templates/stylesheet.css index 29012183..0198979a 100644 --- a/timetable_kit/templates/stylesheet.css +++ b/timetable_kit/templates/stylesheet.css @@ -21,6 +21,7 @@ {% include "agency_maple_leaf.css" %} {% include "timetable_main.css" %} {% include "timetable_colors.css" %} +{% include "timetable_via.css" %} {% include "for_user.css" %} {% include "time_boxes_characters.css" %}{# Used only if we're using a nontabular font. #} {% include "time_boxes_simple.css" %} diff --git a/timetable_kit/templates/table.html b/timetable_kit/templates/table.html index 7b5d2ccd..4d5a4a04 100644 --- a/timetable_kit/templates/table.html +++ b/timetable_kit/templates/table.html @@ -6,7 +6,7 @@ Used by styler.py. #} - +
{% for row_num in thead_row_nums %} {% include "table_row.html" %} diff --git a/timetable_kit/templates/time_boxes_extras.css b/timetable_kit/templates/time_boxes_extras.css index 5a5aec87..2138b58b 100644 --- a/timetable_kit/templates/time_boxes_extras.css +++ b/timetable_kit/templates/time_boxes_extras.css @@ -38,9 +38,9 @@ To satisfy screen readers, we don't want nested tables. This is the cleanest wa text-align: right; width: 1em; } -.box-time12, box-time24 { +.box-time12, .box-time24 { /* Padding between time and date or between time and right border */ - padding-right: 0.5mm; + padding-right: 1mm; } .box-days { /* MoWeFr, align left */ @@ -64,3 +64,12 @@ To satisfy screen readers, we don't want nested tables. This is the cleanest wa padding-right: 0.5mm; width: 2em; } + +.char-day-running { + font-weight: bold; +} + +.char-day-absent { + font-weight: lighter; + color: lightgrey; +} diff --git a/timetable_kit/templates/timetable_via.css b/timetable_kit/templates/timetable_via.css new file mode 100644 index 00000000..faa19aab --- /dev/null +++ b/timetable_kit/templates/timetable_via.css @@ -0,0 +1,57 @@ +/* + timetable_via.css Part of timetable_kit Copyright 2024 Borketh. + Licensed under GNU Affero GPLv3 or later. + This CSS fragment is to override the default Amtrak style in favour of a + take on older VIA timetables using their most recent style guide (as of 2024) +*/ + +.via-special-css th { + + background-color: #ffcc00; + padding: 2px 1px; + + /* only add lines where they should be */ + border-right: 0.5px solid black; + border-left: 0.5px solid black; + border-bottom: 1px solid black; + border-collapse: separate; +} + +.via-special-css td { + padding: 2px 1px; + + /* only add lines where they should be */ + border-right-style: solid; + border-right-width: 0.5px; + border-left-style: solid; + border-left-width: 0.5px; + border-top-style: hidden; + border-bottom-style: hidden; +} + +.via-special-css { + /* Stuff for the table as a whole */ + border-style: hidden; + vertical-align: center; +} + +/* Alternating colours, like the 2017 timetable, but using "Comfy White" from the style guide. +It makes it slightly easier to read without introducing contrast issues. */ +.via-special-css tr:nth-child(odd) > td { + background-color: #FDF7D9; +} + +.via-special-css tr:nth-child(even) > td { + background-color: #F5F0EA; +} + +.via-special-css .station-cell { + font-family: "Noto Sans", cursive; /* the cursive is for debugging */ + padding-left: 5px; + padding-right: 7px; + +} + +.via-special-css td.time-cell { + font-family: "Noto Sans Mono", cursive +} \ No newline at end of file diff --git a/timetable_kit/text_presentation.py b/timetable_kit/text_presentation.py index b55b0572..534351d1 100644 --- a/timetable_kit/text_presentation.py +++ b/timetable_kit/text_presentation.py @@ -18,16 +18,14 @@ get_bus_icon_html, get_accessible_icon_html, ) +from timetable_kit.styles import StyleHandler +from timetable_kit.utils import span_enclose from timetable_kit.tsn import train_spec_to_tsn # Time stuff from timetable_kit.time import ( TimeTuple, get_zonediff, - explode_timestr, - day_string, - time_short_str_24, - time_short_str_12, ) # Safe version of
@@ -94,7 +92,7 @@ def get_rd_str( is_arrival_line=False, is_departure_line=False, ): - """Return a single character (default " ") with receive-only / discharge- only + """Return a single character (default " ") with receive-only / discharge-only annotation. "R" = Receive-only "D" = Discharge-only "L" = May leave early (unimplemented) "F" = Flag stop "*" = Not a regular passenger stop " " = Anything else. @@ -136,7 +134,7 @@ def get_rd_str( # This seems to be the only way to identify the infamous "L", # which means that the train or bus is allowed to depart ahead of time # - # Obnoxiously, VIA Rail includes the column but it's blank, + # Obnoxiously, VIA Rail includes the column, but it's blank, # which really means "all 1". Handle this elsewhere! # print("timepoint column found") # Should not happen with Amtrak data if timepoint.timepoint == 0: # and it says times aren't exact @@ -229,13 +227,12 @@ def timepoint_str( stop_tz, agency_tz, reference_date, + style: StyleHandler, doing_html=False, box_time_characters=False, reverse=False, two_row=False, use_ar_dp_str=False, - bold_pm=True, - times_24h=False, use_daystring=False, long_days_box=False, short_days_box=False, @@ -267,6 +264,7 @@ def timepoint_str( -- stop_tz: timezone for the stop -- agency_tz: timezone for the agency -- reference_date: reference date for time zone conversion (strictly speaking, needed only for Arizona) + -- style: The style handler that assists in style-specific formatting Options are many: -- two_row: This timepoint gets both arrival and departure rows (default is just one row) -- use_ar_dp_str: Use "Ar " and "Dp " or leave space for them (use only where appropriate) @@ -275,8 +273,6 @@ def timepoint_str( -- box_time_characters: put each character in the time in an HTML box; default is False. For use with fonts which don't have tabular nums. A nasty hack; best to use a font which does have tabular nums. - -- times_24h: use 24-hour military time (default is 12 hour time with "A" and "P" suffix) - -- bold_pm: make PM times bold (even in 24-hour time; only with doing_html) -- use_daystring: append a "MoWeFr" or "Daily" string. Only used on infrequent services. -- long_days_box: Extra-long space for days, for SuMoTuWeTh five-day calendars. -- short_days_box: Extra-short space for days, for "Mo" one-day across-midnight trains. @@ -300,119 +296,66 @@ def timepoint_str( if not doing_html: box_time_characters = False - # Pick function for the actual time printing - if times_24h: - time_str_func = time_short_str_24 - else: - time_str_func = time_short_str_12 - zonediff = get_zonediff(stop_tz, agency_tz, reference_date) - # Fill the TimeTuple and prep string for actual departure time - if pd.isna(timepoint.departure_time): - # Stupid finicky stuff for stops with *no specific time* - # VIA Rail Winnipeg-Churchill has this - departure_time_str = "---" - is_pm = 0 - else: - departure = explode_timestr(timepoint.departure_time, zonediff) - departure_time_str = time_str_func( - departure, box_time_characters=box_time_characters - ) - is_pm = departure.pm - if doing_html: - if bold_pm and is_pm == 1: - departure_time_str = "".join(["", departure_time_str, ""]) - if times_24h: - departure_time_str = "".join( - ['', departure_time_str, ""] - ) - else: - departure_time_str = "".join( - ['', departure_time_str, ""] - ) - - # Fill the TimeTuple and prep string for actual time - if pd.isna(timepoint.arrival_time): - # Stupid finicky stuff for stops with *no specific time* - # VIA Rail Winnipeg-Churchill has this - arrival_time_str = "---" - is_pm = 0 - else: - arrival = explode_timestr(timepoint.arrival_time, zonediff) - arrival_time_str = time_str_func( - arrival, box_time_characters=box_time_characters - ) - is_pm = arrival.pm - if doing_html: - if bold_pm and is_pm == 1: - arrival_time_str = "".join(["", arrival_time_str, ""]) - if times_24h: - arrival_time_str = "".join( - ['', arrival_time_str, ""] - ) + def time_day_strings(time): + nonlocal zonediff, box_time_characters, doing_html, calendar + # Fill the TimeTuple and prep string for actual time + if pd.isna(time): + # Stupid finicky stuff for stops with *no specific time* + # VIA Rail Winnipeg-Churchill has this + return "---", "" else: - arrival_time_str = "".join( - ['', arrival_time_str, ""] + time_tup = TimeTuple.from_gtfs_time_string(time, zonediff) + time_string = style.format_time_string( + gtfs_timestr=time_tup, + tz_difference=zonediff, + html=doing_html, + use_box_spans=box_time_characters, ) - + if use_daystring: + # Note that daystring is VARIABLE LENGTH, and is the only variable-length field + # It must be last and the entire time field must be left-justified as a result + day_string = style.format_day_string( + calendar, time_tup.day, html=doing_html + ) + else: + day_string = "" + return time_string, day_string + + departure_time_str, departure_day_str = time_day_strings(timepoint.departure_time) + arrival_time_str, arrival_daystring = time_day_strings(timepoint.arrival_time) # Need this for lines just containing "Ar" or "Dp" - blank_rd_str = "" - blank_time_str = "" - if doing_html: - blank_rd_str = "".join(['', "", ""]) - if times_24h: - blank_time_str = "".join(['', "", ""]) - else: - blank_time_str = "".join(['', "", ""]) + blank_time_str = style.format_blank_time_string(html=doing_html) + blank_rd_str = span_enclose("box-rd") if doing_html else "" # Fill in the day strings, if we're using it - departure_daystring = "" - arrival_daystring = "" blank_daystring = "" if use_daystring: - if long_days_box: - days_box_class = "box-days-long" - elif short_days_box: - days_box_class = "box-days-short" - else: - days_box_class = "box-days" - # Note that daystring is VARIABLE LENGTH, and is the only variable-length field - # It must be last and the entire time field must be left-justified as a result - if pd.isna(timepoint.departure_time): - # No specified time - departure_daystring = "" - else: - departure_daystring = day_string(calendar, offset=departure.day) - if pd.isna(timepoint.arrival_time): - # No specified time - arrival_daystring = "" - else: - arrival_daystring = day_string(calendar, offset=arrival.day) + days_box_class = ( + "box-days-long" + if long_days_box + else "box-days-short" if short_days_box else "box-days" + ) + if doing_html: - departure_daystring = "".join( - ['', departure_daystring, ""] - ) - arrival_daystring = "".join( - ['', arrival_daystring, ""] - ) - blank_daystring = "".join( - ['', "", ""] - ) + departure_day_str = span_enclose(days_box_class, departure_day_str) + arrival_daystring = span_enclose(days_box_class, arrival_daystring) + blank_daystring = span_enclose(days_box_class) else: # Add a necessary spacer: CSS does it for us in HTML - departure_daystring = "".join([" ", departure_daystring]) - arrival_daystring = "".join([" ", arrival_daystring]) - blank_daystring = "" + departure_day_str = " " + departure_day_str + arrival_daystring = " " + arrival_daystring ar_str = "" # If we are not adding the padding at all -- unwise with two_row dp_str = "" # Again, if we are not adding the "Ar/Dp" at all + ardp_spacer = "" if use_ar_dp_str: if doing_html: # I'd like to make this read better in screen readers, # but there is no clean way to do it. FIXME. - ar_str = 'Ar' - dp_str = 'Dp' + ar_str = span_enclose("box-ardp", "Ar") + dp_str = span_enclose("box-ardp", "Dp") else: ar_str = "Ar " dp_str = "Dp " @@ -471,7 +414,7 @@ def timepoint_str( ar_dp_str, "" if no_rd else rd_str, arrival_time_str if discharge_only else departure_time_str, - arrival_daystring if discharge_only else departure_daystring, + arrival_daystring if discharge_only else departure_day_str, baggage_str, bus_str, ] @@ -601,7 +544,7 @@ def timepoint_str( dp_str, "" if no_rd else departure_rd_str, departure_time_str, - departure_daystring, + departure_day_str, departure_baggage_str, departure_bus_str, ] @@ -746,6 +689,7 @@ def get_station_column_header(doing_html=False): """ return "Station" + def get_mile_column_header(doing_html=False): """Return the header for a column of mileage integers. @@ -856,8 +800,7 @@ def style_updown(reverse: bool, doing_html=False) -> str: # Put arrows on right and left, with spaces text = " ".join([arrow, text, arrow]) - text = "".join(["", text, ""]) - return text + return f"{text}" def get_origin_destination_spacer(doing_html: bool) -> str: diff --git a/timetable_kit/time.py b/timetable_kit/time.py index 8fddeb5f..67363d2f 100644 --- a/timetable_kit/time.py +++ b/timetable_kit/time.py @@ -3,20 +3,19 @@ # Copyright 2021, 2022, 2023, 2024 Nathanael Nerode. Licensed under GNU Affero GPL v.3 or later. """Module for processing GTFS times and producing strings. """ - -from typing import NamedTuple # for TimeTuple +from __future__ import annotations from datetime import datetime, timedelta # for time zones +from functools import total_ordering +from typing import NamedTuple # for TimeTuple from zoneinfo import ZoneInfo # still for time zones -import pandas as pd # Used for DataFrame - # These are mine from timetable_kit.errors import GTFSError -from timetable_kit.debug import debug_print +from timetable_kit.utils import span_enclose -def gtfs_date_to_isoformat(gtfs_date: str) -> str: +def gtfs_date_to_isoformat(gtfs_date: str | int) -> str: """Given a GTFS date string, return an ISO format date string. This is a triviality: it converts 20220310 to 2022-03-10. @@ -80,275 +79,63 @@ def get_zone_str(zone_name, doing_html=False): wrap.""" letter = tz_letter_dict[zone_name] if doing_html: - return "".join(['', letter, ""]) + return span_enclose("box-tz", letter) else: return letter -# This dictionary of special cases for daystring is easier to read -# than hand-writing all the if-thens. -# The special cases are ones which don't need the "rotation trick" -# (or, in the case of SaSu, where we want to *avoid* it) -daystring_special_cases = { - (1, 1, 1, 1, 1, 1, 1): "Daily", - # Missing only one day - (1, 1, 1, 1, 1, 1, 0): "Mo-Sa", - (0, 1, 1, 1, 1, 1, 1): "Tu-Su", - (1, 0, 1, 1, 1, 1, 1): "We-Mo", - (1, 1, 0, 1, 1, 1, 1): "Th-Tu", - (1, 1, 1, 0, 1, 1, 1): "Fr-We", - (1, 1, 1, 1, 0, 1, 1): "Sa-Th", - (1, 1, 1, 1, 1, 0, 1): "Su-Fr", - # Missing two consecutive days (including Mo-Fr) - (1, 1, 1, 1, 1, 0, 0): "Mo-Fr", - (0, 1, 1, 1, 1, 1, 0): "Tu-Sa", - (0, 0, 1, 1, 1, 1, 1): "We-Su", - (1, 0, 0, 1, 1, 1, 1): "Th-Mo", - (1, 1, 0, 0, 1, 1, 1): "Fr-Tu", - (1, 1, 1, 0, 0, 1, 1): "Sa-We", - (1, 1, 1, 1, 0, 0, 1): "Su-Th", - # Missing three consecutive days - (1, 1, 1, 1, 0, 0, 0): "Mo-Th", - (0, 1, 1, 1, 1, 0, 0): "Tu-Fr", - (0, 0, 1, 1, 1, 1, 0): "We-Sa", - (0, 0, 0, 1, 1, 1, 1): "Th-Su", - (1, 0, 0, 0, 1, 1, 1): "Fr-Mo", - (1, 1, 0, 0, 0, 1, 1): "Sa-Tu", - (1, 1, 1, 0, 0, 0, 1): "Su-We", - # Missing four consecutive days - (1, 1, 1, 0, 0, 0, 0): "Mo-We", - (0, 1, 1, 1, 0, 0, 0): "Tu-Th", - (0, 0, 1, 1, 1, 0, 0): "We-Fr", - (0, 0, 0, 1, 1, 1, 0): "Th-Sa", - (0, 0, 0, 0, 1, 1, 1): "Fr-Su", - (1, 0, 0, 0, 0, 1, 1): "Sa-Mo", - (1, 1, 0, 0, 0, 0, 1): "Su-Tu", - # Only running two consecutive days - # (including SaSu, which we need to avoid SuSa in -1 offset cases) - (1, 1, 0, 0, 0, 0, 0): "MoTu", - (0, 1, 1, 0, 0, 0, 0): "TuWe", - (0, 0, 1, 1, 0, 0, 0): "WeTh", - (0, 0, 0, 1, 1, 0, 0): "ThFr", - (0, 0, 0, 0, 1, 1, 0): "FrSa", - (0, 0, 0, 0, 0, 1, 1): "SaSu", - (1, 0, 0, 0, 0, 0, 1): "SuMo", - # Only running on one day a week - (1, 0, 0, 0, 0, 0, 0): "Mo", - (0, 1, 0, 0, 0, 0, 0): "Tu", - (0, 0, 1, 0, 0, 0, 0): "We", - (0, 0, 0, 1, 0, 0, 0): "Th", - (0, 0, 0, 0, 1, 0, 0): "Fr", - (0, 0, 0, 0, 0, 1, 0): "Sa", - (0, 0, 0, 0, 0, 0, 1): "Su", -} - - -def day_string(calendar, offset: int = 0) -> str: - """Return "MoWeFr" style string for days of week. - - Given a calendar DataTable which contains only a single row for a single service, - this returns a string like "Daily" or "MoWeFr" for the serviced days of the week. - - Use offset to get the string for stops which are more than 24 hours after initial - depature. Beware of time zone changes! - - I have had more requests for tweaks to this format than anything else! - """ - days_of_service_list = calendar.to_dict("records") - # if there are zero or duplicate service records, we error out. - if len(days_of_service_list) == 0: - raise GTFSError("daystring() can't handle an empty calendar") - if len(days_of_service_list) >= 2: - raise GTFSError( - "daystring() can't handle two calendars for service_id: ", - days_of_service_list, - ) - days_of_service = days_of_service_list[0] - - # Use modulo to correct the offset to the range 0:6 - # Note timezone differences can lead to -1 offset. - # Later stations on the route lead to positive offset. - offset %= 7 - - # OK. Fast encoding version here as a list of 1s and 0s. - days_of_service_vector = [ - days_of_service["monday"], - days_of_service["tuesday"], - days_of_service["wednesday"], - days_of_service["thursday"], - days_of_service["friday"], - days_of_service["saturday"], - days_of_service["sunday"], - ] - - # Do the offset rotation. - def rotate_right(l, n): - return l[-n:] + l[:-n] - - days_of_service_vector = rotate_right(days_of_service_vector, offset) - - # Try the lookup-table path. - try: - daystring = daystring_special_cases[tuple(days_of_service_vector)] - return daystring - except KeyError: - pass - - # Lookup-table path failed. - # Now we have to do it the hard way, by just patching days of the week together. - # This probably means the days of non-operation are non-consecutive (MWF or whatever). - - # Now we get tricky. We want the days of the week to line up as they cycle around the clock. - # This is kind of messy! We always use the order of the original, zero-offset day. - # That's slightly wacky for the -1 offsets -- Su is first instead of Mo -- but that is OK. - daystring = "" - if days_of_service["monday"]: - daystring += ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"][offset] - if days_of_service["tuesday"]: - daystring += ["Tu", "We", "Th", "Fr", "Sa", "Su", "Mo"][offset] - if days_of_service["wednesday"]: - daystring += ["We", "Th", "Fr", "Sa", "Su", "Mo", "Tu"][offset] - if days_of_service["thursday"]: - daystring += ["Th", "Fr", "Sa", "Su", "Mo", "Tu", "We"][offset] - if days_of_service["friday"]: - daystring += ["Fr", "Sa", "Su", "Mo", "Tu", "We", "Th"][offset] - if days_of_service["saturday"]: - daystring += ["Sa", "Su", "Mo", "Tu", "We", "Th", "Fr"][offset] - if days_of_service["sunday"]: - daystring += ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"][offset] - - if daystring == "": - raise GTFSError("No days of operation?!?") - - # Generic case - return daystring - - # Timestr functions +@total_ordering class TimeTuple(NamedTuple): """Class with time broken into pieces for printing.""" day: int - pm: int - hour: int + pm: bool + hour12: int hour24: int min: int sec: int - -def explode_timestr(timestr: str, zonediff: int = 0) -> TimeTuple: - """Given a GTFS timestr, return a TimeTuple. - - TimeTuple is a namedtuple giving 'day', 'pm', 'hour' (12 hour), 'hour24' ,'min', - 'sec'. - - zonediff is the number of hours to adjust to convert to local time before exploding. - """ - try: - longhours, mins, secs = [int(x) for x in timestr.split(":")] - longhours += zonediff # this is the timezone adjustment - except Exception as exc: - # Winnipeg-Churchill timetable has NaNs -- don't let it get here! - raise GTFSError("Timestr didn't parse right", timestr) from exc - # Return all-zeroes to identify where it happened - # return TimeTuple(day=0,pm=0,hour=0,hour24=0,min=0,sec=0) - # Note: the following does the right thing for negative hours - # (which can be created by the timezone adjustment) - # It will give -1 days and positive hours24. - [days, hours24] = divmod(longhours, 24) - [pm, hours] = divmod(hours24, 12) - my_time = TimeTuple(day=days, pm=pm, hour=hours, hour24=hours24, min=mins, sec=secs) - # could do as dict, but seems cleaner this way - return my_time - - -def time_short_str_24(time: TimeTuple, box_time_characters=False) -> str: - """Given an exploded TimeTuple, give a short version of the time suitable for a - timetable. - - But do it in "military" format from 0:00 to 23:59. - doing_html: Box each character in a html span, to simulate tabular numbers with non-tabular fonts. - """ - # Note that this is very explicitly designed to be fixed width - time_text = [ - f"{time.hour24 : >2}", - ":", - f"{time.min :0>2}", - ] - time_str = "".join(time_text) # String suitable for plaintext - if box_time_characters: - # There are exactly five characters, by construction. 23:59 is largest. - html_time_str = "".join( - [ - '', - time_str[0], - "", - '', - time_str[1], - "", - '', - time_str[2], - "", - '', - time_str[3], - "", - '', - time_str[4], - "", - ] + @classmethod + def from_gtfs_time_string( + cls, timestr: str | TimeTuple, zonediff: int = 0 + ) -> TimeTuple: + """Given a GTFS timestr, return a TimeTuple. + + TimeTuple is a namedtuple giving 'day', 'pm', 'hour' (12 hour), 'hour24' ,'min', + 'sec'. + + zonediff is the number of hours to adjust to convert to local time before exploding. + """ + if isinstance(timestr, TimeTuple): + return timestr + try: + longhours, mins, secs = [int(x) for x in timestr.split(":")] + longhours += zonediff # this is the timezone adjustment + except Exception as exc: + # Winnipeg-Churchill timetable has NaNs -- don't let it get here! + raise GTFSError("Timestr didn't parse right", timestr) from exc + # Return all-zeroes to identify where it happened + # return TimeTuple(day=0,pm=0,hour=0,hour24=0,min=0,sec=0) + # Note: the following does the right thing for negative hours + # (which can be created by the timezone adjustment) + # It will give -1 days and positive hours24. + [days, hours24] = divmod(longhours, 24) + [pm, hours] = divmod(hours24, 12) + my_time = TimeTuple( + day=days, pm=bool(pm), hour12=hours, hour24=hours24, min=mins, sec=secs ) - time_str = html_time_str - return time_str + # could do as dict, but seems cleaner this way + return my_time + def __lt__(self, other: TimeTuple) -> bool: + return self.modulo24_str() < other.modulo24_str() -# Named constant useful for the next method: -ampm_str = ["A", "P"] # index into this to get the am or pm string + def __eq__(self, other: TimeTuple) -> bool: + return self.modulo24_str() == other.modulo24_str() - -def time_short_str_12(time: TimeTuple, box_time_characters=False) -> str: - """Given an exploded TimeTuple, give a short version of the time suitable for a - timetable. - - Do it with AM and PM. - doing_html: Box each character in a html span, to simulate tabular numbers with non-tabular fonts. - """ - # Note that this is very explicitly designed to be fixed width - hour = time.hour - if hour == 0: - hour = 12 - time_text = [ - f"{hour : >2}", - ":", - f"{time.min :0>2}", - ampm_str[time.pm], - ] - time_str = "".join(time_text) # String suitable for plaintext - if box_time_characters: - # There are exactly six characters, by construction. 12:59P is largest. - html_time_str = "".join( - [ - '', - time_str[0], - "", - '', - time_str[1], - "", - '', - time_str[2], - "", - '', - time_str[3], - "", - '', - time_str[4], - "", - '', - time_str[5], - "", - ] - ) - time_str = html_time_str - return time_str + def modulo24_str(self) -> str: + return f"{self.hour24: >2}:{self.min:0>2}:{self.sec:0>2}" def modulo24(raw_timestr: str) -> str: @@ -356,5 +143,5 @@ def modulo24(raw_timestr: str) -> str: Used to sort trains by departure time in list_trains.py. """ - time = explode_timestr(raw_timestr) - return f"{time.hour24: >2}:{time.min:0>2}:{time.sec:0>2}" + time = TimeTuple.from_gtfs_time_string(raw_timestr) + return time.modulo24_str() diff --git a/timetable_kit/timetable.py b/timetable_kit/timetable.py index e4457d33..d155f3b2 100755 --- a/timetable_kit/timetable.py +++ b/timetable_kit/timetable.py @@ -11,65 +11,60 @@ ######################### # Other people's packages -import sys # sys.exit(0), sys.exit(1), and sys.path import os # for os.getenv import os.path # for os.path abilities including os.path.isdir import shutil # To copy files -from pathlib import Path - +import sys # sys.exit(0), sys.exit(1), and sys.path from datetime import date, timedelta # for seeking a valid reference date +from pathlib import Path from weasyprint import HTML as weasyHTML # type: ignore # Tell MyPy this has no type stubs +# For copying into the final HTML folder +from timetable_kit import connecting_services +from timetable_kit import icons + ############ # My modules # This (runtime_config) stores critical data supplied at runtime such as the agency subpackage to use. from timetable_kit import runtime_config - -# My errors, for seeking a valid reference date -from timetable_kit.errors import NoTripError, TwoTripsError +from timetable_kit.convenience_types import HtmlAndCss +from timetable_kit.core import ( + TTSpec, + fill_tt_spec, +) #################################### # Specific functions from my modules # Note namespaces are separate for each file/module # Also note: python packaging is really sucky for direct script testing. from timetable_kit.debug import set_debug_level, debug_print -from timetable_kit.file_handling import read_list_file -from timetable_kit.convenience_types import HtmlAndCss +# My errors, for seeking a valid reference date +from timetable_kit.errors import NoTripError, TwoTripsError +from timetable_kit.file_handling import read_list_file +from timetable_kit.initialize import initialize_feed +from timetable_kit.page_layout import ( + produce_html_page, + produce_html_file, +) # We call these repeatedly, so give them shorthand names from timetable_kit.runtime_config import agency from timetable_kit.runtime_config import agency_singleton - -# The actual value of agency will be set up later, after reading the arguments -# It is unsafe to do it here! - -from timetable_kit.initialize import initialize_feed - +from timetable_kit.styles import StyleHandler from timetable_kit.timetable_argparse import make_tt_arg_parser -from timetable_kit.core import ( - TTSpec, - fill_tt_spec, -) from timetable_kit.timetable_class import ( Timetable, -) -from timetable_kit.page_layout import ( - produce_html_page, - produce_html_file, + TTConfig, ) -# For copying into the final HTML folder -from timetable_kit import connecting_services -from timetable_kit import icons - # Module-level globals for memoization _prepared_output_dirs = [] _prepared_output_dirs_for_rpa = [] -def copy_supporting_files_to_output_dir(output_dirname, for_rpa=False): +def copy_supporting_files_to_output_dir(output_dir: str | Path, for_rpa=False): """Copy supporting files (icons, fonts) to the output directory. Necessary for Weasyprint, and for the HTML to display right. @@ -77,7 +72,7 @@ def copy_supporting_files_to_output_dir(output_dirname, for_rpa=False): # Copy the image files to the destination directory. # Necessary for weasyprint to work right! - output_dir = Path(output_dirname) + output_dir = Path(output_dir) # Memoize. # We would like to save on copying by caching the fact that we've done this. @@ -151,23 +146,17 @@ def copy_supporting_files_to_output_dir(output_dirname, for_rpa=False): def search_date( - *, - input_dirname: str, - spec_file, - command_line_reference_date, - num_days, # Number of days to try - gtfs_filename, - patch_the_feed, + config: TTConfig, *, spec_file: str, num_days: int # Number of days to try ) -> None: """For a single spec file, seek a valid date.""" # Acquire the feed, enhance it, do generic patching. - master_feed = initialize_feed(gtfs=gtfs_filename, patch_the_feed=patch_the_feed) + master_feed = initialize_feed(config) # Load the tt-spec, both aux and csv # Also sets tt_id value in the aux - spec = TTSpec.from_files(spec_file, input_dir=input_dirname) + spec: TTSpec = TTSpec.from_files(spec_file, input_dir=config.input_dir) # Set reference date override -- does nothing if passed "None" - spec.set_reference_date(command_line_reference_date) + spec.set_reference_date(config.reference_date) # Use datetime to convert to a "date" type base_reference_date = date.fromisoformat(spec.aux["reference_date"]) @@ -183,21 +172,22 @@ def search_date( # We could but we want the whole NEC NB/Weekday TT to be valid over the same dates... # Find the date range on which the entire reduced feed is valid - (latest_start_date, earliest_end_date) = reduced_feed.get_valid_date_range() + date_range = reduced_feed.get_valid_date_range() debug_print( 1, - f"For {spec_file}: believed valid from {latest_start_date} to {earliest_end_date}", + f"For {spec_file}: believed valid from {date_range.latest_start_date} " + f"to {date_range.earliest_end_date}", ) - if not latest_start_date or not earliest_end_date: + if date_range.is_invalid(): debug_print(1, "Rejecting calendar with no validity") continue - if latest_start_date == earliest_end_date: + if date_range.is_one_day(): debug_print(1, "Rejecting one-day calendar") continue # Test run. - t_plaintext: Timetable = fill_tt_spec( - spec, today_feed=reduced_feed, doing_html=False + _t_plaintext: Timetable = fill_tt_spec( + spec, style=StyleHandler(), today_feed=reduced_feed, doing_html=False ) except NoTripError: debug_print(1, "No trip found: trying next date") @@ -213,58 +203,49 @@ def search_date( debug_print(1, "Exhausted all dates without finding good date.") -def produce_several_timetables( - list_file_list, - *, - gtfs_filename=None, - do_csv=False, - do_html=True, - do_pdf=True, - author=None, - command_line_reference_date=None, - input_dirname=None, - output_dirname=None, - patch_the_feed=True, -) -> None: +def produce_several_timetables(list_file_list, config: TTConfig) -> None: """Main program to run from other Python programs. Doesn't mess around with args or environment variables. Does not take a default gtfs filename. DOES take filenames and directory names. """ - if not author: + if not config.author: print("produce_several_timetables: author is mandatory!") sys.exit(1) - if not input_dirname: - print("produce_several_timetables: input_dirname is mandatory!") + if not config.input_dir: + print("produce_several_timetables: input_dir is mandatory!") sys.exit(1) - debug_print(1, "Using input_dir", input_dirname) + debug_print(1, "Using input_dir", config.input_dir) - if not output_dirname: - print("produce_several_timetables: output_dirname is mandatory!") + if not config.output_dir: + print("produce_several_timetables: output_dir is mandatory!") sys.exit(1) - debug_print(1, "Using output_dir", output_dirname) - output_dir = Path(output_dirname) + debug_print(1, "Using output_dir", config.output_dir) + output_dir = Path(config.output_dir) - if not gtfs_filename: + if not config.gtfs_filename: print("produce_several_timetables: gtfs_filename is mandatory!") sys.exit(1) + style = StyleHandler( + config.style_filename + ) # defaults to "default" on multiple levels, so doesn't need checking + # Doing PDF requires doing HTML first. - if do_pdf: - do_html = True + config.cascade_todos() # The following are rather finicky in their ordering: # Acquire the feed, enhance it, do generic patching. - master_feed = initialize_feed(gtfs=gtfs_filename, patch_the_feed=patch_the_feed) + master_feed = initialize_feed(config) # Loop over files specified at the command line for list_file in list_file_list: debug_print(1, "Producing timetable for", list_file) if list_file.endswith(".list"): - (title, *spec_files) = read_list_file(list_file, input_dir=input_dirname) + (title, *spec_files) = read_list_file(list_file, input_dir=config.input_dir) output_filename_base = list_file.removesuffix(".list") else: # Single file. Treat as if it were a list of one @@ -282,37 +263,37 @@ def produce_several_timetables( for spec_file in spec_files: # Load the tt-spec, both aux and csv # Also sets tt_id value in the aux - spec = TTSpec.from_files(spec_file, input_dir=input_dirname) + spec = TTSpec.from_files(spec_file, input_dir=config.input_dir) # Set reference date override -- does nothing if passed "None" - spec.set_reference_date(command_line_reference_date) + spec.set_reference_date(config.reference_date) # Filter feed by reference date and by the train numbers (trip_short_names) in the spec header reduced_feed = spec.filter_and_reduce_feed(master_feed) # Note that we don't re-reduce the feed for a max-columns split spec (like the NEC). # We could but we want the whole NEC NB/Weekday TT to be valid over the same dates... # Find the date range on which the entire reduced feed is valid - (latest_start_date, earliest_end_date) = reduced_feed.get_valid_date_range() + date_range = reduced_feed.get_valid_date_range() debug_print( 1, - f"For {spec_file}: believed valid from {latest_start_date} to {earliest_end_date}", + f"For {spec_file}: believed valid from {date_range.latest_start_date} to {date_range.earliest_end_date}", ) spec_filename_base = spec.aux["output_filename"] - if do_csv: + if config.do_csv: # CSV can only do one page at a time. Use the subspec name. # Also don't split big specs for CSV. The end-user can do that. t_plaintext: Timetable = fill_tt_spec( - spec, today_feed=reduced_feed, doing_html=False + spec, style=style, today_feed=reduced_feed, doing_html=False ) # Note that there is a real danger of overwriting the source file. # Avoid this by adding an extra suffix to the timetable name. path_for_csv = output_dir / Path(spec_filename_base + "-out.csv") t_plaintext.write_csv_file(path_for_csv) - if do_html: + if config.do_html: # Set true if true on any spec - for_rpa = for_rpa or bool(spec.aux.get("for_rpa")) + for_rpa = for_rpa or style.for_rpa # OK! This code is intended to simplify work on the NEC. if spec.aux.get("max_columns_per_page", 0): @@ -329,8 +310,9 @@ def produce_several_timetables( for subspec in split_specs: # Main timetable, same for HTML and PDF t: Timetable = fill_tt_spec( - subspec, today_feed=reduced_feed, doing_html=True + subspec, style=style, today_feed=reduced_feed, doing_html=True ) + t.set_style(style) # Render to HTML timetable_styled_html = t.render() debug_print(1, "HTML styled") @@ -340,15 +322,15 @@ def produce_several_timetables( new_page = produce_html_page( timetable_styled_html, spec=subspec, - author=author, - start_date=str(latest_start_date), - end_date=str(earliest_end_date), + config=config, + style=style, + date_range=date_range, ) page_list.append(new_page) # End loop over specs split out programmatically from a single .csv file # Still in loop over files within a list file - if do_html: + if config.do_html: # This is done if the list file was really just a single spec file # Fill these only if it wasn't filled by the list file # i.e. only in the single-spec-file case @@ -360,10 +342,13 @@ def produce_several_timetables( # Out of loop over files within a list file # Still in loop over files specified at the command line - if do_html: + if config.do_html: # Produce complete multi-page HTML file. timetable_finished_html = produce_html_file( - page_list, title=title, for_rpa=for_rpa + page_list, + title=title, + for_rpa=for_rpa, + agency_special_css=style.special_css_tag, ) path_for_html = output_dir / Path(output_filename_base + ".html") with open(path_for_html, "w") as outfile: @@ -374,7 +359,7 @@ def produce_several_timetables( # This is memoized, so it won't duplicate work if called repeatedly. copy_supporting_files_to_output_dir(output_dir, for_rpa) - if do_pdf: + if config.do_pdf: # Pick up already-created HTML, convert to PDF weasy_html_pathname = str(path_for_html) html_for_weasy = weasyHTML(filename=weasy_html_pathname) @@ -382,7 +367,7 @@ def produce_several_timetables( debug_print(1, "Writing PDF file...") html_for_weasy.write_pdf(path_for_weasy) debug_print(1, "Wrote PDF file", path_for_weasy) - debug_print(1, "Done producing timetable for", spec_file) + debug_print(1, "Done producing timetable for", list_file) # Out of loop over files specified at the command line @@ -429,7 +414,7 @@ def main(): spec_file_list = [*args.tt_spec_files, *args.positional_spec_files] - if spec_file_list == []: + if not spec_file_list: if must_get_gtfs: # It's OK to have no specs if we were just downloading GTFS. # In this case, just quit. @@ -440,19 +425,19 @@ def main(): my_arg_parser.print_usage() sys.exit(1) - input_dirname = ( + input_dir = ( args.input_dirname or runtime_config.agency_input_dir or os.getenv("TIMETABLE_KIT_INPUT_DIR") or "." ) - if not os.path.isdir(input_dirname): - print("Input dir", input_dirname, "does not exist. Aborting.") + if not os.path.isdir(input_dir): + print("Input dir", input_dir, "does not exist. Aborting.") sys.exit(1) - output_dirname = args.output_dirname or os.getenv("TIMETABLE_KIT_OUTPUT_DIR") or "." - if not os.path.isdir(output_dirname): - print("Output dir", output_dirname, "does not exist. Aborting.") + output_dir = args.output_dirname or os.getenv("TIMETABLE_KIT_OUTPUT_DIR") or "." + if not os.path.isdir(output_dir): + print("Output dir", output_dir, "does not exist. Aborting.") sys.exit(1) author = args.author or os.getenv("TIMETABLE_KIT_AUTHOR") or os.getenv("AUTHOR") @@ -460,10 +445,20 @@ def main(): print("--author is mandatory!") sys.exit(1) - # If nopatch, don't patch the feed. Otherwise, do patch it. - patch_the_feed = not args.nopatch - - command_line_reference_date = args.reference_date # Does not default, may be None + config = TTConfig( + do_csv=args.do_csv, + do_html=args.do_html, + do_pdf=args.do_pdf, + author=author, + agency=agency_singleton(), + sponsor="RPA", # TODO this should be configurable through cmdline, and have effects + gtfs_filename=gtfs_filename, + style_filename=args.style_filename, + input_dir=input_dir, + output_dir=output_dir, + patch_the_feed=not args.nopatch, # If nopatch, don't patch the feed. Otherwise, do patch it. + reference_date=args.reference_date, + ) # Special case for --search argument if args.search is not None: @@ -474,28 +469,14 @@ def main(): # Ideally do this as part of regular processing. FIXME # But I didn't want to break anything search_date( - input_dirname=input_dirname, + config, spec_file=spec_file, - command_line_reference_date=command_line_reference_date, num_days=num_days, - gtfs_filename=gtfs_filename, - patch_the_feed=patch_the_feed, ) # Bail out early return - produce_several_timetables( - list_file_list=spec_file_list, - gtfs_filename=gtfs_filename, - do_csv=args.do_csv, - do_html=args.do_html, - do_pdf=args.do_pdf, - author=author, - command_line_reference_date=command_line_reference_date, - input_dirname=input_dirname, - output_dirname=output_dirname, - patch_the_feed=patch_the_feed, - ) + produce_several_timetables(list_file_list=spec_file_list, config=config) ########################## diff --git a/timetable_kit/timetable_argparse.py b/timetable_kit/timetable_argparse.py index 2f34a263..fbd2ee2e 100644 --- a/timetable_kit/timetable_argparse.py +++ b/timetable_kit/timetable_argparse.py @@ -87,7 +87,6 @@ def add_search_argument(parser: argparse.ArgumentParser): ) - def add_day_argument(parser: argparse.ArgumentParser): """Add the --day argument to a parser.""" parser.add_argument( @@ -176,6 +175,17 @@ def add_positional_spec_files_argument(parser: argparse.ArgumentParser): ) +def add_style_argument(parser: argparse.ArgumentParser): + parser.add_argument( + "--style", + help=""" + The filename of the style toml. Can be just the filename (like "via") - it will be found automagically. + """, + dest="style_filename", + default="default", + ) + + def make_tt_arg_parser(): """Make argument parser for timetable.py.""" parser = argparse.ArgumentParser( @@ -188,6 +198,7 @@ def make_tt_arg_parser(): add_agency_argument(parser) add_gtfs_argument(parser) add_get_gtfs_argument(parser) + add_style_argument(parser) add_date_argument(parser) add_debug_argument(parser) diff --git a/timetable_kit/timetable_class.py b/timetable_kit/timetable_class.py index 8bcd687d..a9906468 100644 --- a/timetable_kit/timetable_class.py +++ b/timetable_kit/timetable_class.py @@ -6,20 +6,22 @@ Created when PANDAS's Styler wasn't doing what I wanted. """ +import html # for html.escape import os # for os.PathLike +from dataclasses import dataclass from functools import cache # for memoization -import html # for html.escape +from pathlib import Path import pandas as pd -from pandas import DataFrame - from jinja2 import Template # for typehints # My packages from timetable_kit.debug import debug_print +from timetable_kit.generic_agency import Agency # For the Jinja templates from timetable_kit.load_resources import template_environment +from timetable_kit.styles import StyleHandler class Timetable: @@ -91,6 +93,12 @@ def __init__(self, spec) -> None: f'id="{table_id}" class="tt-table" aria-label="{aria_label} Timetable"' ) + def set_style(self, style: StyleHandler): + # this is stupid, FIXME + self.table_attributes = self.table_attributes.replace( + 'class="', f'class="{style.special_css_tag} ' + ) + def write_csv_file(self, file: os.PathLike | str) -> None: """Write this out as a CSV file at the given path. @@ -133,3 +141,29 @@ def render(self) -> str: } output = self.get_table_tpl().render(params) return output + + +@dataclass +class TTConfig: + + author: str + agency: Agency + sponsor: str # for later + + gtfs_filename: str | Path + style_filename: str | Path + input_dir: str | Path + output_dir: str | Path + reference_date: str + + do_csv: bool + do_html: bool + do_pdf: bool + patch_the_feed: bool = True + + def cascade_todos(self): + """ + Post-init function to make sure prerequisites happen + """ + self.do_html |= self.do_pdf + self.do_csv |= self.do_html diff --git a/timetable_kit/tsn.py b/timetable_kit/tsn.py index 816ad28c..5e2e91ec 100644 --- a/timetable_kit/tsn.py +++ b/timetable_kit/tsn.py @@ -12,15 +12,15 @@ Also contains other routines which look up trips by tsn. """ -from timetable_kit.errors import ( NoTripError, TwoTripsError ) from timetable_kit.debug import debug_print -from timetable_kit.runtime_config import agency -from timetable_kit.runtime_config import agency_singleton - -# import gtfs_kit +from timetable_kit.errors import NoTripError, TwoTripsError # List of days which are GTFS column headers from timetable_kit.feed_enhanced import GTFS_DAYS, FeedEnhanced +from timetable_kit.runtime_config import agency_singleton + + +# import gtfs_kit def train_spec_to_tsn(train_spec: str) -> str: diff --git a/timetable_kit/utils.py b/timetable_kit/utils.py new file mode 100644 index 00000000..44429e5e --- /dev/null +++ b/timetable_kit/utils.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from timetable_kit.debug import debug_print + + +def span(class_name: str = "") -> str: + """Returns a span tag with the class specified or a closing tag if blank""" + return f'' if class_name else "" + + +def span_enclose(span_class: str = "", *text: str) -> str: + """Returns the text provided surrounded in a span tag with the specified tag. + If multiple pieces of text are provided, they are stitched together before being surrounded in the tag. + If no text is provided, the empty span tag is returned. + If no class is provided, the stitched together text is returned. + If you call this with no arguments, you will get a blank string. What are you doing? + """ + if span_class: + return f"{span(span_class)}{''.join(text)}{span()}" + else: + return "".join(text) + + +def recurse_update_dict(default: dict, update: dict) -> dict: + for key, value in default.items(): + if isinstance(value, dict): + if key in update: + update_val = update[key] + if isinstance(update_val, dict): + recurse_update_dict(value, update_val) + else: + default[key] = update_val + else: + default[key] = update.get(key, value) + return default + + +def test(): + assert span() == "" + assert span("classy") == '' + assert span_enclose("classy") == '' + assert span_enclose("classy", "hi there") == 'hi there' + assert ( + span_enclose("classy", "hi there", " it's testing time") + == 'hi there it\'s testing time' + ) + assert span_enclose( + "both", + span_enclose("first", "stuff"), + span_enclose("second", "stuff"), + ) == ( + '' + 'stuff' + 'stuff' + "" + ) + + +if __name__ == "__main__": + test() + debug_print(0, "Tests passed!") diff --git a/timetable_kit/via/agency.py b/timetable_kit/via/agency.py index 55b07d82..71b542e7 100644 --- a/timetable_kit/via/agency.py +++ b/timetable_kit/via/agency.py @@ -7,29 +7,25 @@ """ from typing import Tuple -from timetable_kit.feed_enhanced import FeedEnhanced -from timetable_kit.generic_agency import Agency - # For generic reassembly functions import timetable_kit.text_assembly as text_assembly -from timetable_kit.text_assembly import SAFE_BR # for patch_feed import timetable_kit.via.gtfs_patches as gtfs_patches +# For get_route_name +import timetable_kit.via.route_names as route_names + # For checked baggage, sleeper trains, major stations list import timetable_kit.via.special_data as special_data - -# All the rest are for get_station_name_pretty -from timetable_kit.debug import set_debug_level, debug_print +from timetable_kit.feed_enhanced import FeedEnhanced +from timetable_kit.generic_agency import Agency +from timetable_kit.text_assembly import SAFE_BR # Map from station codes to connecting service names # This is stashed in a class variable from timetable_kit.via.connecting_services_data import connecting_services_dict -# For get_route_name -import timetable_kit.via.route_names as route_names - # For getting the province for a given stop code from timetable_kit.via.province_data import stop_code_to_province @@ -86,12 +82,8 @@ def add_via_disclaimer(self, doing_html=True) -> bool: """ return True - def agency_css_class(self) -> str: - """Name of a CSS class for agency-specific styling.""" - return "via-special-css" - def get_route_name(self, today_feed: FeedEnhanced, route_id: str) -> str: - """Given today_feed and a route_id, produce a suitalbe name for a column + """Given today_feed and a route_id, produce a suitable name for a column subheading. The implementation is VIA-specific. @@ -190,11 +182,10 @@ def get_station_name_from( ) -> str: """Get a phrase like "from Toronto" for the station.""" # Special case Quebec City -- this is very long - if doing_html == True and station_code == "QBEC": + if doing_html is True and station_code == "QBEC": return "from" + SAFE_BR + "Québec" + SAFE_BR + "City" # Otherwise revert to default implementation - return super.get_station_name_from( - self, + return super().get_station_name_from( station_code, doing_multiline_text=doing_multiline_text, doing_html=doing_html, @@ -205,11 +196,10 @@ def get_station_name_to( ) -> str: """Get a phrase like "to Toronto" for the station.""" # Special case Quebec City -- it's long - if doing_html == True and station_code == "QBEC": + if doing_html is True and station_code == "QBEC": return "to Québec" + SAFE_BR + "City" # Otherwise revert to default implementation - return super.get_station_name_to( - self, + return super().get_station_name_to( station_code, doing_multiline_text=doing_multiline_text, doing_html=doing_html, diff --git a/timetable_kit/via/get_gtfs.py b/timetable_kit/via/get_gtfs.py index 92d0cead..3c613a84 100755 --- a/timetable_kit/via/get_gtfs.py +++ b/timetable_kit/via/get_gtfs.py @@ -21,6 +21,10 @@ def get_gtfs_files(): return _gtfs_files +def main(): + get_gtfs_files().download_and_save() + + # MAIN PROGRAM if __name__ == "__main__": - get_gtfs_files().download_and_save() + main()