diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..50387435 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index a55e7a17..79ee123c 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,5 @@ - \ No newline at end of file diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java index 8c924b59..ac61b7cc 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java @@ -8,18 +8,30 @@ */ @RequiredArgsConstructor @Getter -public enum GtfsScheduleFile { - AGENCY("agency.txt"), - CALENDAR("calendar.txt"), - CALENDAR_DATES("calendar_dates.txt"), - // FARE_ATTRIBUTES("fare_attributes.txt"), - // FARE_RULES("fare_rules.txt"), - // FREQUENCIES("frequencies.txt"), - STOPS("stops.txt"), - ROUTES("routes.txt"), - // SHAPES("shapes.txt"), - TRIPS("trips.txt"), - STOP_TIMES("stop_times.txt"); +enum GtfsScheduleFile { + // FEED_INFO("feed_info.txt", Presence.OPTIONAL), + // ATTRIBUTIONS("attributions.txt", Presence.OPTIONAL), + AGENCY("agency.txt", Presence.REQUIRED), + CALENDAR("calendar.txt", Presence.CONDITIONALLY_REQUIRED), + CALENDAR_DATES("calendar_dates.txt", Presence.CONDITIONALLY_REQUIRED), + // FARE_ATTRIBUTES("fare_attributes.txt", Presence.OPTIONAL), + // FARE_RULES("fare_rules.txt", Presence.OPTIONAL), + // FREQUENCIES("frequencies.txt", Presence.OPTIONAL), + STOPS("stops.txt", Presence.REQUIRED), + ROUTES("routes.txt", Presence.REQUIRED), + // SHAPES("shapes.txt", Presence.OPTIONAL), + TRIPS("trips.txt", Presence.REQUIRED), + STOP_TIMES("stop_times.txt", Presence.REQUIRED); + // TRANSFERS("transfers.txt", Presence.OPTIONAL); private final String fileName; + private final Presence presence; + + public enum Presence { + REQUIRED, + OPTIONAL, + CONDITIONALLY_REQUIRED, + CONDITIONALLY_FORBIDDEN, + RECOMMENDED + } } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java index 16610e5a..e3ff25cd 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java @@ -85,10 +85,8 @@ private void parseStop(CSVRecord record) { } private void parseRoute(CSVRecord record) { - // 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); + record.get("route_long_name"), RouteType.parse(record.get("route_type"))); } private void parseTrips(CSVRecord record) { diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java index 86ce17b6..d8aa3908 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java @@ -10,10 +10,7 @@ import org.apache.commons.io.ByteOrderMark; import org.apache.commons.io.input.BOMInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; +import java.io.*; import java.nio.charset.StandardCharsets; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -44,8 +41,8 @@ private static void readFromDirectory(File directory, GtfsScheduleParser parser) if (csvFile.exists()) { log.info("Reading GTFS CSV file: {}", csvFile.getAbsolutePath()); readCsvFile(csvFile, parser, fileType); - } else { - log.warn("GTFS CSV file {} not found", csvFile.getAbsolutePath()); + } else if (fileType.getPresence() == GtfsScheduleFile.Presence.REQUIRED) { + throw new FileNotFoundException("Required GTFS CSV file" + csvFile.getAbsolutePath() + " not found"); } } } @@ -63,8 +60,9 @@ private static void readFromZip(File zipFile, GtfsScheduleParser parser) throws .get(), StandardCharsets.UTF_8)) { readCsvRecords(reader, parser, fileType); } - } else { - log.warn("GTFS file {} not found in ZIP", fileType.getFileName()); + } else if (fileType.getPresence() == GtfsScheduleFile.Presence.REQUIRED) { + throw new FileNotFoundException( + "Required GTFS CSV file" + fileType.getFileName() + " not found in ZIP"); } } } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/type/DefaultRouteType.java b/src/main/java/ch/naviqore/gtfs/schedule/type/DefaultRouteType.java new file mode 100644 index 00000000..a97d1157 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/DefaultRouteType.java @@ -0,0 +1,38 @@ +package ch.naviqore.gtfs.schedule.type; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public enum DefaultRouteType implements 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 code; + private final String description; + + public static DefaultRouteType parse(String code) { + return parse(Integer.parseInt(code)); + } + + public static DefaultRouteType parse(int code) { + for (DefaultRouteType type : DefaultRouteType.values()) { + if (type.code == code) { + return type; + } + } + throw new IllegalArgumentException("No default route type with code " + code + " found"); + } +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/type/HierarchicalVehicleType.java b/src/main/java/ch/naviqore/gtfs/schedule/type/HierarchicalVehicleType.java new file mode 100644 index 00000000..d6f43562 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/HierarchicalVehicleType.java @@ -0,0 +1,116 @@ +package ch.naviqore.gtfs.schedule.type; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public enum HierarchicalVehicleType implements RouteType { + RAILWAY_SERVICE(100, "Railway Service"), + HIGH_SPEED_RAIL_SERVICE(101, "High Speed Rail Service"), + LONG_DISTANCE_TRAINS(102, "Long Distance Trains"), + INTER_REGIONAL_RAIL_SERVICE(103, "Inter Regional Rail Service"), + CAR_TRANSPORT_RAIL_SERVICE(104, "Car Transport Rail Service"), + SLEEPER_RAIL_SERVICE(105, "Sleeper Rail Service"), + REGIONAL_RAIL_SERVICE(106, "Regional Rail Service"), + TOURIST_RAILWAY_SERVICE(107, "Tourist Railway Service"), + RAIL_SHUTTLE_WITHIN_COMPLEX(108, "Rail Shuttle (Within Complex)"), + SUBURBAN_RAILWAY(109, "Suburban Railway"), + REPLACEMENT_RAIL_SERVICE(110, "Replacement Rail Service"), + SPECIAL_RAIL_SERVICE(111, "Special Rail Service"), + LORRY_TRANSPORT_RAIL_SERVICE(112, "Lorry Transport Rail Service"), + ALL_RAIL_SERVICES(113, "All Rail Services"), + CROSS_COUNTRY_RAIL_SERVICE(114, "Cross-Country Rail Service"), + VEHICLE_TRANSPORT_RAIL_SERVICE(115, "Vehicle Transport Rail Service"), + RACK_AND_PINION_RAILWAY(116, "Rack and Pinion Railway"), + ADDITIONAL_RAIL_SERVICE(117, "Additional Rail Service"), + + COACH_SERVICE(200, "Coach Service"), + INTERNATIONAL_COACH_SERVICE(201, "International Coach Service"), + NATIONAL_COACH_SERVICE(202, "National Coach Service"), + SHUTTLE_COACH_SERVICE(203, "Shuttle Coach Service"), + REGIONAL_COACH_SERVICE(204, "Regional Coach Service"), + SPECIAL_COACH_SERVICE(205, "Special Coach Service"), + SIGHTSEEING_COACH_SERVICE(206, "Sightseeing Coach Service"), + TOURIST_COACH_SERVICE(207, "Tourist Coach Service"), + COMMUTER_COACH_SERVICE(208, "Commuter Coach Service"), + ALL_COACH_SERVICES(209, "All Coach Services"), + + URBAN_RAILWAY_SERVICE(400, "Urban Railway Service"), + METRO_SERVICE(401, "Metro Service"), + UNDERGROUND_SERVICE(402, "Underground Service"), + ALL_URBAN_RAILWAY_SERVICES(404, "All Urban Railway Services"), + MONORAIL(405, "Monorail"), + + BUS_SERVICE(700, "Bus Service"), + REGIONAL_BUS_SERVICE(701, "Regional Bus Service"), + EXPRESS_BUS_SERVICE(702, "Express Bus Service"), + LOCAL_BUS_SERVICE(704, "Local Bus Service"), + NIGHT_BUS_SERVICE(705, "Night Bus Service"), + POST_BUS_SERVICE(706, "Post Bus Service"), + SPECIAL_NEEDS_BUS(707, "Special Needs Bus"), + MOBILITY_BUS_SERVICE(708, "Mobility Bus Service"), + MOBILITY_BUS_FOR_REGISTERED_DISABLED(709, "Mobility Bus for Registered Disabled"), + SIGHTSEEING_BUS(710, "Sightseeing Bus"), + SHUTTLE_BUS(711, "Shuttle Bus"), + SCHOOL_BUS(712, "School Bus"), + SCHOOL_AND_PUBLIC_SERVICE_BUS(713, "School and Public Service Bus"), + RAIL_REPLACEMENT_BUS_SERVICE(714, "Rail Replacement Bus Service"), + DEMAND_AND_RESPONSE_BUS_SERVICE(715, "Demand and Response Bus Service"), + ALL_BUS_SERVICES(716, "All Bus Services"), + + TROLLEYBUS_SERVICE(800, "Trolleybus Service"), + + TRAM_SERVICE(900, "Tram Service"), + CITY_TRAM_SERVICE(901, "City Tram Service"), + LOCAL_TRAM_SERVICE(902, "Local Tram Service"), + REGIONAL_TRAM_SERVICE(903, "Regional Tram Service"), + SIGHTSEEING_TRAM_SERVICE(904, "Sightseeing Tram Service"), + SHUTTLE_TRAM_SERVICE(905, "Shuttle Tram Service"), + ALL_TRAM_SERVICES(906, "All Tram Services"), + + WATER_TRANSPORT_SERVICE(1000, "Water Transport Service"), + AIR_SERVICE(1100, "Air Service"), + + FERRY_SERVICE(1200, "Ferry Service"), + + AERIAL_LIFT_SERVICE(1300, "Aerial Lift Service"), + TELECABIN_SERVICE(1301, "Telecabin Service"), + CABLE_CAR_SERVICE(1302, "Cable Car Service"), + ELEVATOR_SERVICE(1303, "Elevator Service"), + CHAIR_LIFT_SERVICE(1304, "Chair Lift Service"), + DRAG_LIFT_SERVICE(1305, "Drag Lift Service"), + SMALL_TELECABIN_SERVICE(1306, "Small Telecabin Service"), + ALL_TELECABIN_SERVICES(1307, "All Telecabin Services"), + + FUNICULAR_SERVICE(1400, "Funicular Service"), + + TAXI_SERVICE(1500, "Taxi Service"), + COMMUNAL_TAXI_SERVICE(1501, "Communal Taxi Service"), + WATER_TAXI_SERVICE(1502, "Water Taxi Service"), + RAIL_TAXI_SERVICE(1503, "Rail Taxi Service"), + BIKE_TAXI_SERVICE(1504, "Bike Taxi Service"), + LICENSED_TAXI_SERVICE(1505, "Licensed Taxi Service"), + PRIVATE_HIRE_SERVICE_VEHICLE(1506, "Private Hire Service Vehicle"), + ALL_TAXI_SERVICES(1507, "All Taxi Services"), + + MISCELLANEOUS_SERVICE(1700, "Miscellaneous Service"), + HORSE_DRAWN_CARRIAGE(1702, "Horse-drawn Carriage"); + + private final int code; + private final String description; + + public static HierarchicalVehicleType parse(String code) { + return parse(Integer.parseInt(code)); + } + + public static HierarchicalVehicleType parse(int code) { + for (HierarchicalVehicleType type : HierarchicalVehicleType.values()) { + if (type.code == code) { + return type; + } + } + throw new IllegalArgumentException("No hierarchical vehicle type with code " + code + " found"); + } +} diff --git a/src/main/java/ch/naviqore/gtfs/schedule/type/RouteType.java b/src/main/java/ch/naviqore/gtfs/schedule/type/RouteType.java index 35432d82..043fe6ee 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/type/RouteType.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/RouteType.java @@ -1,38 +1,46 @@ package ch.naviqore.gtfs.schedule.type; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; +/** + * Provides a unified approach to handling different modes of transportation of routes within a GTFS feed. Implementing + * this interface allows for retrieval of both unique identifier codes and descriptions of transportation route types. + * + * @author munterfi + */ +public interface RouteType { -@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."); + /** + * Parses a string to the corresponding RouteType: Either default GTFS route type or Hierarchical Vehicle Type + * (HVT). + * + * @param code the string code to parse + * @return the corresponding RouteType + * @throws NumberFormatException if the code is not a valid integer + * @throws IllegalArgumentException if the code is negative or invalid + */ - private final int value; - private final String description; - - public static RouteType parse(String value) { - return parse(Integer.parseInt(value)); + static RouteType parse(String code) { + return parse(Integer.parseInt(code)); } - public static RouteType parse(int value) { - for (RouteType type : RouteType.values()) { - if (type.value == value) { - return type; - } + static RouteType parse(int code) { + if (code < 0) { + throw new IllegalArgumentException("Invalid negative RouteType code: " + code); + } + if (code <= 12) { + return DefaultRouteType.parse(code); + } else { + return HierarchicalVehicleType.parse(code); } - throw new IllegalArgumentException("No route type with value " + value + " found"); } + + /** + * Retrieves the code associated with the route type. + */ + int getCode(); + + /** + * Retrieves a description of the route type. + */ + String getDescription(); + } 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 e6b29072..dc1238ec 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java @@ -16,6 +16,10 @@ public final class ServiceDayTime implements Comparable { private final int totalSeconds; + public ServiceDayTime(int seconds) { + this.totalSeconds = seconds; + } + public ServiceDayTime(int hours, int minutes, int seconds) { this.totalSeconds = seconds + 60 * minutes + 3600 * hours; } diff --git a/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java b/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java new file mode 100644 index 00000000..a1576642 --- /dev/null +++ b/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java @@ -0,0 +1,105 @@ +package ch.naviqore.raptor; + +import ch.naviqore.gtfs.schedule.model.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Splits the routes of a GTFS schedule into sub-routes. In a GTFS schedule, a route can have multiple trips with + * different stop sequences. This class groups trips with the same stop sequence into sub-routes and assigns them the + * parent route. + * + * @author munterfi + */ +@Log4j2 +public class GtfsRoutePartitioner { + private final Map> subRoutes = new HashMap<>(); + + public GtfsRoutePartitioner(GtfsSchedule schedule) { + log.info("Partitioning GTFS schedule with {} routes into sub-routes", schedule.getRoutes().size()); + schedule.getRoutes().values().forEach(this::processRoute); + log.info("Found {} sub-routes in schedule", subRoutes.values().stream().mapToInt(Map::size).sum()); + } + + private void processRoute(Route route) { + Map sequenceKeyToSubRoute = new HashMap<>(); + route.getTrips().forEach(trip -> { + String key = generateStopSequenceKey(trip); + SubRoute subRoute = sequenceKeyToSubRoute.computeIfAbsent(key, + s -> new SubRoute(String.format("%s_sr%d", route.getId(), sequenceKeyToSubRoute.size() + 1), route, + key, extractStopSequence(trip))); + subRoute.addTrip(trip); + }); + subRoutes.put(route, sequenceKeyToSubRoute); + log.debug("Route {} split into {} sub-routes", route.getId(), sequenceKeyToSubRoute.size()); + } + + private String generateStopSequenceKey(Trip trip) { + return trip.getStopTimes().stream().map(t -> t.stop().getId()).collect(Collectors.joining("-")); + } + + private List extractStopSequence(Trip trip) { + List sequence = new ArrayList<>(); + for (StopTime stopTime : trip.getStopTimes()) { + sequence.add(stopTime.stop()); + } + return sequence; + } + + public List getSubRoutes(Route route) { + Map currentSubRoutes = subRoutes.get(route); + if (currentSubRoutes == null) { + throw new IllegalArgumentException("Route " + route.getId() + " not found in schedule"); + } + return new ArrayList<>(currentSubRoutes.values()); + } + + public SubRoute getSubRoute(Trip trip) { + Map currentSubRoutes = subRoutes.get(trip.getRoute()); + if (currentSubRoutes == null) { + throw new IllegalArgumentException("Trip " + trip.getId() + " not found in schedule"); + } + String key = generateStopSequenceKey(trip); + return currentSubRoutes.get(key); + } + + /** + * A sub-route belongs to a route, but has a unique stop sequence. + */ + @RequiredArgsConstructor + @Getter + public static class SubRoute { + private final String id; + private final Route route; + @Getter(AccessLevel.NONE) + private final String stopSequenceKey; + private final List stopsSequence; + private final List trips = new ArrayList<>(); + + private 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 = (SubRoute) obj; + return Objects.equals(this.id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + public String toString() { + return "SubRoute[" + "id=" + id + ", " + "route=" + route + ", " + "stopSequence=" + stopSequenceKey + ']'; + } + } +} diff --git a/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java b/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java index 3ecbdc13..2934678a 100644 --- a/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java +++ b/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java @@ -1,9 +1,11 @@ package ch.naviqore.raptor; -import ch.naviqore.gtfs.schedule.model.*; +import ch.naviqore.gtfs.schedule.model.GtfsSchedule; +import ch.naviqore.gtfs.schedule.model.Stop; +import ch.naviqore.gtfs.schedule.model.StopTime; +import ch.naviqore.gtfs.schedule.model.Trip; import ch.naviqore.raptor.model.Raptor; import ch.naviqore.raptor.model.RaptorBuilder; -import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import java.time.LocalDate; @@ -16,34 +18,39 @@ * * @author munterfi */ -@RequiredArgsConstructor @Log4j2 public class GtfsToRaptorConverter { + private final Set subRoutes = new HashSet<>(); private final Set stops = new HashSet<>(); - private final Set routes = new HashSet<>(); - private final RaptorBuilder builder; + private final RaptorBuilder builder = Raptor.builder(); + private final GtfsRoutePartitioner partitioner; + private final GtfsSchedule schedule; - public Raptor convert(GtfsSchedule schedule, LocalDate date) { + public GtfsToRaptorConverter(GtfsSchedule schedule) { + this.partitioner = new GtfsRoutePartitioner(schedule); + this.schedule = schedule; + } + + public Raptor convert(LocalDate date) { List activeTrips = schedule.getActiveTrips(date); log.info("Converting {} active trips from GTFS schedule to Raptor model", activeTrips.size()); for (Trip trip : activeTrips) { - Route route = trip.getRoute(); - if (!routes.contains(route)) { - routes.add(route); - builder.addRoute(route.getId()); - // TODO: Add test for consistency of route stops. Since in GTFS are defined per trip, but Raptor - // builder expects them to be the same for all trips of a route. + // Route route = trip.getRoute(); + GtfsRoutePartitioner.SubRoute subRoute = partitioner.getSubRoute(trip); + if (!subRoutes.contains(subRoute)) { + subRoutes.add(subRoute); + builder.addRoute(subRoute.getId()); for (StopTime stopTime : trip.getStopTimes()) { if (!stops.contains(stopTime.stop())) { stops.add(stopTime.stop()); builder.addStop(stopTime.stop().getId()); } - builder.addRouteStop(stopTime.stop().getId(), route.getId()); + builder.addRouteStop(stopTime.stop().getId(), subRoute.getId()); } } for (StopTime stopTime : trip.getStopTimes()) { - builder.addStopTime(stopTime.stop().getId(), route.getId(), stopTime.arrival().getTotalSeconds(), + builder.addStopTime(stopTime.stop().getId(), subRoute.getId(), stopTime.arrival().getTotalSeconds(), stopTime.departure().getTotalSeconds()); } } diff --git a/src/main/java/ch/naviqore/raptor/model/RouteTraversal.java b/src/main/java/ch/naviqore/raptor/model/RouteTraversal.java index adfcb9ca..3b2c3935 100644 --- a/src/main/java/ch/naviqore/raptor/model/RouteTraversal.java +++ b/src/main/java/ch/naviqore/raptor/model/RouteTraversal.java @@ -8,5 +8,4 @@ * @param routeStops route stops */ public record RouteTraversal(StopTime[] stopTimes, Route[] routes, RouteStop[] routeStops) { - } diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java index 585e3ed9..85660ee2 100644 --- a/src/test/java/ch/naviqore/Benchmark.java +++ b/src/test/java/ch/naviqore/Benchmark.java @@ -45,7 +45,6 @@ public static void main(String[] args) throws IOException, InterruptedException RouteRequest[] requests = sampleRouteRequests(stopIds); RoutingResult[] results = processRequests(raptor, requests); writeResultsToCsv(results); - } private static GtfsSchedule initializeSchedule() throws IOException, InterruptedException { @@ -56,7 +55,7 @@ private static GtfsSchedule initializeSchedule() throws IOException, Interrupted } private static Raptor initializeRaptor(GtfsSchedule schedule) throws InterruptedException { - Raptor raptor = new GtfsToRaptorConverter(Raptor.builder()).convert(schedule, DATE); + Raptor raptor = new GtfsToRaptorConverter(schedule).convert(DATE); manageResources(); return raptor; } diff --git a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java index 59d03da2..e0d92641 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java @@ -28,14 +28,14 @@ void setUp() { } @Test - void readFromZipFile(@TempDir Path tempDir) throws IOException { + void shouldReadFromZipFile(@TempDir Path tempDir) throws IOException { File zipFile = GtfsScheduleTestData.prepareZipDataset(tempDir); GtfsSchedule schedule = gtfsScheduleReader.read(zipFile.getAbsolutePath()); assertScheduleSizes(schedule); } @Test - void readFromDirectory(@TempDir Path tempDir) throws IOException { + void shouldReadFromDirectory(@TempDir Path tempDir) throws IOException { File unzippedDir = GtfsScheduleTestData.prepareUnzippedDataset(tempDir); GtfsSchedule schedule = gtfsScheduleReader.read(unzippedDir.getAbsolutePath()); assertScheduleSizes(schedule); diff --git a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java index 3791dcf0..b8d9e993 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java @@ -1,53 +1,31 @@ package ch.naviqore.gtfs.schedule.model; -import ch.naviqore.gtfs.schedule.type.RouteType; import ch.naviqore.gtfs.schedule.type.ServiceDayTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; -import java.time.DayOfWeek; -import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.Month; -import java.util.EnumSet; +import java.util.List; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +@ExtendWith(GtfsScheduleTestExtension.class) class GtfsScheduleTest { - private static final LocalDate START_DATE = LocalDate.of(2024, Month.APRIL, 1); - private static final LocalDate END_DATE = START_DATE.plusMonths(1); - private static final LocalDateTime WEEKDAY_8_AM = LocalDateTime.of(2024, Month.APRIL, 26, 8, 0); - private static final LocalDateTime WEEKDAY_9_AM = WEEKDAY_8_AM.plusHours(1); - private static final LocalDateTime SATURDAY_9_AM = LocalDateTime.of(2024, Month.APRIL, 27, 9, 0); - private GtfsSchedule schedule; @BeforeEach - void setUp() { - schedule = GtfsSchedule.builder() - .addAgency("agency1", "City Transit", "http://citytransit.example.com", "Europe/Zurich") - .addStop("stop1", "Main Station", 47.3769, 8.5417) - .addStop("stop2", "Central Park", 47.3779, 8.5407) - .addStop("stop3", "Hill Valley", 47.3780, 8.5390) - .addStop("stop4", "East Side", 47.3785, 8.5350) - .addStop("stop5", "West End", 47.3750, 8.5300) - .addRoute("route1", "agency1", "101", "Main Line", RouteType.BUS) - .addRoute("route2", "agency1", "102", "Cross Town", RouteType.BUS) - .addRoute("route3", "agency1", "103", "Circulator", RouteType.BUS) - .addCalendar("weekdays", EnumSet.range(DayOfWeek.MONDAY, DayOfWeek.FRIDAY), START_DATE, END_DATE) - .addCalendar("weekends", EnumSet.of(DayOfWeek.SATURDAY), START_DATE, END_DATE) - .addTrip("trip1", "route1", "weekdays") - .addTrip("trip2", "route2", "weekdays") - .addTrip("trip3", "route3", "weekends") - .addStopTime("trip1", "stop1", new ServiceDayTime(8, 0, 0), new ServiceDayTime(8, 5, 0)) - .addStopTime("trip1", "stop2", new ServiceDayTime(8, 10, 0), new ServiceDayTime(8, 15, 0)) - .addStopTime("trip2", "stop3", new ServiceDayTime(9, 0, 0), new ServiceDayTime(9, 5, 0)) - .addStopTime("trip2", "stop4", new ServiceDayTime(9, 10, 0), new ServiceDayTime(9, 15, 0)) - .addStopTime("trip3", "stop5", new ServiceDayTime(10, 0, 0), new ServiceDayTime(10, 5, 0)) - .addStopTime("trip3", "stop1", new ServiceDayTime(10, 10, 0), new ServiceDayTime(10, 15, 0)) + void setUp(GtfsScheduleTestBuilder builder) { + schedule = builder.withAddAgency() + .withAddCalendars() + .withAddCalendarDates() + .withAddInterCity() + .withAddUnderground() + .withAddBus() .build(); } @@ -56,7 +34,7 @@ class Builder { @Test void shouldCorrectlyCountAgencies() { - assertThat(schedule.getAgencies()).hasSize(1); + assertThat(schedule.getAgencies()).hasSize(2); } @Test @@ -66,12 +44,12 @@ void shouldCorrectlyCountRoutes() { @Test void shouldCorrectlyCountStops() { - assertThat(schedule.getStops()).hasSize(5); + assertThat(schedule.getStops()).hasSize(9); } @Test void shouldCorrectlyCountTrips() { - assertThat(schedule.getTrips()).hasSize(3); + assertThat(schedule.getTrips()).hasSize(671); } @Test @@ -84,34 +62,127 @@ void shouldCorrectlyCountCalendars() { class NearestStops { @Test - void shouldFindStopsWithin500Meters() { - assertThat(schedule.getNearestStops(47.3769, 8.5417, 500)).hasSize(3) + void shouldFindStopWithin1Meter() { + assertThat(schedule.getNearestStops(47.5, 8.5, 1)).hasSize(1).extracting("id").containsOnly("s2"); + } + + @Test + void shouldFindStopsWithin10000Meters() { + assertThat(schedule.getNearestStops(47.5, 8.5, 10000)).hasSize(3) + .extracting("id") + .containsOnly("u6", "s2", "u3"); + } + + @Test + void shouldFindAllStops() { + assertThat(schedule.getNearestStops(47.5, 8.5, Integer.MAX_VALUE)).hasSize(9) .extracting("id") - .containsOnly("stop1", "stop2", "stop3"); + .containsOnly("s1", "s2", "s3", "u1", "u2", "u3", "u4", "u5", "u6"); } @Test void shouldFindNoStopsWhenNoneAreCloseEnough() { - assertThat(schedule.getNearestStops(47.3800, 8.5500, 100)).isEmpty(); + assertThat(schedule.getNearestStops(47.6, 8.5, 100)).isEmpty(); } } @Nested class NextDepartures { + private static final String STOP_ID = "s2"; + private static final int LIMIT = 5; + + private static void assertWeekendAndHoliday(List departures) { + // assert departures times are correct + List expectedDepartures = List.of(ServiceDayTime.parse("08:15:00"), + ServiceDayTime.parse("08:15:00"), ServiceDayTime.parse("09:15:00"), + ServiceDayTime.parse("09:15:00"), ServiceDayTime.parse("10:15:00")); + assertThat(departures).hasSize(LIMIT) + .extracting(StopTime::departure) + .containsExactlyElementsOf(expectedDepartures); + + // assert trips are correct + List expectedTripIds = List.of("route1_we_f_4", "route1_we_r_4", "route1_we_f_5", "route1_we_r_5", + "route1_we_f_6"); + List tripIds = departures.stream().map(stopTime -> stopTime.trip().getId()).toList(); + assertThat(tripIds).containsExactlyElementsOf(expectedTripIds); + + // assert routes are correct + Set expectedRouteIds = Set.of("route1"); + List routeIds = departures.stream().map(stopTime -> stopTime.trip().getRoute().getId()).toList(); + assertThat(routeIds).allMatch(expectedRouteIds::contains); + } + @Test void shouldReturnNextDeparturesOnWeekday() { - assertThat(schedule.getNextDepartures("stop1", WEEKDAY_8_AM, Integer.MAX_VALUE)).hasSize(1); + List departures = schedule.getNextDepartures(STOP_ID, + GtfsScheduleTestBuilder.Moments.WEEKDAY_8_AM, LIMIT); + + // assert departures times are correct + List expectedDepartures = List.of(ServiceDayTime.parse("08:00:00"), + ServiceDayTime.parse("08:03:00"), ServiceDayTime.parse("08:09:00"), + ServiceDayTime.parse("08:09:00"), ServiceDayTime.parse("08:15:00")); + assertThat(departures).hasSize(LIMIT) + .extracting(StopTime::departure) + .containsExactlyElementsOf(expectedDepartures); + + // assert trips are correct + List expectedTripIds = List.of("route3_wd_f_16", "route3_wd_r_17", "route3_wd_f_17", + "route3_wd_r_17", "route1_wd_f_7"); + List tripIds = departures.stream().map(stopTime -> stopTime.trip().getId()).toList(); + assertThat(tripIds).containsExactlyElementsOf(expectedTripIds); + + // assert routes are correct + Set expectedRouteIds = Set.of("route1", "route3"); + List routeIds = departures.stream().map(stopTime -> stopTime.trip().getRoute().getId()).toList(); + assertThat(routeIds).allMatch(expectedRouteIds::contains); + } + + @Test + void shouldReturnNextDeparturesOnWeekend() { + List departures = schedule.getNextDepartures(STOP_ID, + GtfsScheduleTestBuilder.Moments.WEEKEND_8_AM, LIMIT); + + assertWeekendAndHoliday(departures); } @Test - void shouldReturnNoNextDeparturesOnWeekday() { - assertThat(schedule.getNextDepartures("stop1", WEEKDAY_9_AM, Integer.MAX_VALUE)).isEmpty(); + void shouldReturnNextDeparturesOnHoliday() { + List departures = schedule.getNextDepartures(STOP_ID, + GtfsScheduleTestBuilder.Moments.HOLIDAY.atTime(8, 0), LIMIT); + + assertWeekendAndHoliday(departures); } @Test - void shouldReturnNextDeparturesOnSaturday() { - assertThat(schedule.getNextDepartures("stop1", SATURDAY_9_AM, Integer.MAX_VALUE)).hasSize(1); + void shouldReturnNextDeparturesAfterMidnight() { + List departures = schedule.getNextDepartures(STOP_ID, + GtfsScheduleTestBuilder.Moments.WEEKDAY_12_PM, LIMIT); + + // assert departures times are correct + List expectedDepartures = List.of(ServiceDayTime.parse("24:00:00"), + ServiceDayTime.parse("24:03:00"), ServiceDayTime.parse("24:09:00"), + ServiceDayTime.parse("24:09:00"), ServiceDayTime.parse("24:15:00")); + assertThat(departures).hasSize(LIMIT) + .extracting(StopTime::departure) + .containsExactlyElementsOf(expectedDepartures); + + // assert trips are correct + List expectedTripIds = List.of("route3_wd_f_80", "route3_wd_r_81", "route3_wd_f_81", + "route3_wd_r_81", "route1_wd_f_39"); + List tripIds = departures.stream().map(stopTime -> stopTime.trip().getId()).toList(); + assertThat(tripIds).containsExactlyElementsOf(expectedTripIds); + + // assert routes are correct + Set expectedRouteIds = Set.of("route1", "route3"); + List routeIds = departures.stream().map(stopTime -> stopTime.trip().getRoute().getId()).toList(); + assertThat(routeIds).allMatch(expectedRouteIds::contains); + } + + @Test + void shouldReturnNoNextDeparturesOnNoServiceDay() { + assertThat(schedule.getNextDepartures(STOP_ID, GtfsScheduleTestBuilder.Moments.NO_SERVICE.atTime(8, 0), + Integer.MAX_VALUE)).isEmpty(); } @Test @@ -124,23 +195,46 @@ void shouldReturnNoDeparturesFromUnknownStop() { @Nested class ActiveTrips { + private static void assertWeekendAndHoliday(List activeTrips) { + assertThat(activeTrips).hasSize(168) + .extracting(trip -> trip.getRoute().getId()) + .containsAll(Set.of("route1", "route2")); + } + @Test void shouldReturnActiveTripsOnWeekday() { - assertThat(schedule.getActiveTrips(WEEKDAY_8_AM.toLocalDate())).hasSize(2) - .extracting("id") - .containsOnly("trip1", "trip2"); + List activeTrips = schedule.getActiveTrips( + GtfsScheduleTestBuilder.Moments.WEEKDAY_8_AM.toLocalDate()); + + assertThat(activeTrips).hasSize(503) + .extracting(trip -> trip.getRoute().getId()) + .containsAll(Set.of("route1", "route2", "route3")); } @Test void shouldReturnActiveTripsOnWeekend() { - assertThat(schedule.getActiveTrips(SATURDAY_9_AM.toLocalDate())).hasSize(1) - .extracting("id") - .containsOnly("trip3"); + List activeTrips = schedule.getActiveTrips( + GtfsScheduleTestBuilder.Moments.WEEKEND_8_AM.toLocalDate()); + + assertWeekendAndHoliday(activeTrips); + } + + @Test + void shouldReturnActiveTripsOnHoliday() { + List activeTrips = schedule.getActiveTrips(GtfsScheduleTestBuilder.Moments.HOLIDAY); + + assertWeekendAndHoliday(activeTrips); + } + + @Test + void shouldReturnNoActiveTripsForDaysOutsideValidity() { + assertThat(schedule.getActiveTrips(GtfsScheduleTestBuilder.Validity.PERIOD_START.minusDays(1))).isEmpty(); + assertThat(schedule.getActiveTrips(GtfsScheduleTestBuilder.Validity.PERIOD_END.plusDays(1))).isEmpty(); } @Test void shouldReturnNoActiveTripsForNonServiceDay() { - assertThat(schedule.getActiveTrips(END_DATE.plusMonths(1))).isEmpty(); + assertThat(schedule.getActiveTrips(GtfsScheduleTestBuilder.Moments.NO_SERVICE)).isEmpty(); } } } diff --git a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java new file mode 100644 index 00000000..c52d74db --- /dev/null +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java @@ -0,0 +1,182 @@ +package ch.naviqore.gtfs.schedule.model; + +import ch.naviqore.gtfs.schedule.type.*; +import lombok.NoArgsConstructor; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Month; +import java.util.*; + +/** + * Test builder to set up a GTFS schedule for testing purposes. + *

+ * Simple example schedule: + *

+ *            u1 ------------------ u2
+ *            |                   / |
+ *            |                /    |
+ *            |             /       |
+ * s1 ------- u6 ------- s2 ------- u3 ------- s3
+ *            |        /            |
+ *            |     /               |
+ *            |  /                  |
+ *            u5 ------------------ u4
+ * 
+ * Routes: + *
    + *
  • route1 - Everyday service, InterCity train from Other City to Different City via Main Station (s1, s2, s3)
  • + *
  • route2 - Everyday service, Underground system covering six stops (u1, u2, u3, u4, u5, u6)
  • + *
  • route3 - Weekday service, Bus between South-West, Main Station, and North-East (u5, s2, u2)
  • + *
+ * Stations: + *
    + *
  • s1 - Other City (47.5, 7.5)
  • + *
  • s2 - Main Station (47.5, 8.5)
  • + *
  • s3 - Different City (47.5, 9.5)
  • + *
+ * Underground: + *
    + *
  • u1 - North-West (47.6, 8.4)
  • + *
  • u2 - North-East (47.6, 8.6)
  • + *
  • u3 - East (47.5, 8.6)
  • + *
  • u4 - South-East (47.4, 8.6)
  • + *
  • u5 - South-West (47.4, 8.4)
  • + *
  • u6 - West (47.5, 8.4)
  • + *
+ * + * @author munterfi + */ +@NoArgsConstructor +public class GtfsScheduleTestBuilder { + + private static final int NO_HEADWAY = -1; + private static final Map STOPS = Map.of("s1", new Stop("s1", "Other City", 47.5, 7.5), "s2", + new Stop("s2", "Main Station", 47.5, 8.5), "s3", new Stop("s3", "Different City", 47.5, 9.5), "u1", + new Stop("u1", "North-West", 47.6, 8.4), "u2", new Stop("u2", "North-East", 47.6, 8.6), "u3", + new Stop("u3", "East", 47.5, 8.6), "u4", new Stop("u4", "South-East", 47.4, 8.6), "u5", + new Stop("u5", "South-West", 47.4, 8.4), "u6", new Stop("u6", "West", 47.5, 8.4)); + private static final List ROUTES = List.of( + new Route("route1", "agency1", "IC", HierarchicalVehicleType.LONG_DISTANCE_TRAINS, 30, 60, 10, 60, 5, + List.of("s1", "s2", "s3")), + new Route("route2", "agency2", "UNDERGROUND", HierarchicalVehicleType.SUBURBAN_RAILWAY, 5, 10, 10, 3, 1, + List.of("u1", "u2", "u3", "u4", "u5", "u6")), + new Route("route3", "agency2", "BUS", DefaultRouteType.BUS, 15, NO_HEADWAY, 3, 5, 1, + List.of("u5", "s2", "s2"))); + private final Set addedStops = new HashSet<>(); + private final GtfsScheduleBuilder builder = GtfsSchedule.builder(); + + public GtfsScheduleTestBuilder withAddAgency() { + builder.addAgency("agency1", "National Transit", "https://nationaltransit.example.com", "Europe/Zurich"); + builder.addAgency("agency2", "City Transit", "https://citytransit.example.com", "Europe/Zurich"); + return this; + } + + public GtfsScheduleTestBuilder withAddCalendars() { + builder.addCalendar("weekdays", EnumSet.range(DayOfWeek.MONDAY, DayOfWeek.FRIDAY), Validity.PERIOD_START, + Validity.PERIOD_END) + .addCalendar("weekends", EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY), Validity.PERIOD_START, + Validity.PERIOD_END); + return this; + } + + public GtfsScheduleTestBuilder withAddCalendarDates() { + // change service to sunday + builder.addCalendarDate("weekdays", Moments.HOLIDAY, ExceptionType.REMOVED); + builder.addCalendarDate("weekends", Moments.HOLIDAY, ExceptionType.ADDED); + // no service + builder.addCalendarDate("weekdays", Moments.NO_SERVICE, ExceptionType.REMOVED); + return this; + } + + public GtfsScheduleTestBuilder withAddInterCity() { + addRoute(ROUTES.getFirst(), true, true); + return this; + } + + public GtfsScheduleTestBuilder withAddUnderground() { + addRoute(ROUTES.get(1), true, false); + return this; + } + + public GtfsScheduleTestBuilder withAddBus() { + addRoute(ROUTES.get(2), false, true); + return this; + } + + public GtfsSchedule build() { + return builder.build(); + } + + private void addRoute(Route route, boolean everydayService, boolean bidirectional) { + builder.addRoute(route.id, route.agencyId, route.name, route.name + "long", route.routeType); + addStops(route); + addTrips(route, true, false); + if (everydayService) { + addTrips(route, false, false); + } + if (bidirectional) { + addTrips(route, true, true); + if (everydayService) { + addTrips(route, false, true); + } + } + } + + private void addTrips(Route route, boolean weekday, boolean reverse) { + final int travelTime = route.travelTime * 60; + final int dwellTime = route.dwellTime * 60; + final int headway = weekday ? route.headwayWeekday * 60 : route.headwayWeekend * 60; + final List routeStops = new ArrayList<>(route.stops); + String weekdayPostfix = weekday ? "wd" : "we"; + String directionPostfix = "f"; + if (reverse) { + Collections.reverse(routeStops); + directionPostfix = "r"; + } + int tripCount = 0; + for (int tripDepartureTime = Validity.SERVICE_DAY_START.getTotalSeconds() + route.offset * 60; tripDepartureTime <= Validity.SERVICE_DAY_END.getTotalSeconds(); tripDepartureTime += headway) { + String tripId = String.format("%s_%s_%s_%s", route.id, weekdayPostfix, directionPostfix, ++tripCount); + builder.addTrip(tripId, route.id, weekday ? "weekdays" : "weekends"); + int departureTime = tripDepartureTime; + for (String stopId : routeStops) { + builder.addStopTime(tripId, stopId, new ServiceDayTime(departureTime - dwellTime), + new ServiceDayTime(departureTime)); + departureTime += travelTime + dwellTime; + } + } + } + + private void addStops(Route route) { + for (String stopId : route.stops) { + if (!addedStops.contains(stopId)) { + Stop stop = STOPS.get(stopId); + builder.addStop(stop.id, stop.id, stop.lat, stop.lon); + addedStops.add(stopId); + } + } + } + + record Stop(String id, String name, double lat, double lon) { + } + + record Route(String id, String agencyId, String name, RouteType routeType, int headwayWeekday, int headwayWeekend, + int offset, int travelTime, int dwellTime, List stops) { + } + + public static final class Validity { + public static final ServiceDayTime SERVICE_DAY_START = new ServiceDayTime(4, 0, 0); + public static final ServiceDayTime SERVICE_DAY_END = new ServiceDayTime(25, 0, 0); + public static final LocalDate PERIOD_START = LocalDate.of(2024, Month.JANUARY, 1); + public static final LocalDate PERIOD_END = LocalDate.of(2024, Month.DECEMBER, 31); + } + + public static final class Moments { + public static final LocalDateTime WEEKDAY_8_AM = LocalDateTime.of(2024, Month.APRIL, 26, 8, 0); + public static final LocalDateTime WEEKDAY_12_PM = LocalDateTime.of(2024, Month.APRIL, 26, 23, 59); + public static final LocalDateTime WEEKEND_8_AM = LocalDateTime.of(2024, Month.APRIL, 27, 8, 0); + public static final LocalDate NO_SERVICE = LocalDate.of(2024, Month.MAY, 1); + public static final LocalDate HOLIDAY = LocalDate.of(2024, Month.DECEMBER, 25); + } +} diff --git a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestExtension.java b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestExtension.java new file mode 100644 index 00000000..d3ca9ffe --- /dev/null +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestExtension.java @@ -0,0 +1,24 @@ +package ch.naviqore.gtfs.schedule.model; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * Extension for JUnit 5 tests for injecting GtfsScheduleTestBuilder instances. This extension allows test methods to + * receive a GtfsScheduleTestBuilder instance as a parameter. + * + * @author munterfi + */ +public class GtfsScheduleTestExtension implements ParameterResolver { + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return parameterContext.getParameter().getType().equals(GtfsScheduleTestBuilder.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return new GtfsScheduleTestBuilder(); + } +} diff --git a/src/test/java/ch/naviqore/raptor/GtfsRoutePartitionerTest.java b/src/test/java/ch/naviqore/raptor/GtfsRoutePartitionerTest.java new file mode 100644 index 00000000..858f092e --- /dev/null +++ b/src/test/java/ch/naviqore/raptor/GtfsRoutePartitionerTest.java @@ -0,0 +1,45 @@ +package ch.naviqore.raptor; + +import ch.naviqore.gtfs.schedule.model.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(GtfsScheduleTestExtension.class) +class GtfsRoutePartitionerTest { + + private GtfsSchedule schedule; + private GtfsRoutePartitioner partitioner; + + @BeforeEach + void setUp(GtfsScheduleTestBuilder builder) { + schedule = builder.withAddAgency() + .withAddCalendars() + .withAddCalendarDates() + .withAddInterCity() + .withAddUnderground() + .withAddBus() + .build(); + partitioner = new GtfsRoutePartitioner(schedule); + } + + @Test + void getSubRoutes() { + assertThat(partitioner.getSubRoutes(schedule.getRoutes().get("route1"))).as("SubRoutes").hasSize(2); + assertThat(partitioner.getSubRoutes(schedule.getRoutes().get("route2"))).as("SubRoutes").hasSize(1); + assertThat(partitioner.getSubRoutes(schedule.getRoutes().get("route3"))).as("SubRoutes").hasSize(2); + } + + @Test + void getSubRoute() { + for (Route route : schedule.getRoutes().values()) { + for (Trip trip : route.getTrips()) { + GtfsRoutePartitioner.SubRoute subRoute = partitioner.getSubRoute(trip); + assertThat(subRoute).as("SubRoute for trip ID " + trip.getId() + " in route " + route.getId()) + .isNotNull(); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/ch/naviqore/raptor/GtfsToRaptorConverterIT.java b/src/test/java/ch/naviqore/raptor/GtfsToRaptorConverterIT.java index a3e803bd..2f1dcbc4 100644 --- a/src/test/java/ch/naviqore/raptor/GtfsToRaptorConverterIT.java +++ b/src/test/java/ch/naviqore/raptor/GtfsToRaptorConverterIT.java @@ -13,6 +13,8 @@ import java.nio.file.Path; import java.time.LocalDate; +import static org.assertj.core.api.Assertions.assertThat; + class GtfsToRaptorConverterIT { private static final LocalDate DATE = LocalDate.of(2009, 4, 26); @@ -26,7 +28,8 @@ void setUp(@TempDir Path tempDir) throws IOException { @Test void shouldConvertGtfsScheduleToRaptor() { - GtfsToRaptorConverter mapper = new GtfsToRaptorConverter(Raptor.builder()); - Raptor raptor = mapper.convert(schedule, DATE); + GtfsToRaptorConverter mapper = new GtfsToRaptorConverter(schedule); + Raptor raptor = mapper.convert(DATE); + assertThat(raptor).isNotNull(); } } \ No newline at end of file