From e2fd4dad0ab71bad7304d3b923794af28d714574 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Sat, 13 Apr 2024 12:54:39 +0200 Subject: [PATCH] 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 + ']'; } + }