From 050da210954eec7346f5f4773ed0d50c2012604d Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Fri, 3 May 2024 12:03:50 +0200 Subject: [PATCH 1/8] ENH: NAV-14 - Add presence to GTFS file type - Introduce presence enum with levels: REQUIRED, OPTIONAL, CONDITIONALLY_REQUIRED, CONDITIONALLY_FORBIDDEN, RECOMMENDED. - Throw FileNotFoundException if a required file is not provided. --- .../gtfs/schedule/GtfsScheduleFile.java | 30 ++++++++++++------- .../gtfs/schedule/GtfsScheduleReader.java | 9 +++--- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java index 8c924b59..f4f442ed 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java @@ -9,17 +9,25 @@ @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"); + // 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/GtfsScheduleReader.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java index 86ce17b6..9f364bd0 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java @@ -12,6 +12,7 @@ import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; @@ -44,8 +45,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 +64,8 @@ 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"); } } } From 6273663e31d3602acd6b628d4732f1693114c6e7 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Fri, 3 May 2024 16:15:04 +0200 Subject: [PATCH 2/8] ENH: NAV-14 - Add support for extended route types - Introduce interface for route type. - Implement with two enums: DefaultRouteType and HierarchicalVehicleType - Parser decides depending on code which type to choose. --- .../gtfs/schedule/GtfsScheduleParser.java | 4 +- .../gtfs/schedule/type/DefaultRouteType.java | 38 ++++++ .../type/HierarchicalVehicleType.java | 117 ++++++++++++++++++ .../gtfs/schedule/type/RouteType.java | 66 +++++----- .../naviqore/raptor/model/RouteTraversal.java | 1 - .../gtfs/schedule/model/GtfsScheduleTest.java | 8 +- 6 files changed, 197 insertions(+), 37 deletions(-) create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/type/DefaultRouteType.java create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/type/HierarchicalVehicleType.java 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/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..648dae47 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/HierarchicalVehicleType.java @@ -0,0 +1,117 @@ +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", true), + HIGH_SPEED_RAIL_SERVICE(101, "High Speed Rail Service", true), + LONG_DISTANCE_TRAINS(102, "Long Distance Trains", true), + INTER_REGIONAL_RAIL_SERVICE(103, "Inter Regional Rail Service", true), + CAR_TRANSPORT_RAIL_SERVICE(104, "Car Transport Rail Service", false), + SLEEPER_RAIL_SERVICE(105, "Sleeper Rail Service", true), + REGIONAL_RAIL_SERVICE(106, "Regional Rail Service", true), + TOURIST_RAILWAY_SERVICE(107, "Tourist Railway Service", true), + RAIL_SHUTTLE_WITHIN_COMPLEX(108, "Rail Shuttle (Within Complex)", true), + SUBURBAN_RAILWAY(109, "Suburban Railway", true), + REPLACEMENT_RAIL_SERVICE(110, "Replacement Rail Service", false), + SPECIAL_RAIL_SERVICE(111, "Special Rail Service", false), + LORRY_TRANSPORT_RAIL_SERVICE(112, "Lorry Transport Rail Service", false), + ALL_RAIL_SERVICES(113, "All Rail Services", false), + CROSS_COUNTRY_RAIL_SERVICE(114, "Cross-Country Rail Service", false), + VEHICLE_TRANSPORT_RAIL_SERVICE(115, "Vehicle Transport Rail Service", false), + RACK_AND_PINION_RAILWAY(116, "Rack and Pinion Railway", false), + ADDITIONAL_RAIL_SERVICE(117, "Additional Rail Service", false), + + COACH_SERVICE(200, "Coach Service", true), + INTERNATIONAL_COACH_SERVICE(201, "International Coach Service", true), + NATIONAL_COACH_SERVICE(202, "National Coach Service", true), + SHUTTLE_COACH_SERVICE(203, "Shuttle Coach Service", false), + REGIONAL_COACH_SERVICE(204, "Regional Coach Service", true), + SPECIAL_COACH_SERVICE(205, "Special Coach Service", false), + SIGHTSEEING_COACH_SERVICE(206, "Sightseeing Coach Service", false), + TOURIST_COACH_SERVICE(207, "Tourist Coach Service", false), + COMMUTER_COACH_SERVICE(208, "Commuter Coach Service", false), + ALL_COACH_SERVICES(209, "All Coach Services", false), + + URBAN_RAILWAY_SERVICE(400, "Urban Railway Service", true), + METRO_SERVICE(401, "Metro Service", true), + UNDERGROUND_SERVICE(402, "Underground Service", true), + ALL_URBAN_RAILWAY_SERVICES(404, "All Urban Railway Services", false), + MONORAIL(405, "Monorail", true), + + BUS_SERVICE(700, "Bus Service", true), + REGIONAL_BUS_SERVICE(701, "Regional Bus Service", true), + EXPRESS_BUS_SERVICE(702, "Express Bus Service", true), + LOCAL_BUS_SERVICE(704, "Local Bus Service", true), + NIGHT_BUS_SERVICE(705, "Night Bus Service", false), + POST_BUS_SERVICE(706, "Post Bus Service", false), + SPECIAL_NEEDS_BUS(707, "Special Needs Bus", false), + MOBILITY_BUS_SERVICE(708, "Mobility Bus Service", false), + MOBILITY_BUS_FOR_REGISTERED_DISABLED(709, "Mobility Bus for Registered Disabled", false), + SIGHTSEEING_BUS(710, "Sightseeing Bus", false), + SHUTTLE_BUS(711, "Shuttle Bus", false), + SCHOOL_BUS(712, "School Bus", false), + SCHOOL_AND_PUBLIC_SERVICE_BUS(713, "School and Public Service Bus", false), + RAIL_REPLACEMENT_BUS_SERVICE(714, "Rail Replacement Bus Service", false), + DEMAND_AND_RESPONSE_BUS_SERVICE(715, "Demand and Response Bus Service", true), + ALL_BUS_SERVICES(716, "All Bus Services", false), + + TROLLEYBUS_SERVICE(800, "Trolleybus Service", true), + + TRAM_SERVICE(900, "Tram Service", true), + CITY_TRAM_SERVICE(901, "City Tram Service", false), + LOCAL_TRAM_SERVICE(902, "Local Tram Service", false), + REGIONAL_TRAM_SERVICE(903, "Regional Tram Service", false), + SIGHTSEEING_TRAM_SERVICE(904, "Sightseeing Tram Service", false), + SHUTTLE_TRAM_SERVICE(905, "Shuttle Tram Service", false), + ALL_TRAM_SERVICES(906, "All Tram Services", false), + + WATER_TRANSPORT_SERVICE(1000, "Water Transport Service", true), + AIR_SERVICE(1100, "Air Service", false), + + FERRY_SERVICE(1200, "Ferry Service", true), + + AERIAL_LIFT_SERVICE(1300, "Aerial Lift Service", true), + TELECABIN_SERVICE(1301, "Telecabin Service", true), + CABLE_CAR_SERVICE(1302, "Cable Car Service", false), + ELEVATOR_SERVICE(1303, "Elevator Service", false), + CHAIR_LIFT_SERVICE(1304, "Chair Lift Service", false), + DRAG_LIFT_SERVICE(1305, "Drag Lift Service", false), + SMALL_TELECABIN_SERVICE(1306, "Small Telecabin Service", false), + ALL_TELECABIN_SERVICES(1307, "All Telecabin Services", false), + + FUNICULAR_SERVICE(1400, "Funicular Service", true), + + TAXI_SERVICE(1500, "Taxi Service", false), + COMMUNAL_TAXI_SERVICE(1501, "Communal Taxi Service", true), + WATER_TAXI_SERVICE(1502, "Water Taxi Service", false), + RAIL_TAXI_SERVICE(1503, "Rail Taxi Service", false), + BIKE_TAXI_SERVICE(1504, "Bike Taxi Service", false), + LICENSED_TAXI_SERVICE(1505, "Licensed Taxi Service", false), + PRIVATE_HIRE_SERVICE_VEHICLE(1506, "Private Hire Service Vehicle", false), + ALL_TAXI_SERVICES(1507, "All Taxi Services", false), + + MISCELLANEOUS_SERVICE(1700, "Miscellaneous Service", true), + HORSE_DRAWN_CARRIAGE(1702, "Horse-drawn Carriage", true); + + private final int code; + private final String description; + private final boolean supported; + + 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/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/gtfs/schedule/model/GtfsScheduleTest.java b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java index 3791dcf0..fa60c1b9 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java @@ -1,6 +1,6 @@ package ch.naviqore.gtfs.schedule.model; -import ch.naviqore.gtfs.schedule.type.RouteType; +import ch.naviqore.gtfs.schedule.type.DefaultRouteType; import ch.naviqore.gtfs.schedule.type.ServiceDayTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -34,9 +34,9 @@ void setUp() { .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) + .addRoute("route1", "agency1", "101", "Main Line", DefaultRouteType.BUS) + .addRoute("route2", "agency1", "102", "Cross Town", DefaultRouteType.BUS) + .addRoute("route3", "agency1", "103", "Circulator", DefaultRouteType.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") From a3085ac37e62e4caec797ee82a07cd0efca7f53f Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Fri, 3 May 2024 16:16:46 +0200 Subject: [PATCH 3/8] STYLE: NAV-14 - Rename test methods, adjust visibility and remove white space --- src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java | 2 +- src/test/java/ch/naviqore/Benchmark.java | 1 - .../java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java index f4f442ed..13e16130 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java @@ -8,7 +8,7 @@ */ @RequiredArgsConstructor @Getter -public enum GtfsScheduleFile { +enum GtfsScheduleFile { // FEED_INFO("feed_info.txt", Presence.OPTIONAL), // ATTRIBUTIONS("attributions.txt", Presence.OPTIONAL), AGENCY("agency.txt", Presence.REQUIRED), diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java index 585e3ed9..1c0d5add 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 { 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); From 45921e312e9ecf8558c098d067d0fbf4cdba01d5 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Fri, 3 May 2024 17:44:18 +0200 Subject: [PATCH 4/8] REFACTOR: NAV-14 - Introduce test builder pattern for GTFS module - Add test extension for parameter injection of the builder. - Adjust existing test. --- .../gtfs/schedule/model/GtfsScheduleTest.java | 66 ++++---------- .../model/GtfsScheduleTestBuilder.java | 85 +++++++++++++++++++ .../model/GtfsScheduleTestExtension.java | 24 ++++++ 3 files changed, 127 insertions(+), 48 deletions(-) create mode 100644 src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java create mode 100644 src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestExtension.java 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 fa60c1b9..2ab09724 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java @@ -1,54 +1,24 @@ package ch.naviqore.gtfs.schedule.model; -import ch.naviqore.gtfs.schedule.type.DefaultRouteType; -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 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", DefaultRouteType.BUS) - .addRoute("route2", "agency1", "102", "Cross Town", DefaultRouteType.BUS) - .addRoute("route3", "agency1", "103", "Circulator", DefaultRouteType.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)) - .build(); + void setUp(GtfsScheduleTestBuilder builder) { + schedule = builder.withAddAgency().withAddCalendars().withAddStops().withAddRoutes().withAddTrips() + .withAddStopTimes().build(); } @Nested @@ -56,7 +26,7 @@ class Builder { @Test void shouldCorrectlyCountAgencies() { - assertThat(schedule.getAgencies()).hasSize(1); + assertThat(schedule.getAgencies()).hasSize(2); } @Test @@ -85,8 +55,7 @@ class NearestStops { @Test void shouldFindStopsWithin500Meters() { - assertThat(schedule.getNearestStops(47.3769, 8.5417, 500)).hasSize(3) - .extracting("id") + assertThat(schedule.getNearestStops(47.3769, 8.5417, 500)).hasSize(3).extracting("id") .containsOnly("stop1", "stop2", "stop3"); } @@ -101,17 +70,20 @@ class NextDepartures { @Test void shouldReturnNextDeparturesOnWeekday() { - assertThat(schedule.getNextDepartures("stop1", WEEKDAY_8_AM, Integer.MAX_VALUE)).hasSize(1); + assertThat(schedule.getNextDepartures("stop1", GtfsScheduleTestBuilder.Moments.WEEKDAY_8_AM, + Integer.MAX_VALUE)).hasSize(1); } @Test void shouldReturnNoNextDeparturesOnWeekday() { - assertThat(schedule.getNextDepartures("stop1", WEEKDAY_9_AM, Integer.MAX_VALUE)).isEmpty(); + assertThat(schedule.getNextDepartures("stop1", GtfsScheduleTestBuilder.Moments.WEEKDAY_9_AM, + Integer.MAX_VALUE)).isEmpty(); } @Test void shouldReturnNextDeparturesOnSaturday() { - assertThat(schedule.getNextDepartures("stop1", SATURDAY_9_AM, Integer.MAX_VALUE)).hasSize(1); + assertThat(schedule.getNextDepartures("stop1", GtfsScheduleTestBuilder.Moments.SATURDAY_9_AM, + Integer.MAX_VALUE)).hasSize(1); } @Test @@ -126,21 +98,19 @@ class ActiveTrips { @Test void shouldReturnActiveTripsOnWeekday() { - assertThat(schedule.getActiveTrips(WEEKDAY_8_AM.toLocalDate())).hasSize(2) - .extracting("id") - .containsOnly("trip1", "trip2"); + assertThat(schedule.getActiveTrips(GtfsScheduleTestBuilder.Moments.WEEKDAY_8_AM.toLocalDate())).hasSize(2) + .extracting("id").containsOnly("trip1", "trip2"); } @Test void shouldReturnActiveTripsOnWeekend() { - assertThat(schedule.getActiveTrips(SATURDAY_9_AM.toLocalDate())).hasSize(1) - .extracting("id") - .containsOnly("trip3"); + assertThat(schedule.getActiveTrips(GtfsScheduleTestBuilder.Moments.SATURDAY_9_AM.toLocalDate())).hasSize(1) + .extracting("id").containsOnly("trip3"); } @Test void shouldReturnNoActiveTripsForNonServiceDay() { - assertThat(schedule.getActiveTrips(END_DATE.plusMonths(1))).isEmpty(); + assertThat(schedule.getActiveTrips(GtfsScheduleTestBuilder.Validity.END_DATE.plusMonths(1))).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..cb998ce8 --- /dev/null +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java @@ -0,0 +1,85 @@ +package ch.naviqore.gtfs.schedule.model; + +import ch.naviqore.gtfs.schedule.type.DefaultRouteType; +import ch.naviqore.gtfs.schedule.type.HierarchicalVehicleType; +import ch.naviqore.gtfs.schedule.type.ServiceDayTime; +import lombok.NoArgsConstructor; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Month; +import java.util.EnumSet; + +/** + * Test builder to set up a GTFS schedule for testing purposes. + * + * @author munterfi + */ +@NoArgsConstructor +public class GtfsScheduleTestBuilder { + + public static final class Validity { + public static final LocalDate START_DATE = LocalDate.of(2024, Month.APRIL, 1); + public static final LocalDate END_DATE = START_DATE.plusMonths(1); + } + + 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_9_AM = WEEKDAY_8_AM.plusHours(1); + public static final LocalDateTime SATURDAY_9_AM = LocalDateTime.of(2024, Month.APRIL, 27, 9, 0); + } + + 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.START_DATE, + Validity.END_DATE) + .addCalendar("weekends", EnumSet.of(DayOfWeek.SATURDAY), Validity.START_DATE, Validity.END_DATE); + return this; + } + + public GtfsScheduleTestBuilder withAddStops() { + builder.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); + return this; + } + + public GtfsScheduleTestBuilder withAddRoutes() { + builder.addRoute("route1", "agency1", "101", "Main Line", DefaultRouteType.RAIL) + .addRoute("route2", "agency2", "102", "Cross Town", DefaultRouteType.BUS) + .addRoute("route3", "agency2", "103", "Circulator", HierarchicalVehicleType.SUBURBAN_RAILWAY); + return this; + } + + public GtfsScheduleTestBuilder withAddTrips() { + builder.addTrip("trip1", "route1", "weekdays").addTrip("trip2", "route2", "weekdays") + .addTrip("trip3", "route3", "weekends"); + return this; + } + + public GtfsScheduleTestBuilder withAddStopTimes() { + builder.addStopTime("trip1", "stop1", hms("08:00:00"), hms("08:05:00")) + .addStopTime("trip1", "stop2", hms("08:10:00"), hms("08:15:00")) + .addStopTime("trip2", "stop3", hms("09:00:00"), hms("09:05:00")) + .addStopTime("trip2", "stop4", hms("09:10:00"), hms("09:15:00")) + .addStopTime("trip3", "stop5", hms("10:00:00"), hms("10:05:00")) + .addStopTime("trip3", "stop1", hms("10:10:00"), hms("10:15:00")); + return this; + } + + public GtfsSchedule build() { + return builder.build(); + } + + private static ServiceDayTime hms(String time) { + return ServiceDayTime.parse(time); + } +} 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(); + } +} From 2b1cdc2636366670fe0ca7b95d60feafdc706aab Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Sat, 4 May 2024 13:35:15 +0200 Subject: [PATCH 5/8] ENH: NAV-14 - Improve testing of GTFS schedule - Create larger example schedule with more calendar variations and both directions. - Increase test coverage concerning boundary cases in next departures (midnight), nearest stops (same location query) and active trips (dates outside validity or no service day). --- .../gtfs/schedule/type/ServiceDayTime.java | 4 + .../gtfs/schedule/model/GtfsScheduleTest.java | 154 ++++++++++++--- .../model/GtfsScheduleTestBuilder.java | 177 ++++++++++++++---- 3 files changed, 280 insertions(+), 55 deletions(-) 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/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java index 2ab09724..573a1fba 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java @@ -1,11 +1,14 @@ package ch.naviqore.gtfs.schedule.model; +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.LocalDateTime; +import java.util.List; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -17,8 +20,8 @@ class GtfsScheduleTest { @BeforeEach void setUp(GtfsScheduleTestBuilder builder) { - schedule = builder.withAddAgency().withAddCalendars().withAddStops().withAddRoutes().withAddTrips() - .withAddStopTimes().build(); + schedule = builder.withAddAgency().withAddCalendars().withAddCalendarDates().withAddInterCity() + .withAddUnderground().withAddBus().build(); } @Nested @@ -36,12 +39,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 @@ -54,36 +57,122 @@ void shouldCorrectlyCountCalendars() { class NearestStops { @Test - void shouldFindStopsWithin500Meters() { - assertThat(schedule.getNearestStops(47.3769, 8.5417, 500)).hasSize(3).extracting("id") - .containsOnly("stop1", "stop2", "stop3"); + 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("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", GtfsScheduleTestBuilder.Moments.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:00: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_16", "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 shouldReturnNoNextDeparturesOnWeekday() { - assertThat(schedule.getNextDepartures("stop1", GtfsScheduleTestBuilder.Moments.WEEKDAY_9_AM, - Integer.MAX_VALUE)).isEmpty(); + void shouldReturnNextDeparturesOnWeekend() { + List departures = schedule.getNextDepartures(STOP_ID, + GtfsScheduleTestBuilder.Moments.WEEKEND_8_AM, LIMIT); + + assertWeekendAndHoliday(departures); } @Test - void shouldReturnNextDeparturesOnSaturday() { - assertThat(schedule.getNextDepartures("stop1", GtfsScheduleTestBuilder.Moments.SATURDAY_9_AM, - Integer.MAX_VALUE)).hasSize(1); + void shouldReturnNextDeparturesOnHoliday() { + List departures = schedule.getNextDepartures(STOP_ID, + GtfsScheduleTestBuilder.Moments.HOLIDAY.atTime(8, 0), LIMIT); + + assertWeekendAndHoliday(departures); + } + + @Test + 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:00: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_80", "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 @@ -96,21 +185,44 @@ 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(GtfsScheduleTestBuilder.Moments.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(GtfsScheduleTestBuilder.Moments.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(GtfsScheduleTestBuilder.Validity.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 index cb998ce8..8c77bec1 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java @@ -1,7 +1,9 @@ package ch.naviqore.gtfs.schedule.model; import ch.naviqore.gtfs.schedule.type.DefaultRouteType; +import ch.naviqore.gtfs.schedule.type.ExceptionType; import ch.naviqore.gtfs.schedule.type.HierarchicalVehicleType; +import ch.naviqore.gtfs.schedule.type.RouteType; import ch.naviqore.gtfs.schedule.type.ServiceDayTime; import lombok.NoArgsConstructor; @@ -9,27 +11,70 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.Month; +import java.util.ArrayList; +import java.util.Collections; import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; /** * 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 { - public static final class Validity { - public static final LocalDate START_DATE = LocalDate.of(2024, Month.APRIL, 1); - public static final LocalDate END_DATE = START_DATE.plusMonths(1); - } - - 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_9_AM = WEEKDAY_8_AM.plusHours(1); - public static final LocalDateTime SATURDAY_9_AM = LocalDateTime.of(2024, Month.APRIL, 27, 9, 0); - } - + 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() { @@ -39,39 +84,35 @@ public GtfsScheduleTestBuilder withAddAgency() { } public GtfsScheduleTestBuilder withAddCalendars() { - builder.addCalendar("weekdays", EnumSet.range(DayOfWeek.MONDAY, DayOfWeek.FRIDAY), Validity.START_DATE, - Validity.END_DATE) - .addCalendar("weekends", EnumSet.of(DayOfWeek.SATURDAY), Validity.START_DATE, Validity.END_DATE); + 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 withAddStops() { - builder.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); + 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 withAddRoutes() { - builder.addRoute("route1", "agency1", "101", "Main Line", DefaultRouteType.RAIL) - .addRoute("route2", "agency2", "102", "Cross Town", DefaultRouteType.BUS) - .addRoute("route3", "agency2", "103", "Circulator", HierarchicalVehicleType.SUBURBAN_RAILWAY); + public GtfsScheduleTestBuilder withAddInterCity() { + addRoute(ROUTES.getFirst(), true, true); return this; } - public GtfsScheduleTestBuilder withAddTrips() { - builder.addTrip("trip1", "route1", "weekdays").addTrip("trip2", "route2", "weekdays") - .addTrip("trip3", "route3", "weekends"); + + public GtfsScheduleTestBuilder withAddUnderground() { + addRoute(ROUTES.get(1), true, false); return this; } - public GtfsScheduleTestBuilder withAddStopTimes() { - builder.addStopTime("trip1", "stop1", hms("08:00:00"), hms("08:05:00")) - .addStopTime("trip1", "stop2", hms("08:10:00"), hms("08:15:00")) - .addStopTime("trip2", "stop3", hms("09:00:00"), hms("09:05:00")) - .addStopTime("trip2", "stop4", hms("09:10:00"), hms("09:15:00")) - .addStopTime("trip3", "stop5", hms("10:00:00"), hms("10:05:00")) - .addStopTime("trip3", "stop1", hms("10:10:00"), hms("10:15:00")); + public GtfsScheduleTestBuilder withAddBus() { + addRoute(ROUTES.get(2), false, true); return this; } @@ -79,7 +120,75 @@ public GtfsSchedule build() { return builder.build(); } - private static ServiceDayTime hms(String time) { - return ServiceDayTime.parse(time); + + 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 : route.stops) { + 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); } } From eac4f5ccb93e31ca25624a63c565baf538746b71 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Sat, 4 May 2024 16:37:15 +0200 Subject: [PATCH 6/8] ORG: NAV-6 - Set code style for project - Add IntelliJ code style to project. - Reformat complete project. --- .idea/codeStyles/Project.xml | 25 ++++++++++++++++ .idea/codeStyles/codeStyleConfig.xml | 2 +- .../gtfs/schedule/GtfsScheduleFile.java | 6 +++- .../gtfs/schedule/GtfsScheduleReader.java | 9 ++---- .../gtfs/schedule/model/GtfsScheduleTest.java | 30 +++++++++++++------ .../model/GtfsScheduleTestBuilder.java | 16 ++-------- 6 files changed, 57 insertions(+), 31 deletions(-) create mode 100644 .idea/codeStyles/Project.xml 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 13e16130..ac61b7cc 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java @@ -28,6 +28,10 @@ enum GtfsScheduleFile { private final Presence presence; public enum Presence { - REQUIRED, OPTIONAL, CONDITIONALLY_REQUIRED, CONDITIONALLY_FORBIDDEN, RECOMMENDED + REQUIRED, + OPTIONAL, + CONDITIONALLY_REQUIRED, + CONDITIONALLY_FORBIDDEN, + RECOMMENDED } } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java index 9f364bd0..d8aa3908 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java @@ -10,11 +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.FileNotFoundException; -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; @@ -65,7 +61,8 @@ private static void readFromZip(File zipFile, GtfsScheduleParser parser) throws readCsvRecords(reader, parser, fileType); } } else if (fileType.getPresence() == GtfsScheduleFile.Presence.REQUIRED) { - throw new FileNotFoundException("Required GTFS CSV file" + fileType.getFileName() + " not found in ZIP"); + throw new FileNotFoundException( + "Required GTFS CSV file" + fileType.getFileName() + " not found in ZIP"); } } } 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 573a1fba..5e5a5cdc 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java @@ -20,8 +20,13 @@ class GtfsScheduleTest { @BeforeEach void setUp(GtfsScheduleTestBuilder builder) { - schedule = builder.withAddAgency().withAddCalendars().withAddCalendarDates().withAddInterCity() - .withAddUnderground().withAddBus().build(); + schedule = builder.withAddAgency() + .withAddCalendars() + .withAddCalendarDates() + .withAddInterCity() + .withAddUnderground() + .withAddBus() + .build(); } @Nested @@ -63,13 +68,15 @@ void shouldFindStopWithin1Meter() { @Test void shouldFindStopsWithin10000Meters() { - assertThat(schedule.getNearestStops(47.5, 8.5, 10000)).hasSize(3).extracting("id") + 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") + assertThat(schedule.getNearestStops(47.5, 8.5, Integer.MAX_VALUE)).hasSize(9) + .extracting("id") .containsOnly("s1", "s2", "s3", "u1", "u2", "u3", "u4", "u5", "u6"); } @@ -90,7 +97,8 @@ private static void assertWeekendAndHoliday(List departures) { 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) + assertThat(departures).hasSize(LIMIT) + .extracting(StopTime::departure) .containsExactlyElementsOf(expectedDepartures); // assert trips are correct @@ -114,7 +122,8 @@ void shouldReturnNextDeparturesOnWeekday() { List expectedDepartures = List.of(ServiceDayTime.parse("08:00:00"), ServiceDayTime.parse("08:00:00"), ServiceDayTime.parse("08:09:00"), ServiceDayTime.parse("08:09:00"), ServiceDayTime.parse("08:15:00")); - assertThat(departures).hasSize(LIMIT).extracting(StopTime::departure) + assertThat(departures).hasSize(LIMIT) + .extracting(StopTime::departure) .containsExactlyElementsOf(expectedDepartures); // assert trips are correct @@ -154,7 +163,8 @@ void shouldReturnNextDeparturesAfterMidnight() { List expectedDepartures = List.of(ServiceDayTime.parse("24:00:00"), ServiceDayTime.parse("24:00:00"), ServiceDayTime.parse("24:09:00"), ServiceDayTime.parse("24:09:00"), ServiceDayTime.parse("24:15:00")); - assertThat(departures).hasSize(LIMIT).extracting(StopTime::departure) + assertThat(departures).hasSize(LIMIT) + .extracting(StopTime::departure) .containsExactlyElementsOf(expectedDepartures); // assert trips are correct @@ -186,7 +196,8 @@ void shouldReturnNoDeparturesFromUnknownStop() { class ActiveTrips { private static void assertWeekendAndHoliday(List activeTrips) { - assertThat(activeTrips).hasSize(168).extracting(trip -> trip.getRoute().getId()) + assertThat(activeTrips).hasSize(168) + .extracting(trip -> trip.getRoute().getId()) .containsAll(Set.of("route1", "route2")); } @@ -195,7 +206,8 @@ void shouldReturnActiveTripsOnWeekday() { List activeTrips = schedule.getActiveTrips( GtfsScheduleTestBuilder.Moments.WEEKDAY_8_AM.toLocalDate()); - assertThat(activeTrips).hasSize(503).extracting(trip -> trip.getRoute().getId()) + assertThat(activeTrips).hasSize(503) + .extracting(trip -> trip.getRoute().getId()) .containsAll(Set.of("route1", "route2", "route3")); } diff --git a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java index 8c77bec1..2c9b06a1 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java @@ -1,23 +1,13 @@ package ch.naviqore.gtfs.schedule.model; -import ch.naviqore.gtfs.schedule.type.DefaultRouteType; -import ch.naviqore.gtfs.schedule.type.ExceptionType; -import ch.naviqore.gtfs.schedule.type.HierarchicalVehicleType; -import ch.naviqore.gtfs.schedule.type.RouteType; -import ch.naviqore.gtfs.schedule.type.ServiceDayTime; +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.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * Test builder to set up a GTFS schedule for testing purposes. @@ -105,7 +95,6 @@ public GtfsScheduleTestBuilder withAddInterCity() { return this; } - public GtfsScheduleTestBuilder withAddUnderground() { addRoute(ROUTES.get(1), true, false); return this; @@ -120,7 +109,6 @@ 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); From f2479d362190c6c66a53421b8d86e92ce4ad69ec Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Sat, 4 May 2024 18:08:28 +0200 Subject: [PATCH 7/8] ENH: NAV-19 - Partition routes into sub-routes with the same stop sequence - Add GtfsRoutePartitioner and corresponding test. - Adjust GtfsToRaptorConverter to use the GtfsRoutePartitioner. - Fix bug in GtfsScheduleTestBuilder; reversed routes also have reversed stops sequences. --- .../naviqore/raptor/GtfsRoutePartitioner.java | 105 ++++++++++++++++++ .../raptor/GtfsToRaptorConverter.java | 35 +++--- src/test/java/ch/naviqore/Benchmark.java | 2 +- .../gtfs/schedule/model/GtfsScheduleTest.java | 8 +- .../model/GtfsScheduleTestBuilder.java | 2 +- .../raptor/GtfsRoutePartitionerTest.java | 45 ++++++++ .../raptor/GtfsToRaptorConverterIT.java | 7 +- 7 files changed, 182 insertions(+), 22 deletions(-) create mode 100644 src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java create mode 100644 src/test/java/ch/naviqore/raptor/GtfsRoutePartitionerTest.java 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/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java index 1c0d5add..85660ee2 100644 --- a/src/test/java/ch/naviqore/Benchmark.java +++ b/src/test/java/ch/naviqore/Benchmark.java @@ -55,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/model/GtfsScheduleTest.java b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java index 5e5a5cdc..b8d9e993 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java @@ -120,14 +120,14 @@ void shouldReturnNextDeparturesOnWeekday() { // assert departures times are correct List expectedDepartures = List.of(ServiceDayTime.parse("08:00:00"), - ServiceDayTime.parse("08:00:00"), ServiceDayTime.parse("08:09: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_16", "route3_wd_f_17", + 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); @@ -161,14 +161,14 @@ void shouldReturnNextDeparturesAfterMidnight() { // assert departures times are correct List expectedDepartures = List.of(ServiceDayTime.parse("24:00:00"), - ServiceDayTime.parse("24:00:00"), ServiceDayTime.parse("24:09: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_80", "route3_wd_f_81", + 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); diff --git a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java index 2c9b06a1..c52d74db 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java @@ -140,7 +140,7 @@ private void addTrips(Route route, boolean weekday, boolean reverse) { 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 : route.stops) { + for (String stopId : routeStops) { builder.addStopTime(tripId, stopId, new ServiceDayTime(departureTime - dwellTime), new ServiceDayTime(departureTime)); departureTime += travelTime + dwellTime; 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 From 5958bad66e3dcaf4c5f149f69b6ba5b68e003a58 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Mon, 6 May 2024 16:12:16 +0200 Subject: [PATCH 8/8] ENH: NAV-14 - Remove supported field from HierarchicalVehicleType --- .../type/HierarchicalVehicleType.java | 181 +++++++++--------- 1 file changed, 90 insertions(+), 91 deletions(-) diff --git a/src/main/java/ch/naviqore/gtfs/schedule/type/HierarchicalVehicleType.java b/src/main/java/ch/naviqore/gtfs/schedule/type/HierarchicalVehicleType.java index 648dae47..d6f43562 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/type/HierarchicalVehicleType.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/HierarchicalVehicleType.java @@ -7,100 +7,99 @@ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @Getter public enum HierarchicalVehicleType implements RouteType { - RAILWAY_SERVICE(100, "Railway Service", true), - HIGH_SPEED_RAIL_SERVICE(101, "High Speed Rail Service", true), - LONG_DISTANCE_TRAINS(102, "Long Distance Trains", true), - INTER_REGIONAL_RAIL_SERVICE(103, "Inter Regional Rail Service", true), - CAR_TRANSPORT_RAIL_SERVICE(104, "Car Transport Rail Service", false), - SLEEPER_RAIL_SERVICE(105, "Sleeper Rail Service", true), - REGIONAL_RAIL_SERVICE(106, "Regional Rail Service", true), - TOURIST_RAILWAY_SERVICE(107, "Tourist Railway Service", true), - RAIL_SHUTTLE_WITHIN_COMPLEX(108, "Rail Shuttle (Within Complex)", true), - SUBURBAN_RAILWAY(109, "Suburban Railway", true), - REPLACEMENT_RAIL_SERVICE(110, "Replacement Rail Service", false), - SPECIAL_RAIL_SERVICE(111, "Special Rail Service", false), - LORRY_TRANSPORT_RAIL_SERVICE(112, "Lorry Transport Rail Service", false), - ALL_RAIL_SERVICES(113, "All Rail Services", false), - CROSS_COUNTRY_RAIL_SERVICE(114, "Cross-Country Rail Service", false), - VEHICLE_TRANSPORT_RAIL_SERVICE(115, "Vehicle Transport Rail Service", false), - RACK_AND_PINION_RAILWAY(116, "Rack and Pinion Railway", false), - ADDITIONAL_RAIL_SERVICE(117, "Additional Rail Service", false), - - COACH_SERVICE(200, "Coach Service", true), - INTERNATIONAL_COACH_SERVICE(201, "International Coach Service", true), - NATIONAL_COACH_SERVICE(202, "National Coach Service", true), - SHUTTLE_COACH_SERVICE(203, "Shuttle Coach Service", false), - REGIONAL_COACH_SERVICE(204, "Regional Coach Service", true), - SPECIAL_COACH_SERVICE(205, "Special Coach Service", false), - SIGHTSEEING_COACH_SERVICE(206, "Sightseeing Coach Service", false), - TOURIST_COACH_SERVICE(207, "Tourist Coach Service", false), - COMMUTER_COACH_SERVICE(208, "Commuter Coach Service", false), - ALL_COACH_SERVICES(209, "All Coach Services", false), - - URBAN_RAILWAY_SERVICE(400, "Urban Railway Service", true), - METRO_SERVICE(401, "Metro Service", true), - UNDERGROUND_SERVICE(402, "Underground Service", true), - ALL_URBAN_RAILWAY_SERVICES(404, "All Urban Railway Services", false), - MONORAIL(405, "Monorail", true), - - BUS_SERVICE(700, "Bus Service", true), - REGIONAL_BUS_SERVICE(701, "Regional Bus Service", true), - EXPRESS_BUS_SERVICE(702, "Express Bus Service", true), - LOCAL_BUS_SERVICE(704, "Local Bus Service", true), - NIGHT_BUS_SERVICE(705, "Night Bus Service", false), - POST_BUS_SERVICE(706, "Post Bus Service", false), - SPECIAL_NEEDS_BUS(707, "Special Needs Bus", false), - MOBILITY_BUS_SERVICE(708, "Mobility Bus Service", false), - MOBILITY_BUS_FOR_REGISTERED_DISABLED(709, "Mobility Bus for Registered Disabled", false), - SIGHTSEEING_BUS(710, "Sightseeing Bus", false), - SHUTTLE_BUS(711, "Shuttle Bus", false), - SCHOOL_BUS(712, "School Bus", false), - SCHOOL_AND_PUBLIC_SERVICE_BUS(713, "School and Public Service Bus", false), - RAIL_REPLACEMENT_BUS_SERVICE(714, "Rail Replacement Bus Service", false), - DEMAND_AND_RESPONSE_BUS_SERVICE(715, "Demand and Response Bus Service", true), - ALL_BUS_SERVICES(716, "All Bus Services", false), - - TROLLEYBUS_SERVICE(800, "Trolleybus Service", true), - - TRAM_SERVICE(900, "Tram Service", true), - CITY_TRAM_SERVICE(901, "City Tram Service", false), - LOCAL_TRAM_SERVICE(902, "Local Tram Service", false), - REGIONAL_TRAM_SERVICE(903, "Regional Tram Service", false), - SIGHTSEEING_TRAM_SERVICE(904, "Sightseeing Tram Service", false), - SHUTTLE_TRAM_SERVICE(905, "Shuttle Tram Service", false), - ALL_TRAM_SERVICES(906, "All Tram Services", false), - - WATER_TRANSPORT_SERVICE(1000, "Water Transport Service", true), - AIR_SERVICE(1100, "Air Service", false), - - FERRY_SERVICE(1200, "Ferry Service", true), - - AERIAL_LIFT_SERVICE(1300, "Aerial Lift Service", true), - TELECABIN_SERVICE(1301, "Telecabin Service", true), - CABLE_CAR_SERVICE(1302, "Cable Car Service", false), - ELEVATOR_SERVICE(1303, "Elevator Service", false), - CHAIR_LIFT_SERVICE(1304, "Chair Lift Service", false), - DRAG_LIFT_SERVICE(1305, "Drag Lift Service", false), - SMALL_TELECABIN_SERVICE(1306, "Small Telecabin Service", false), - ALL_TELECABIN_SERVICES(1307, "All Telecabin Services", false), - - FUNICULAR_SERVICE(1400, "Funicular Service", true), - - TAXI_SERVICE(1500, "Taxi Service", false), - COMMUNAL_TAXI_SERVICE(1501, "Communal Taxi Service", true), - WATER_TAXI_SERVICE(1502, "Water Taxi Service", false), - RAIL_TAXI_SERVICE(1503, "Rail Taxi Service", false), - BIKE_TAXI_SERVICE(1504, "Bike Taxi Service", false), - LICENSED_TAXI_SERVICE(1505, "Licensed Taxi Service", false), - PRIVATE_HIRE_SERVICE_VEHICLE(1506, "Private Hire Service Vehicle", false), - ALL_TAXI_SERVICES(1507, "All Taxi Services", false), - - MISCELLANEOUS_SERVICE(1700, "Miscellaneous Service", true), - HORSE_DRAWN_CARRIAGE(1702, "Horse-drawn Carriage", true); + 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; - private final boolean supported; public static HierarchicalVehicleType parse(String code) { return parse(Integer.parseInt(code));