From 3dc54d27665c6e161fb610d59bf0bd465da7e39e Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Thu, 11 Apr 2024 00:22:03 +0200 Subject: [PATCH 1/4] Implement basic GTFS parser - Reader that binds to ZIP file or directory. - Builder that checks schedule consistency. --- .github/workflows/maven-ci.yml | 18 ++-- .../ch/naviqore/gtfs/schedule/Agency.java | 4 + .../ch/naviqore/gtfs/schedule/Calendar.java | 8 ++ .../naviqore/gtfs/schedule/CalendarDate.java | 6 ++ .../naviqore/gtfs/schedule/ExceptionType.java | 28 ++++++ .../naviqore/gtfs/schedule/GtfsSchedule.java | 16 +++ .../gtfs/schedule/GtfsScheduleBuilder.java | 99 +++++++++++++++++++ .../gtfs/schedule/GtfsScheduleReader.java | 53 +++++++++- .../java/ch/naviqore/gtfs/schedule/Route.java | 4 + .../ch/naviqore/gtfs/schedule/RouteType.java | 36 +++++++ .../java/ch/naviqore/gtfs/schedule/Stop.java | 4 + .../java/ch/naviqore/gtfs/schedule/Trip.java | 4 + .../gtfs/schedule/GtfsScheduleReaderIT.java | 13 +-- 13 files changed, 271 insertions(+), 22 deletions(-) create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/Agency.java create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/Calendar.java create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/CalendarDate.java create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/ExceptionType.java create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/GtfsSchedule.java create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleBuilder.java create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/Route.java create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/RouteType.java create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/Stop.java create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/Trip.java diff --git a/.github/workflows/maven-ci.yml b/.github/workflows/maven-ci.yml index 11dcdd50..86a37869 100644 --- a/.github/workflows/maven-ci.yml +++ b/.github/workflows/maven-ci.yml @@ -20,15 +20,15 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up JDK 21 - uses: actions/setup-java@v3 - with: - java-version: '21' - distribution: 'temurin' - cache: maven - - name: Build with Maven - run: mvn -B package --file pom.xml + - uses: actions/checkout@v3 + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B verify --file pom.xml # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive # - name: Update dependency graph diff --git a/src/main/java/ch/naviqore/gtfs/schedule/Agency.java b/src/main/java/ch/naviqore/gtfs/schedule/Agency.java new file mode 100644 index 00000000..f2b376e0 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/Agency.java @@ -0,0 +1,4 @@ +package ch.naviqore.gtfs.schedule; + +public record Agency(String agency, String name, String url, String timezone) { +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/Calendar.java b/src/main/java/ch/naviqore/gtfs/schedule/Calendar.java new file mode 100644 index 00000000..aaf5cf07 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/Calendar.java @@ -0,0 +1,8 @@ +package ch.naviqore.gtfs.schedule; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.EnumSet; + +public record Calendar(String id, EnumSet serviceDays, LocalDate startDate, LocalDate endDate) { +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/CalendarDate.java b/src/main/java/ch/naviqore/gtfs/schedule/CalendarDate.java new file mode 100644 index 00000000..78c9f9f9 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/CalendarDate.java @@ -0,0 +1,6 @@ +package ch.naviqore.gtfs.schedule; + +import java.time.LocalDate; + +public record CalendarDate(Calendar calendar, LocalDate date, ExceptionType type) { +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/ExceptionType.java b/src/main/java/ch/naviqore/gtfs/schedule/ExceptionType.java new file mode 100644 index 00000000..1f6c8ec6 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/ExceptionType.java @@ -0,0 +1,28 @@ +package ch.naviqore.gtfs.schedule; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public enum ExceptionType { + ADDED(1, "Service has been added for the specified date."), + REMOVED(2, "Service has been removed for the specified date."); + + private final int value; + private final String description; + + public static ExceptionType parse(String value) { + return parse(Integer.parseInt(value)); + } + + public static ExceptionType parse(int value) { + for (ExceptionType type : ExceptionType.values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("No exception type with value " + value + " found"); + } +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsSchedule.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsSchedule.java new file mode 100644 index 00000000..4473613b --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsSchedule.java @@ -0,0 +1,16 @@ +package ch.naviqore.gtfs.schedule; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public class GtfsSchedule { + + private final Map agencies; + + public Agency getAgency(String id) { + return agencies.get(id); + } +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleBuilder.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleBuilder.java new file mode 100644 index 00000000..8c151bf3 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleBuilder.java @@ -0,0 +1,99 @@ +package ch.naviqore.gtfs.schedule; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.log4j.Log4j2; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Log4j2 +public class GtfsScheduleBuilder { + + private final Map agencies = new HashMap<>(); + private final Map stops = new HashMap<>(); + private final Map routes = new HashMap<>(); + private final Map calendars = new HashMap<>(); + private final Map trips = new HashMap<>(); + + public static GtfsScheduleBuilder builder() { + return new GtfsScheduleBuilder(); + } + + public GtfsScheduleBuilder addAgency(String id, String name, String url, String timezone) { + if (agencies.containsKey(id)) { + throw new IllegalArgumentException("Agency " + id + " already exists"); + } + log.debug("Adding agency {}", id); + agencies.put(id, new Agency(id, name, url, timezone)); + return this; + } + + public GtfsScheduleBuilder addStop(String id, String name, double lat, double lon) { + if (stops.containsKey(id)) { + throw new IllegalArgumentException("Agency " + id + " already exists"); + } + log.debug("Adding stop {}", id); + stops.put(id, new Stop(id, name, lat, lon)); + return this; + } + + public GtfsScheduleBuilder addRoute(String id, String agencyId, String shortName, String longName, RouteType type) { + if (routes.containsKey(id)) { + throw new IllegalArgumentException("Route " + id + " already exists"); + } + Agency agency = agencies.get(agencyId); + if (agency == null) { + throw new IllegalArgumentException("Agency " + agencyId + " does not exist"); + } + log.debug("Adding route {}", id); + routes.put(id, new Route(id, agency, shortName, longName, type)); + return this; + } + + public GtfsScheduleBuilder addCalendar(String id, EnumSet serviceDays, LocalDate startDate, + LocalDate endDate) { + if (calendars.containsKey(id)) { + throw new IllegalArgumentException("Calendar " + id + " already exists"); + } + log.debug("Adding calendar {}", id); + calendars.put(id, new Calendar(id, serviceDays, startDate, endDate)); + return this; + } + + public GtfsScheduleBuilder addCalendarDate(String calendarId, LocalDate date, ExceptionType type) { + Calendar calendar = calendars.get(calendarId); + if (calendar == null) { + throw new IllegalArgumentException("Calendar " + calendarId + " does not exist"); + } + log.debug("Adding calendar {}-{}", calendarId, date); + // TODO: Handle calendar dates + var calendarDate = new CalendarDate(calendar, date, type); + return this; + } + + public GtfsScheduleBuilder addTrip(String id, String routeId, String serviceId) { + if (trips.containsKey(id)) { + throw new IllegalArgumentException("Trip " + id + " already exists"); + } + Route route = routes.get(routeId); + if (route == null) { + throw new IllegalArgumentException("Route " + routeId + " does not exist"); + } + Calendar calendar = calendars.get(serviceId); + if (calendar == null) { + throw new IllegalArgumentException("Calendar " + serviceId + " does not exist"); + } + log.debug("Adding trip {}", id); + trips.put(id, new Trip(id, route, calendar)); + return this; + } + + public GtfsSchedule build() { + return new GtfsSchedule(agencies); + } +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java index f63c7c75..5b6d81fb 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java @@ -13,6 +13,10 @@ import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -38,6 +42,10 @@ public class GtfsScheduleReader { private static final String ZIP_FILE_EXTENSION = ".zip"; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final Map DAY_MAPPINGS = Map.of("monday", DayOfWeek.MONDAY, "tuesday", + DayOfWeek.TUESDAY, "wednesday", DayOfWeek.WEDNESDAY, "thursday", DayOfWeek.THURSDAY, "friday", + DayOfWeek.FRIDAY, "saturday", DayOfWeek.SATURDAY, "sunday", DayOfWeek.SUNDAY); /** * Standard GTFS file types and their corresponding file names. @@ -60,9 +68,9 @@ public enum GtfsFile { private final String fileName; } - public Map> read(String path) throws IOException { + public GtfsSchedule read(String path) throws IOException { File file = new File(path); - Map> records = new HashMap<>(); + Map> records; if (file.isDirectory()) { log.info("Reading GTFS CSV files from directory: {}", path); @@ -73,7 +81,43 @@ public Map> read(String path) throws IOException { } else { throw new IllegalArgumentException("Path must be a directory or a .zip file"); } - return records; + + return buildSchedule(records); + } + + private GtfsSchedule buildSchedule(Map> records) { + GtfsScheduleBuilder builder = GtfsScheduleBuilder.builder(); + for (CSVRecord record : records.get(GtfsFile.AGENCY)) { + builder.addAgency(record.get("agency_id"), record.get("agency_name"), record.get("agency_url"), + record.get("agency_timezone")); + } + for (CSVRecord record : records.get(GtfsFile.STOPS)) { + builder.addStop(record.get("stop_id"), record.get("stop_name"), Double.parseDouble(record.get("stop_lat")), + Double.parseDouble(record.get("stop_lon"))); + } + for (CSVRecord record : records.get(GtfsFile.ROUTES)) { + builder.addRoute(record.get("route_id"), record.get("agency_id"), record.get("route_short_name"), + record.get("route_long_name"), RouteType.parse(record.get("route_type"))); + } + for (CSVRecord record : records.get(GtfsFile.CALENDAR)) { + EnumSet serviceDays = EnumSet.noneOf(DayOfWeek.class); + DAY_MAPPINGS.forEach((key, value) -> { + if ("1".equals(record.get(key))) { + serviceDays.add(value); + } + }); + builder.addCalendar(record.get("service_id"), serviceDays, + LocalDate.parse(record.get("start_date"), DATE_FORMATTER), + LocalDate.parse(record.get("end_date"), DATE_FORMATTER)); + } + for (CSVRecord record : records.get(GtfsFile.CALENDAR_DATES)) { + builder.addCalendarDate(record.get("service_id"), LocalDate.parse(record.get("date"), DATE_FORMATTER), + ExceptionType.parse(record.get("exception_type"))); + } + for (CSVRecord record : records.get(GtfsFile.TRIPS)) { + builder.addTrip(record.get("trip_id"), record.get("route_id"), record.get("service_id")); + } + return builder.build(); } private Map> readFromDirectory(File directory) throws IOException { @@ -100,7 +144,8 @@ private Map> readFromZip(File zipFile) throws IOExcept ZipEntry entry = zf.getEntry(fileType.getFileName()); if (entry != null) { log.debug("Reading GTFS file from ZIP: {}", entry.getName()); - try (InputStreamReader reader = new InputStreamReader(zf.getInputStream(entry), StandardCharsets.UTF_8)) { + try (InputStreamReader reader = new InputStreamReader(zf.getInputStream(entry), + StandardCharsets.UTF_8)) { records.put(fileType, readCsv(reader)); } } else { diff --git a/src/main/java/ch/naviqore/gtfs/schedule/Route.java b/src/main/java/ch/naviqore/gtfs/schedule/Route.java new file mode 100644 index 00000000..91c97ee2 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/Route.java @@ -0,0 +1,4 @@ +package ch.naviqore.gtfs.schedule; + +public record Route(String id, Agency agency, String shortName, String longName, RouteType type) { +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/RouteType.java b/src/main/java/ch/naviqore/gtfs/schedule/RouteType.java new file mode 100644 index 00000000..e476fda3 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/RouteType.java @@ -0,0 +1,36 @@ +package ch.naviqore.gtfs.schedule; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public enum RouteType { + TRAM(0, "Tram, Streetcar, Light rail. Any light rail or street level system within a metropolitan area."), + SUBWAY(1, "Subway, Metro. Any underground rail system within a metropolitan area."), + RAIL(2, "Rail. Used for intercity or long-distance travel."), + BUS(3, "Bus. Used for short- and long-distance bus routes."), + FERRY(4, "Ferry. Used for short- and long-distance boat service."), + CABLE_TRAM(5, "Cable tram. Used for street-level rail cars where the cable runs beneath the vehicle (e.g., cable car in San Francisco)."), + AERIAL_LIFT(6, "Aerial lift, suspended cable car (e.g., gondola lift, aerial tramway). Cable transport where cabins, cars, gondolas or open chairs are suspended by means of one or more cables."), + FUNICULAR(7, "Funicular. Any rail system designed for steep inclines."), + TROLLEYBUS(11, "Trolleybus. Electric buses that draw power from overhead wires using poles."), + MONORAIL(12, "Monorail. Railway in which the track consists of a single rail or a beam."); + + private final int value; + private final String description; + + public static RouteType parse(String value) { + return parse(Integer.parseInt(value)); + } + + public static RouteType parse(int value) { + for (RouteType type : RouteType.values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("No route type with value " + value + " found"); + } +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/Stop.java b/src/main/java/ch/naviqore/gtfs/schedule/Stop.java new file mode 100644 index 00000000..e10b3ef9 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/Stop.java @@ -0,0 +1,4 @@ +package ch.naviqore.gtfs.schedule; + +public record Stop(String id, String name, double lat, double lon) { +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/Trip.java b/src/main/java/ch/naviqore/gtfs/schedule/Trip.java new file mode 100644 index 00000000..9c4f12e8 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/Trip.java @@ -0,0 +1,4 @@ +package ch.naviqore.gtfs.schedule; + +public record Trip(String id, Route route, Calendar calendar) { +} diff --git a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java index db3302de..0da46364 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java @@ -1,6 +1,5 @@ package ch.naviqore.gtfs.schedule; -import org.apache.commons.csv.CSVRecord; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -8,10 +7,6 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertFalse; class GtfsScheduleReaderIT { @@ -25,14 +20,14 @@ void setUp() { @Test void readFromZipFile(@TempDir Path tempDir) throws IOException { File zipFile = GtfsScheduleTestData.prepareZipDataset(tempDir); - Map> records = gtfsScheduleReader.read(zipFile.getAbsolutePath()); - assertFalse(records.isEmpty(), "The records map should not be empty"); + GtfsSchedule schedule = gtfsScheduleReader.read(zipFile.getAbsolutePath()); + // assertFalse(records.isEmpty(), "The records map should not be empty"); } @Test void readFromDirectory(@TempDir Path tempDir) throws IOException { File unzippedDir = GtfsScheduleTestData.prepareUnzippedDataset(tempDir); - Map> records = gtfsScheduleReader.read(unzippedDir.getAbsolutePath()); - assertFalse(records.isEmpty(), "The records map should not be empty"); + GtfsSchedule schedule = gtfsScheduleReader.read(unzippedDir.getAbsolutePath()); + // assertFalse(records.isEmpty(), "The records map should not be empty"); } } \ No newline at end of file From 70d8c1b95ea6cdee970cb5dd5c4882b56c04cca8 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Thu, 11 Apr 2024 13:40:56 +0200 Subject: [PATCH 2/4] Parse all required fields and fields - Refactor entities into model package. - Add stop times to trip and trips to routes. --- .../ch/naviqore/gtfs/schedule/Calendar.java | 8 ---- .../gtfs/schedule/GtfsScheduleReader.java | 25 ++++++----- .../java/ch/naviqore/gtfs/schedule/Route.java | 4 -- .../java/ch/naviqore/gtfs/schedule/Trip.java | 4 -- .../gtfs/schedule/{ => model}/Agency.java | 2 +- .../gtfs/schedule/model/Calendar.java | 45 +++++++++++++++++++ .../schedule/{ => model}/CalendarDate.java | 4 +- .../schedule/{ => model}/GtfsSchedule.java | 2 +- .../{ => model}/GtfsScheduleBuilder.java | 32 ++++++++++--- .../naviqore/gtfs/schedule/model/Route.java | 44 ++++++++++++++++++ .../gtfs/schedule/{ => model}/Stop.java | 2 +- .../gtfs/schedule/model/StopTime.java | 6 +++ .../ch/naviqore/gtfs/schedule/model/Trip.java | 45 +++++++++++++++++++ .../schedule/{ => type}/ExceptionType.java | 2 +- .../gtfs/schedule/{ => type}/RouteType.java | 2 +- .../gtfs/schedule/type/ServiceDayTime.java | 44 ++++++++++++++++++ .../gtfs/schedule/GtfsScheduleReaderIT.java | 1 + 17 files changed, 233 insertions(+), 39 deletions(-) delete mode 100644 src/main/java/ch/naviqore/gtfs/schedule/Calendar.java delete mode 100644 src/main/java/ch/naviqore/gtfs/schedule/Route.java delete mode 100644 src/main/java/ch/naviqore/gtfs/schedule/Trip.java rename src/main/java/ch/naviqore/gtfs/schedule/{ => model}/Agency.java (66%) create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/model/Calendar.java rename src/main/java/ch/naviqore/gtfs/schedule/{ => model}/CalendarDate.java (54%) rename src/main/java/ch/naviqore/gtfs/schedule/{ => model}/GtfsSchedule.java (88%) rename src/main/java/ch/naviqore/gtfs/schedule/{ => model}/GtfsScheduleBuilder.java (74%) create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/model/Route.java rename src/main/java/ch/naviqore/gtfs/schedule/{ => model}/Stop.java (63%) create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/model/StopTime.java create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java rename src/main/java/ch/naviqore/gtfs/schedule/{ => type}/ExceptionType.java (95%) rename src/main/java/ch/naviqore/gtfs/schedule/{ => type}/RouteType.java (97%) create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java diff --git a/src/main/java/ch/naviqore/gtfs/schedule/Calendar.java b/src/main/java/ch/naviqore/gtfs/schedule/Calendar.java deleted file mode 100644 index aaf5cf07..00000000 --- a/src/main/java/ch/naviqore/gtfs/schedule/Calendar.java +++ /dev/null @@ -1,8 +0,0 @@ -package ch.naviqore.gtfs.schedule; - -import java.time.DayOfWeek; -import java.time.LocalDate; -import java.util.EnumSet; - -public record Calendar(String id, EnumSet serviceDays, LocalDate startDate, LocalDate endDate) { -} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java index 5b6d81fb..cb6c095c 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java @@ -1,5 +1,10 @@ package ch.naviqore.gtfs.schedule; +import ch.naviqore.gtfs.schedule.model.GtfsSchedule; +import ch.naviqore.gtfs.schedule.model.GtfsScheduleBuilder; +import ch.naviqore.gtfs.schedule.type.ExceptionType; +import ch.naviqore.gtfs.schedule.type.RouteType; +import ch.naviqore.gtfs.schedule.type.ServiceDayTime; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; @@ -53,17 +58,10 @@ public class GtfsScheduleReader { @RequiredArgsConstructor @Getter public enum GtfsFile { - AGENCY("agency.txt"), - CALENDAR_DATES("calendar_dates.txt"), - CALENDAR("calendar.txt"), - FARE_ATTRIBUTES("fare_attributes.txt"), - FARE_RULES("fare_rules.txt"), - FREQUENCIES("frequencies.txt"), - ROUTES("routes.txt"), - SHAPES("shapes.txt"), - STOP_TIMES("stop_times.txt"), - STOPS("stops.txt"), - TRIPS("trips.txt"); + AGENCY("agency.txt"), CALENDAR_DATES("calendar_dates.txt"), CALENDAR("calendar.txt"), FARE_ATTRIBUTES( + "fare_attributes.txt"), FARE_RULES("fare_rules.txt"), FREQUENCIES("frequencies.txt"), ROUTES( + "routes.txt"), SHAPES("shapes.txt"), STOP_TIMES("stop_times.txt"), STOPS("stops.txt"), TRIPS( + "trips.txt"); private final String fileName; } @@ -117,6 +115,11 @@ private GtfsSchedule buildSchedule(Map> records) { for (CSVRecord record : records.get(GtfsFile.TRIPS)) { builder.addTrip(record.get("trip_id"), record.get("route_id"), record.get("service_id")); } + for (CSVRecord record : records.get(GtfsFile.STOP_TIMES)) { + builder.addStopTime(record.get("trip_id"), record.get("stop_id"), + ServiceDayTime.parse(record.get("arrival_time")), + ServiceDayTime.parse(record.get("departure_time"))); + } return builder.build(); } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/Route.java b/src/main/java/ch/naviqore/gtfs/schedule/Route.java deleted file mode 100644 index 91c97ee2..00000000 --- a/src/main/java/ch/naviqore/gtfs/schedule/Route.java +++ /dev/null @@ -1,4 +0,0 @@ -package ch.naviqore.gtfs.schedule; - -public record Route(String id, Agency agency, String shortName, String longName, RouteType type) { -} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/Trip.java b/src/main/java/ch/naviqore/gtfs/schedule/Trip.java deleted file mode 100644 index 9c4f12e8..00000000 --- a/src/main/java/ch/naviqore/gtfs/schedule/Trip.java +++ /dev/null @@ -1,4 +0,0 @@ -package ch.naviqore.gtfs.schedule; - -public record Trip(String id, Route route, Calendar calendar) { -} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/Agency.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Agency.java similarity index 66% rename from src/main/java/ch/naviqore/gtfs/schedule/Agency.java rename to src/main/java/ch/naviqore/gtfs/schedule/model/Agency.java index f2b376e0..a15dfee9 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/Agency.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Agency.java @@ -1,4 +1,4 @@ -package ch.naviqore.gtfs.schedule; +package ch.naviqore.gtfs.schedule.model; public record Agency(String agency, String name, String url, String timezone) { } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Calendar.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Calendar.java new file mode 100644 index 00000000..c718e784 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Calendar.java @@ -0,0 +1,45 @@ +package ch.naviqore.gtfs.schedule.model; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +@Getter +public final class Calendar { + private final String id; + private final EnumSet serviceDays; + private final LocalDate startDate; + private final LocalDate endDate; + private final Map calendarDates = new HashMap<>(); + + void addCalendarDate(CalendarDate calendarDate) { + calendarDates.put(calendarDate.date(), calendarDate); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (Calendar) obj; + return Objects.equals(this.id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Calendar[" + "id=" + id + ", " + "serviceDays=" + serviceDays + ", " + "startDate=" + startDate + ", " + "endDate=" + endDate + ']'; + } + +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/CalendarDate.java b/src/main/java/ch/naviqore/gtfs/schedule/model/CalendarDate.java similarity index 54% rename from src/main/java/ch/naviqore/gtfs/schedule/CalendarDate.java rename to src/main/java/ch/naviqore/gtfs/schedule/model/CalendarDate.java index 78c9f9f9..4e943425 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/CalendarDate.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/CalendarDate.java @@ -1,4 +1,6 @@ -package ch.naviqore.gtfs.schedule; +package ch.naviqore.gtfs.schedule.model; + +import ch.naviqore.gtfs.schedule.type.ExceptionType; import java.time.LocalDate; diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsSchedule.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java similarity index 88% rename from src/main/java/ch/naviqore/gtfs/schedule/GtfsSchedule.java rename to src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java index 4473613b..003e808b 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsSchedule.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java @@ -1,4 +1,4 @@ -package ch.naviqore.gtfs.schedule; +package ch.naviqore.gtfs.schedule.model; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleBuilder.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java similarity index 74% rename from src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleBuilder.java rename to src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java index 8c151bf3..1de36e6c 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleBuilder.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java @@ -1,5 +1,8 @@ -package ch.naviqore.gtfs.schedule; +package ch.naviqore.gtfs.schedule.model; +import ch.naviqore.gtfs.schedule.type.ExceptionType; +import ch.naviqore.gtfs.schedule.type.RouteType; +import ch.naviqore.gtfs.schedule.type.ServiceDayTime; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -55,8 +58,7 @@ public GtfsScheduleBuilder addRoute(String id, String agencyId, String shortName return this; } - public GtfsScheduleBuilder addCalendar(String id, EnumSet serviceDays, LocalDate startDate, - LocalDate endDate) { + public GtfsScheduleBuilder addCalendar(String id, EnumSet serviceDays, LocalDate startDate, LocalDate endDate) { if (calendars.containsKey(id)) { throw new IllegalArgumentException("Calendar " + id + " already exists"); } @@ -71,8 +73,8 @@ public GtfsScheduleBuilder addCalendarDate(String calendarId, LocalDate date, Ex throw new IllegalArgumentException("Calendar " + calendarId + " does not exist"); } log.debug("Adding calendar {}-{}", calendarId, date); - // TODO: Handle calendar dates - var calendarDate = new CalendarDate(calendar, date, type); + CalendarDate calendarDate = new CalendarDate(calendar, date, type); + calendar.addCalendarDate(calendarDate); return this; } @@ -89,7 +91,25 @@ public GtfsScheduleBuilder addTrip(String id, String routeId, String serviceId) throw new IllegalArgumentException("Calendar " + serviceId + " does not exist"); } log.debug("Adding trip {}", id); - trips.put(id, new Trip(id, route, calendar)); + Trip trip = new Trip(id, route, calendar); + route.addTrip(trip); + trips.put(id, trip); + return this; + } + + public GtfsScheduleBuilder addStopTime(String tripId, String stopId, ServiceDayTime arrival, ServiceDayTime departure) { + Trip trip = trips.get(tripId); + if (trip == null) { + throw new IllegalArgumentException("Trip " + tripId + " does not exist"); + } + Stop stop = stops.get(stopId); + if (stop == null) { + throw new IllegalArgumentException("Stop " + stopId + " does not exist"); + } + log.debug("Adding stop {} to trip {} ({}-{})", stopId, tripId, arrival, departure); + StopTime stopTime = new StopTime(stop, trip, arrival, departure); + trip.addStopTime(stopTime); + // TODO: Add stop time to stop return this; } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java new file mode 100644 index 00000000..47139f65 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java @@ -0,0 +1,44 @@ +package ch.naviqore.gtfs.schedule.model; + +import ch.naviqore.gtfs.schedule.type.RouteType; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +@Getter +public final class Route { + private final String id; + private final Agency agency; + private final String shortName; + private final String longName; + private final RouteType type; + private final List trips = new ArrayList<>(); + + void addTrip(Trip trip) { + trips.add(trip); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (Route) obj; + return Objects.equals(this.id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Route[" + "id=" + id + ", " + "agency=" + agency + ", " + "shortName=" + shortName + ", " + "longName=" + longName + ", " + "type=" + type + ']'; + } + +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/Stop.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java similarity index 63% rename from src/main/java/ch/naviqore/gtfs/schedule/Stop.java rename to src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java index e10b3ef9..d570ddff 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/Stop.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java @@ -1,4 +1,4 @@ -package ch.naviqore.gtfs.schedule; +package ch.naviqore.gtfs.schedule.model; public record Stop(String id, String name, double lat, double lon) { } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/StopTime.java b/src/main/java/ch/naviqore/gtfs/schedule/model/StopTime.java new file mode 100644 index 00000000..122eeecf --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/StopTime.java @@ -0,0 +1,6 @@ +package ch.naviqore.gtfs.schedule.model; + +import ch.naviqore.gtfs.schedule.type.ServiceDayTime; + +public record StopTime(Stop stop, Trip trip, ServiceDayTime arrival, ServiceDayTime departure) { +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java new file mode 100644 index 00000000..973a8e29 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java @@ -0,0 +1,45 @@ +package ch.naviqore.gtfs.schedule.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@RequiredArgsConstructor +@Getter +public final class Trip implements Comparable { + private final String id; + private final Route route; + private final Calendar calendar; + private final List stopTimes = new ArrayList<>(); + + void addStopTime(StopTime stopTime) { + stopTimes.add(stopTime); + } + + @Override + public int compareTo(Trip o) { + // TODO: Sort stopTimes, then return first stop. + return 0; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (Trip) obj; + return Objects.equals(this.id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Trip[" + "id=" + id + ", " + "route=" + route + ", " + "calendar=" + calendar + ']'; + } +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/ExceptionType.java b/src/main/java/ch/naviqore/gtfs/schedule/type/ExceptionType.java similarity index 95% rename from src/main/java/ch/naviqore/gtfs/schedule/ExceptionType.java rename to src/main/java/ch/naviqore/gtfs/schedule/type/ExceptionType.java index 1f6c8ec6..6bd88e71 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/ExceptionType.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/ExceptionType.java @@ -1,4 +1,4 @@ -package ch.naviqore.gtfs.schedule; +package ch.naviqore.gtfs.schedule.type; import lombok.AccessLevel; import lombok.Getter; diff --git a/src/main/java/ch/naviqore/gtfs/schedule/RouteType.java b/src/main/java/ch/naviqore/gtfs/schedule/type/RouteType.java similarity index 97% rename from src/main/java/ch/naviqore/gtfs/schedule/RouteType.java rename to src/main/java/ch/naviqore/gtfs/schedule/type/RouteType.java index e476fda3..9c17d404 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/RouteType.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/RouteType.java @@ -1,4 +1,4 @@ -package ch.naviqore.gtfs.schedule; +package ch.naviqore.gtfs.schedule.type; import lombok.AccessLevel; import lombok.Getter; diff --git a/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java b/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java new file mode 100644 index 00000000..ea37c8b5 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java @@ -0,0 +1,44 @@ +package ch.naviqore.gtfs.schedule.type; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + + +/** + * Service day time + *

+ * A service day may end after 24:00:00 and times like 25:30:00 are possible values. java.time.LocalTime does not + * support this. Therefore, this time class is used to be able to cover service days bigger than 24 hours. + * + * @author munterfi + */ +@EqualsAndHashCode +@Getter +public class ServiceDayTime implements Comparable { + private final int totalSeconds; + + public ServiceDayTime(int hours, int minutes, int seconds) { + this.totalSeconds = seconds + 60 * minutes + 3600 * hours; + } + + public static ServiceDayTime parse(String timeString) { + String[] parts = timeString.split(":"); + int hours = Integer.parseInt(parts[0]); + int minutes = Integer.parseInt(parts[1]); + int seconds = Integer.parseInt(parts[2]); + return new ServiceDayTime(hours, minutes, seconds); + } + + @Override + public int compareTo(ServiceDayTime o) { + return Integer.compare(totalSeconds, o.totalSeconds); + } + + @Override + public String toString() { + int hours = totalSeconds / 3600; + int minutes = (totalSeconds % 3600) / 60; + int seconds = totalSeconds % 60; + return String.format("%02d:%02d:%02d", hours, minutes, seconds); + } +} diff --git a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java index 0da46364..42937656 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java @@ -1,5 +1,6 @@ package ch.naviqore.gtfs.schedule; +import ch.naviqore.gtfs.schedule.model.GtfsSchedule; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; From e2fd4dad0ab71bad7304d3b923794af28d714574 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Sat, 13 Apr 2024 12:54:39 +0200 Subject: [PATCH 3/4] Add GTFS schedule service day - Extract a service day from the schedule. - Introduce Initializable interface to mark objects that need construction. --- .../gtfs/schedule/model/Calendar.java | 19 ++++++++ .../gtfs/schedule/model/CalendarDate.java | 6 ++- .../gtfs/schedule/model/GtfsSchedule.java | 43 +++++++++++++++++- .../schedule/model/GtfsScheduleBuilder.java | 23 ++++++++-- .../gtfs/schedule/model/GtfsScheduleDay.java | 36 +++++++++++++++ .../gtfs/schedule/model/Initializable.java | 19 ++++++++ .../naviqore/gtfs/schedule/model/Route.java | 9 +++- .../ch/naviqore/gtfs/schedule/model/Stop.java | 45 ++++++++++++++++++- .../gtfs/schedule/model/StopTime.java | 18 +++++++- .../ch/naviqore/gtfs/schedule/model/Trip.java | 12 +++-- 10 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleDay.java create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/model/Initializable.java diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Calendar.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Calendar.java index c718e784..c7a2d8eb 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/Calendar.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Calendar.java @@ -1,5 +1,6 @@ package ch.naviqore.gtfs.schedule.model; +import ch.naviqore.gtfs.schedule.type.ExceptionType; import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -20,6 +21,24 @@ public final class Calendar { private final LocalDate endDate; private final Map calendarDates = new HashMap<>(); + /** + * Determines if the service is operational on a specific day, considering both regular service days and + * exceptions. + * + * @param date the date to check for service availability + * @return true if the service is operational on the given date, false otherwise + */ + public boolean isServiceAvailable(LocalDate date) { + if (date.isBefore(startDate) || date.isAfter(endDate)) { + return false; + } + CalendarDate exception = calendarDates.get(date); + if (exception != null) { + return exception.type() == ExceptionType.ADDED; + } + return serviceDays.contains(date.getDayOfWeek()); + } + void addCalendarDate(CalendarDate calendarDate) { calendarDates.put(calendarDate.date(), calendarDate); } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/CalendarDate.java b/src/main/java/ch/naviqore/gtfs/schedule/model/CalendarDate.java index 4e943425..b2dd4eb9 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/CalendarDate.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/CalendarDate.java @@ -4,5 +4,9 @@ import java.time.LocalDate; -public record CalendarDate(Calendar calendar, LocalDate date, ExceptionType type) { +public record CalendarDate(Calendar calendar, LocalDate date, ExceptionType type) implements Comparable { + @Override + public int compareTo(CalendarDate o) { + return this.date.compareTo(o.date); + } } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java index 003e808b..04db8fb1 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java @@ -3,14 +3,53 @@ import lombok.AccessLevel; import lombok.RequiredArgsConstructor; +import java.time.LocalDate; +import java.util.Collections; import java.util.Map; +import java.util.stream.Collectors; @RequiredArgsConstructor(access = AccessLevel.PACKAGE) public class GtfsSchedule { private final Map agencies; + private final Map calendars; + private final Map stops; + private final Map routes; + private final Map trips; - public Agency getAgency(String id) { - return agencies.get(id); + /** + * Retrieves a snapshot of the GTFS schedule active on a specific date. + * + * @param date the date for which the active schedule is requested. + * @return GtfsScheduleDay containing only the active routes, stops, and trips for the specified date. + */ + public GtfsScheduleDay getScheduleForDay(LocalDate date) { + Map activeTrips = trips.entrySet().stream() + .filter(entry -> entry.getValue().getCalendar().isServiceAvailable(date)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + // TODO: Implement efficiently without copying. + // return new GtfsScheduleDay(date, activeStops, activeRoutes, activeTrips); + return null; + } + + public Map getAgencies() { + return Collections.unmodifiableMap(agencies); + } + + public Map getCalendars() { + return Collections.unmodifiableMap(calendars); + } + + public Map getStops() { + return Collections.unmodifiableMap(stops); + } + + public Map getRoutes() { + return Collections.unmodifiableMap(routes); + } + + public Map getTrips() { + return Collections.unmodifiableMap(trips); } } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java index 1de36e6c..e56887f9 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java @@ -13,14 +13,27 @@ import java.util.HashMap; import java.util.Map; +/** + * General Transit Feed Specification (GTFS) schedule builder + *

+ * Provides a builder pattern implementation for constructing a {@link GtfsSchedule} instance. This class encapsulates + * the complexity of assembling a GTFS schedule by incrementally adding components such as agencies, stops, routes, + * trips, and calendars. The builder ensures that all components are added in a controlled manner and that the resulting + * schedule is consistent and ready for use. + * + *

Instances of this class should be obtained through the static {@code builder()} method. This class uses + * a private constructor to enforce the use of the builder pattern.

+ * + * @author munterfi + */ @NoArgsConstructor(access = AccessLevel.PRIVATE) @Log4j2 public class GtfsScheduleBuilder { private final Map agencies = new HashMap<>(); + private final Map calendars = new HashMap<>(); private final Map stops = new HashMap<>(); private final Map routes = new HashMap<>(); - private final Map calendars = new HashMap<>(); private final Map trips = new HashMap<>(); public static GtfsScheduleBuilder builder() { @@ -108,12 +121,16 @@ public GtfsScheduleBuilder addStopTime(String tripId, String stopId, ServiceDayT } log.debug("Adding stop {} to trip {} ({}-{})", stopId, tripId, arrival, departure); StopTime stopTime = new StopTime(stop, trip, arrival, departure); + stop.addStopTime(stopTime); trip.addStopTime(stopTime); - // TODO: Add stop time to stop return this; } public GtfsSchedule build() { - return new GtfsSchedule(agencies); + log.info("Building schedule with {} stops, {} routes and {} trips", stops.size(), routes.size(), trips.size()); + trips.values().parallelStream().forEach(Trip::initialize); + stops.values().parallelStream().forEach(Stop::initialize); + routes.values().parallelStream().forEach(Route::initialize); + return new GtfsSchedule(agencies, calendars, stops, routes, trips); } } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleDay.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleDay.java new file mode 100644 index 00000000..25d93101 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleDay.java @@ -0,0 +1,36 @@ +package ch.naviqore.gtfs.schedule.model; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDate; +import java.util.Collections; +import java.util.Map; + +/** + * GTFS Schedule Service Day + *

+ * Represents a daily snapshot of the GTFS schedule, containing only the active services on a specific date. + */ +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public class GtfsScheduleDay { + + @Getter + private final LocalDate date; + private final Map stops; + private final Map routes; + private final Map trips; + + public Map getStops() { + return Collections.unmodifiableMap(stops); + } + + public Map getRoutes() { + return Collections.unmodifiableMap(routes); + } + + public Map getTrips() { + return Collections.unmodifiableMap(trips); + } +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Initializable.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Initializable.java new file mode 100644 index 00000000..0e535225 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Initializable.java @@ -0,0 +1,19 @@ +package ch.naviqore.gtfs.schedule.model; + +/** + * Initializable class + *

+ * This internal interface should be implemented by classes that require initialization steps to be executed before they + * are considered fully ready and operational. This interface is designed to enforce a consistent initialization pattern + * across different components of the GTFS (General Transit Feed Specification) schedule model. + * + * @author munterfi + */ +interface Initializable { + + /** + * Initializes the implementing object. + */ + void initialize(); + +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java index 47139f65..986a19e9 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java @@ -6,12 +6,13 @@ import lombok.RequiredArgsConstructor; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; @RequiredArgsConstructor(access = AccessLevel.PACKAGE) @Getter -public final class Route { +public final class Route implements Initializable { private final String id; private final Agency agency; private final String shortName; @@ -23,6 +24,12 @@ void addTrip(Trip trip) { trips.add(trip); } + @Override + public void initialize() { + Collections.sort(trips); + + } + @Override public boolean equals(Object obj) { if (obj == this) return true; diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java index d570ddff..41a80219 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java @@ -1,4 +1,47 @@ package ch.naviqore.gtfs.schedule.model; -public record Stop(String id, String name, double lat, double lon) { +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +@Getter +public final class Stop implements Initializable { + private final String id; + private final String name; + private final double lat; + private final double lon; + private final List stopTimes = new ArrayList<>(); + + void addStopTime(StopTime stopTime) { + stopTimes.add(stopTime); + } + + @Override + public void initialize() { + Collections.sort(stopTimes); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (Stop) obj; + return Objects.equals(this.id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Stop[" + "id=" + id + ", " + "name=" + name + ", " + "lat=" + lat + ", " + "lon=" + lon + ']'; + } } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/StopTime.java b/src/main/java/ch/naviqore/gtfs/schedule/model/StopTime.java index 122eeecf..85b320b0 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/StopTime.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/StopTime.java @@ -2,5 +2,21 @@ import ch.naviqore.gtfs.schedule.type.ServiceDayTime; -public record StopTime(Stop stop, Trip trip, ServiceDayTime arrival, ServiceDayTime departure) { +public record StopTime(Stop stop, Trip trip, ServiceDayTime arrival, + ServiceDayTime departure) implements Comparable { + + public StopTime { + if (arrival.compareTo(departure) > 0) { + throw new IllegalArgumentException("Arrival time must be before departure time."); + } + } + + /** + * StopTimes are compared based on departures. + */ + @Override + public int compareTo(StopTime o) { + return this.departure.compareTo(o.departure); + } } + diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java index 973a8e29..3735c3bb 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java @@ -4,12 +4,13 @@ import lombok.RequiredArgsConstructor; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; @RequiredArgsConstructor @Getter -public final class Trip implements Comparable { +public final class Trip implements Comparable, Initializable { private final String id; private final Route route; private final Calendar calendar; @@ -19,10 +20,14 @@ void addStopTime(StopTime stopTime) { stopTimes.add(stopTime); } + @Override + public void initialize() { + Collections.sort(stopTimes); + } + @Override public int compareTo(Trip o) { - // TODO: Sort stopTimes, then return first stop. - return 0; + return this.stopTimes.getFirst().compareTo(o.stopTimes.getFirst()); } @Override @@ -42,4 +47,5 @@ public int hashCode() { public String toString() { return "Trip[" + "id=" + id + ", " + "route=" + route + ", " + "calendar=" + calendar + ']'; } + } From f0cc196bc4fc2b5d85b6557513f66c23641a4589 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Sun, 14 Apr 2024 18:32:49 +0200 Subject: [PATCH 4/4] Add cache for value objects - Example with GTFS of Switzerland. - Add cache for value objects to reduce the schedule size. --- .../gtfs/schedule/GtfsScheduleParser.java | 98 +++++++++++++++++++ .../gtfs/schedule/GtfsScheduleReader.java | 91 +++++++---------- .../ch/naviqore/gtfs/schedule/RunExample.java | 18 ++++ .../schedule/model/GtfsScheduleBuilder.java | 30 +++++- .../gtfs/schedule/type/ServiceDayTime.java | 3 +- 5 files changed, 177 insertions(+), 63 deletions(-) create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/RunExample.java diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java new file mode 100644 index 00000000..715f73f2 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java @@ -0,0 +1,98 @@ +package ch.naviqore.gtfs.schedule; + +import ch.naviqore.gtfs.schedule.model.GtfsScheduleBuilder; +import ch.naviqore.gtfs.schedule.type.ExceptionType; +import ch.naviqore.gtfs.schedule.type.RouteType; +import ch.naviqore.gtfs.schedule.type.ServiceDayTime; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.csv.CSVRecord; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +/** + * GTFS CSV Records Parser + * + * @author munterfi + */ +@RequiredArgsConstructor +@Log4j2 +class GtfsScheduleParser { + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final Map DAY_MAPPINGS = Map.of("monday", DayOfWeek.MONDAY, "tuesday", + DayOfWeek.TUESDAY, "wednesday", DayOfWeek.WEDNESDAY, "thursday", DayOfWeek.THURSDAY, "friday", + DayOfWeek.FRIDAY, "saturday", DayOfWeek.SATURDAY, "sunday", DayOfWeek.SUNDAY); + private final GtfsScheduleBuilder builder; + + void parseAgencies(List records) { + log.info("Parsing {} agency records", records.size()); + for (CSVRecord record : records) { + builder.addAgency(record.get("agency_id"), record.get("agency_name"), record.get("agency_url"), + record.get("agency_timezone")); + } + } + + void parseCalendars(List records) { + log.info("Parsing {} calendar records", records.size()); + for (CSVRecord record : records) { + EnumSet serviceDays = EnumSet.noneOf(DayOfWeek.class); + DAY_MAPPINGS.forEach((key, value) -> { + if ("1".equals(record.get(key))) { + serviceDays.add(value); + } + }); + builder.addCalendar(record.get("service_id"), serviceDays, + LocalDate.parse(record.get("start_date"), DATE_FORMATTER), + LocalDate.parse(record.get("end_date"), DATE_FORMATTER)); + } + } + + void parseCalendarDates(List records) { + log.info("Parsing {} calendar date records", records.size()); + for (CSVRecord record : records) { + builder.addCalendarDate(record.get("service_id"), LocalDate.parse(record.get("date"), DATE_FORMATTER), + ExceptionType.parse(record.get("exception_type"))); + } + } + + void parseStops(List records) { + log.info("Parsing {} stop records", records.size()); + for (CSVRecord record : records) { + builder.addStop(record.get("stop_id"), record.get("stop_name"), Double.parseDouble(record.get("stop_lat")), + Double.parseDouble(record.get("stop_lon"))); + } + } + + void parseRoutes(List records) { + log.info("Parsing {} route records", records.size()); + for (CSVRecord record : records) { + // TODO: Route types are not standardized in any way. + // RouteType.parse(record.get("route_type")) + builder.addRoute(record.get("route_id"), record.get("agency_id"), record.get("route_short_name"), + record.get("route_long_name"), RouteType.RAIL); + } + } + + void parseTrips(List records) { + log.info("Parsing {} trip records", records.size()); + for (CSVRecord record : records) { + builder.addTrip(record.get("trip_id"), record.get("route_id"), record.get("service_id")); + } + } + + void parseStopTimes(List records) { + log.info("Parsing {} stop time records", records.size()); + for (CSVRecord record : records) { + builder.addStopTime(record.get("trip_id"), record.get("stop_id"), + ServiceDayTime.parse(record.get("arrival_time")), + ServiceDayTime.parse(record.get("departure_time"))); + } + } + +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java index cb6c095c..c3951b2a 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java @@ -2,9 +2,6 @@ import ch.naviqore.gtfs.schedule.model.GtfsSchedule; import ch.naviqore.gtfs.schedule.model.GtfsScheduleBuilder; -import ch.naviqore.gtfs.schedule.type.ExceptionType; -import ch.naviqore.gtfs.schedule.type.RouteType; -import ch.naviqore.gtfs.schedule.type.ServiceDayTime; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; @@ -12,16 +9,14 @@ import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVRecord; +import org.apache.commons.io.ByteOrderMark; +import org.apache.commons.io.input.BOMInputStream; import java.io.File; -import java.io.FileReader; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.time.DayOfWeek; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -47,10 +42,6 @@ public class GtfsScheduleReader { private static final String ZIP_FILE_EXTENSION = ".zip"; - private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); - private static final Map DAY_MAPPINGS = Map.of("monday", DayOfWeek.MONDAY, "tuesday", - DayOfWeek.TUESDAY, "wednesday", DayOfWeek.WEDNESDAY, "thursday", DayOfWeek.THURSDAY, "friday", - DayOfWeek.FRIDAY, "saturday", DayOfWeek.SATURDAY, "sunday", DayOfWeek.SUNDAY); /** * Standard GTFS file types and their corresponding file names. @@ -58,10 +49,17 @@ public class GtfsScheduleReader { @RequiredArgsConstructor @Getter public enum GtfsFile { - AGENCY("agency.txt"), CALENDAR_DATES("calendar_dates.txt"), CALENDAR("calendar.txt"), FARE_ATTRIBUTES( - "fare_attributes.txt"), FARE_RULES("fare_rules.txt"), FREQUENCIES("frequencies.txt"), ROUTES( - "routes.txt"), SHAPES("shapes.txt"), STOP_TIMES("stop_times.txt"), STOPS("stops.txt"), TRIPS( - "trips.txt"); + AGENCY("agency.txt"), + CALENDAR_DATES("calendar_dates.txt"), + CALENDAR("calendar.txt"), + FARE_ATTRIBUTES("fare_attributes.txt"), + FARE_RULES("fare_rules.txt"), + FREQUENCIES("frequencies.txt"), + ROUTES("routes.txt"), + SHAPES("shapes.txt"), + STOP_TIMES("stop_times.txt"), + STOPS("stops.txt"), + TRIPS("trips.txt"); private final String fileName; } @@ -85,41 +83,14 @@ public GtfsSchedule read(String path) throws IOException { private GtfsSchedule buildSchedule(Map> records) { GtfsScheduleBuilder builder = GtfsScheduleBuilder.builder(); - for (CSVRecord record : records.get(GtfsFile.AGENCY)) { - builder.addAgency(record.get("agency_id"), record.get("agency_name"), record.get("agency_url"), - record.get("agency_timezone")); - } - for (CSVRecord record : records.get(GtfsFile.STOPS)) { - builder.addStop(record.get("stop_id"), record.get("stop_name"), Double.parseDouble(record.get("stop_lat")), - Double.parseDouble(record.get("stop_lon"))); - } - for (CSVRecord record : records.get(GtfsFile.ROUTES)) { - builder.addRoute(record.get("route_id"), record.get("agency_id"), record.get("route_short_name"), - record.get("route_long_name"), RouteType.parse(record.get("route_type"))); - } - for (CSVRecord record : records.get(GtfsFile.CALENDAR)) { - EnumSet serviceDays = EnumSet.noneOf(DayOfWeek.class); - DAY_MAPPINGS.forEach((key, value) -> { - if ("1".equals(record.get(key))) { - serviceDays.add(value); - } - }); - builder.addCalendar(record.get("service_id"), serviceDays, - LocalDate.parse(record.get("start_date"), DATE_FORMATTER), - LocalDate.parse(record.get("end_date"), DATE_FORMATTER)); - } - for (CSVRecord record : records.get(GtfsFile.CALENDAR_DATES)) { - builder.addCalendarDate(record.get("service_id"), LocalDate.parse(record.get("date"), DATE_FORMATTER), - ExceptionType.parse(record.get("exception_type"))); - } - for (CSVRecord record : records.get(GtfsFile.TRIPS)) { - builder.addTrip(record.get("trip_id"), record.get("route_id"), record.get("service_id")); - } - for (CSVRecord record : records.get(GtfsFile.STOP_TIMES)) { - builder.addStopTime(record.get("trip_id"), record.get("stop_id"), - ServiceDayTime.parse(record.get("arrival_time")), - ServiceDayTime.parse(record.get("departure_time"))); - } + GtfsScheduleParser parser = new GtfsScheduleParser(builder); + parser.parseAgencies(records.get(GtfsFile.AGENCY)); + parser.parseCalendars(records.get(GtfsFile.CALENDAR)); + parser.parseCalendarDates(records.get(GtfsFile.CALENDAR_DATES)); + parser.parseStops(records.get(GtfsFile.STOPS)); + parser.parseRoutes(records.get(GtfsFile.ROUTES)); + parser.parseTrips(records.get(GtfsFile.TRIPS)); + parser.parseStopTimes(records.get(GtfsFile.STOP_TIMES)); return builder.build(); } @@ -129,7 +100,7 @@ private Map> readFromDirectory(File directory) throws for (GtfsFile fileType : GtfsFile.values()) { File csvFile = new File(directory, fileType.getFileName()); if (csvFile.exists()) { - log.debug("Reading GTFS CSV file: {}", csvFile.getAbsolutePath()); + log.info("Reading GTFS CSV file: {}", csvFile.getAbsolutePath()); records.put(fileType, readCsvFile(csvFile)); } else { log.warn("GTFS CSV file {} not found", csvFile.getAbsolutePath()); @@ -146,9 +117,12 @@ private Map> readFromZip(File zipFile) throws IOExcept for (GtfsFile fileType : GtfsFile.values()) { ZipEntry entry = zf.getEntry(fileType.getFileName()); if (entry != null) { - log.debug("Reading GTFS file from ZIP: {}", entry.getName()); - try (InputStreamReader reader = new InputStreamReader(zf.getInputStream(entry), - StandardCharsets.UTF_8)) { + log.info("Reading GTFS file from ZIP: {}", entry.getName()); + try (InputStreamReader reader = new InputStreamReader(BOMInputStream.builder() + .setInputStream(zf.getInputStream(entry)) + .setByteOrderMarks(ByteOrderMark.UTF_8) + .setInclude(false) + .get(), StandardCharsets.UTF_8)) { records.put(fileType, readCsv(reader)); } } else { @@ -161,7 +135,11 @@ private Map> readFromZip(File zipFile) throws IOExcept } private List readCsvFile(File file) throws IOException { - try (FileReader reader = new FileReader(file)) { + try (FileInputStream fileInputStream = new FileInputStream(file); + BOMInputStream bomInputStream = BOMInputStream.builder() + .setInputStream(fileInputStream) + .setByteOrderMarks(ByteOrderMark.UTF_8) + .get(); InputStreamReader reader = new InputStreamReader(bomInputStream, StandardCharsets.UTF_8)) { return readCsv(reader); } } @@ -169,6 +147,7 @@ private List readCsvFile(File file) throws IOException { private List readCsv(InputStreamReader reader) throws IOException { CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().setIgnoreHeaderCase(true).setTrim(true).build(); try (CSVParser parser = new CSVParser(reader, format)) { + log.debug("CSV Headers: {}", parser.getHeaderMap().keySet()); return parser.getRecords(); } } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/RunExample.java b/src/main/java/ch/naviqore/gtfs/schedule/RunExample.java new file mode 100644 index 00000000..c077ef1d --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/RunExample.java @@ -0,0 +1,18 @@ +package ch.naviqore.gtfs.schedule; + +import ch.naviqore.gtfs.schedule.model.GtfsSchedule; +import ch.naviqore.gtfs.schedule.model.GtfsScheduleDay; + +import java.io.IOException; +import java.time.LocalDate; + +public class RunExample { + private static final String GTFS_FILE = "/Users/munterfi/Downloads/gtfs_fp2024_2024-04-11_09-11.zip"; + + public static void main(String[] args) throws IOException, InterruptedException { + GtfsSchedule schedule = new GtfsScheduleReader().read(GTFS_FILE); + GtfsScheduleDay scheduleDay = schedule.getScheduleForDay(LocalDate.now()); + System.gc(); + Thread.sleep(30000); + } +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java index e56887f9..7830eb31 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java @@ -12,6 +12,7 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * General Transit Feed Specification (GTFS) schedule builder @@ -30,6 +31,23 @@ @Log4j2 public class GtfsScheduleBuilder { + /** + * Cache for value objects + */ + static class Cache { + private final Map localDates = new ConcurrentHashMap<>(); + private final Map serviceDayTimes = new ConcurrentHashMap<>(); + + public LocalDate getOrAdd(LocalDate value) { + return localDates.computeIfAbsent(value, k -> value); + } + + public ServiceDayTime getOrAdd(ServiceDayTime value) { + return serviceDayTimes.computeIfAbsent(value, k -> value); + } + } + + private final Cache cache = new Cache(); private final Map agencies = new HashMap<>(); private final Map calendars = new HashMap<>(); private final Map stops = new HashMap<>(); @@ -71,12 +89,13 @@ public GtfsScheduleBuilder addRoute(String id, String agencyId, String shortName return this; } - public GtfsScheduleBuilder addCalendar(String id, EnumSet serviceDays, LocalDate startDate, LocalDate endDate) { + public GtfsScheduleBuilder addCalendar(String id, EnumSet serviceDays, LocalDate startDate, + LocalDate endDate) { if (calendars.containsKey(id)) { throw new IllegalArgumentException("Calendar " + id + " already exists"); } log.debug("Adding calendar {}", id); - calendars.put(id, new Calendar(id, serviceDays, startDate, endDate)); + calendars.put(id, new Calendar(id, serviceDays, cache.getOrAdd(startDate), cache.getOrAdd(endDate))); return this; } @@ -86,7 +105,7 @@ public GtfsScheduleBuilder addCalendarDate(String calendarId, LocalDate date, Ex throw new IllegalArgumentException("Calendar " + calendarId + " does not exist"); } log.debug("Adding calendar {}-{}", calendarId, date); - CalendarDate calendarDate = new CalendarDate(calendar, date, type); + CalendarDate calendarDate = new CalendarDate(calendar, cache.getOrAdd(date), type); calendar.addCalendarDate(calendarDate); return this; } @@ -110,7 +129,8 @@ public GtfsScheduleBuilder addTrip(String id, String routeId, String serviceId) return this; } - public GtfsScheduleBuilder addStopTime(String tripId, String stopId, ServiceDayTime arrival, ServiceDayTime departure) { + public GtfsScheduleBuilder addStopTime(String tripId, String stopId, ServiceDayTime arrival, + ServiceDayTime departure) { Trip trip = trips.get(tripId); if (trip == null) { throw new IllegalArgumentException("Trip " + tripId + " does not exist"); @@ -120,7 +140,7 @@ public GtfsScheduleBuilder addStopTime(String tripId, String stopId, ServiceDayT throw new IllegalArgumentException("Stop " + stopId + " does not exist"); } log.debug("Adding stop {} to trip {} ({}-{})", stopId, tripId, arrival, departure); - StopTime stopTime = new StopTime(stop, trip, arrival, departure); + StopTime stopTime = new StopTime(stop, trip, cache.getOrAdd(arrival), cache.getOrAdd(departure)); stop.addStopTime(stopTime); trip.addStopTime(stopTime); return this; diff --git a/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java b/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java index ea37c8b5..e6b29072 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java @@ -3,7 +3,6 @@ import lombok.EqualsAndHashCode; import lombok.Getter; - /** * Service day time *

@@ -14,7 +13,7 @@ */ @EqualsAndHashCode @Getter -public class ServiceDayTime implements Comparable { +public final class ServiceDayTime implements Comparable { private final int totalSeconds; public ServiceDayTime(int hours, int minutes, int seconds) {