From cc2f6183677a405c63f104fa4d421bdb1304310a Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Fri, 28 Jun 2024 22:56:32 +0200 Subject: [PATCH 1/9] =?UTF-8?q?DOC:=20NAV-104=20-=20Add=20Z=C3=BCrich=20Tr?= =?UTF-8?q?ams=20data=20source=20to=20application=20properties.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 06657f6e..89d50c37 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,6 +10,7 @@ logging.level.root=${LOG_LEVEL:INFO} # value is a file path, the GTFS is loaded from the file and the update interval is ignored. Examples: # - gtfs.static.uri=benchmark/input/switzerland.zip # - gtfs.static.uri=https://opentransportdata.swiss/en/dataset/timetable-2024-gtfs2020/permalink +# - gtfs.static.uri=https://connolly.ch/zuerich-trams.zip gtfs.static.uri=${GTFS_STATIC_URL:src/test/resources/ch/naviqore/gtfs/schedule/sample-feed-1.zip} # Cron expression for updating the static GTFS feed from the provided URL. Public transit agencies update their static # GTFS data regularly. Set this interval to match the agency's publish schedule. Default is to update the schedule From f8de5262ab5f10ffcd943ddc369774749c667115 Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Fri, 28 Jun 2024 23:10:04 +0200 Subject: [PATCH 2/9] DOC: NAV-104 - Update openapi.yaml with more powerful iso line api. --- src/main/resources/ch.naviqore.app/openapi.yaml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/resources/ch.naviqore.app/openapi.yaml b/src/main/resources/ch.naviqore.app/openapi.yaml index d8f13d54..636c14f1 100644 --- a/src/main/resources/ch.naviqore.app/openapi.yaml +++ b/src/main/resources/ch.naviqore.app/openapi.yaml @@ -272,6 +272,11 @@ paths: schema: type: integer description: The minimum transfer time between trips in seconds. Defaults to `0`. + - name: returnConnections + in: query + schema: + type: boolean + description: Whether to return the connections for each reachable stop, else the connection field will be null. Defaults to `false`. responses: '200': description: A list of stop and fastest connection pairs for each reachable stop. @@ -280,7 +285,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/EarliestArrival' + $ref: '#/components/schemas/StopConnection' '400': description: Invalid input parameters '404': @@ -346,14 +351,13 @@ components: format: date-time trip: $ref: '#/components/schemas/Trip' - EarliestArrival: + StopConnection: type: object properties: stop: $ref: '#/components/schemas/Stop' - arrivalTime: - type: string - format: date-time + connectingLeg: + $ref: '#/components/schemas/Leg' connection: $ref: '#/components/schemas/Connection' Coordinate: From ea0f9b7c770875d277bd11c413a10a91f8984d58 Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Fri, 28 Jun 2024 23:10:44 +0200 Subject: [PATCH 3/9] ENH: NAV-104 - Make controller/mapper build return objects with optimized payload size for iso lines when requested. --- .../app/controller/RoutingController.java | 25 ++++---- .../java/ch/naviqore/app/dto/DtoMapper.java | 59 ++++++++++++++++++- ...rliestArrival.java => StopConnection.java} | 5 +- src/main/resources/application.properties | 4 +- .../app/controller/RoutingControllerTest.java | 28 ++++----- 5 files changed, 88 insertions(+), 33 deletions(-) rename src/main/java/ch/naviqore/app/dto/{EarliestArrival.java => StopConnection.java} (70%) diff --git a/src/main/java/ch/naviqore/app/controller/RoutingController.java b/src/main/java/ch/naviqore/app/controller/RoutingController.java index af2866f7..97df658d 100644 --- a/src/main/java/ch/naviqore/app/controller/RoutingController.java +++ b/src/main/java/ch/naviqore/app/controller/RoutingController.java @@ -2,7 +2,7 @@ import ch.naviqore.app.dto.Connection; import ch.naviqore.app.dto.DtoMapper; -import ch.naviqore.app.dto.EarliestArrival; +import ch.naviqore.app.dto.StopConnection; import ch.naviqore.app.dto.TimeType; import ch.naviqore.service.PublicTransitService; import ch.naviqore.service.Stop; @@ -129,15 +129,16 @@ public List getConnections(@RequestParam(required = false) String so } @GetMapping("/isolines") - public List getIsolines(@RequestParam(required = false) String sourceStopId, - @RequestParam(required = false, defaultValue = "-91") double sourceLatitude, - @RequestParam(required = false, defaultValue = "-181") double sourceLongitude, - @RequestParam(required = false) LocalDateTime dateTime, - @RequestParam(required = false, defaultValue = "DEPARTURE") TimeType timeType, - @RequestParam(required = false, defaultValue = "2147483647") int maxWalkingDuration, - @RequestParam(required = false, defaultValue = "2147483647") int maxTransferNumber, - @RequestParam(required = false, defaultValue = "2147483647") int maxTravelTime, - @RequestParam(required = false, defaultValue = "0") int minTransferTime) { + public List getIsolines(@RequestParam(required = false) String sourceStopId, + @RequestParam(required = false, defaultValue = "-91") double sourceLatitude, + @RequestParam(required = false, defaultValue = "-181") double sourceLongitude, + @RequestParam(required = false) LocalDateTime dateTime, + @RequestParam(required = false, defaultValue = "DEPARTURE") TimeType timeType, + @RequestParam(required = false, defaultValue = "2147483647") int maxWalkingDuration, + @RequestParam(required = false, defaultValue = "2147483647") int maxTransferNumber, + @RequestParam(required = false, defaultValue = "2147483647") int maxTravelTime, + @RequestParam(required = false, defaultValue = "0") int minTransferTime, + @RequestParam(required = false, defaultValue = "false") boolean returnConnections) { GeoCoordinate sourceCoordinate = null; if (sourceStopId == null) { @@ -163,12 +164,12 @@ public List getIsolines(@RequestParam(required = false) String connections = service.getIsoLines(sourceCoordinate, dateTime, map(timeType), config); } - List arrivals = new ArrayList<>(); + List arrivals = new ArrayList<>(); for (Map.Entry entry : connections.entrySet()) { Stop stop = entry.getKey(); ch.naviqore.service.Connection connection = entry.getValue(); - arrivals.add(map(stop, connection)); + arrivals.add(map(stop, connection, map(timeType), returnConnections)); } return arrivals; diff --git a/src/main/java/ch/naviqore/app/dto/DtoMapper.java b/src/main/java/ch/naviqore/app/dto/DtoMapper.java index 47ee787d..4aa64da0 100644 --- a/src/main/java/ch/naviqore/app/dto/DtoMapper.java +++ b/src/main/java/ch/naviqore/app/dto/DtoMapper.java @@ -6,6 +6,7 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; import java.util.List; @NoArgsConstructor(access = AccessLevel.PRIVATE) @@ -60,8 +61,62 @@ public static Connection map(ch.naviqore.service.Connection connection) { return new Connection(legs); } - public static EarliestArrival map(ch.naviqore.service.Stop stop, ch.naviqore.service.Connection connection) { - return new EarliestArrival(map(stop), connection.getArrivalTime(), map(connection)); + public static StopConnection map(ch.naviqore.service.Stop stop, ch.naviqore.service.Connection serviceConnection, + TimeType timeType, boolean returnConnections) { + + Connection connection = map(serviceConnection); + Leg connectingLeg; + + if (timeType == TimeType.DEPARTURE) { + connectingLeg = connection.getLegs().getLast(); + // create a leg from stop before arrival stop (on trip) to the arrival stop + if (connectingLeg.getTrip() != null) { + int stopTimeIndex = findStopTimeIndexInTrip(connectingLeg.getTrip(), connectingLeg.getToStop(), + connectingLeg.getArrivalTime(), TimeType.ARRIVAL); + StopTime sourceStopTime = connectingLeg.getTrip().getStopTimes().get(stopTimeIndex - 1); + connectingLeg = new Leg(connectingLeg.getType(), sourceStopTime.getStop().getCoordinates(), + connectingLeg.getTo(), sourceStopTime.getStop(), connectingLeg.getToStop(), + sourceStopTime.getDepartureTime(), connectingLeg.getArrivalTime(), connectingLeg.getTrip()); + } + } else { + connectingLeg = connection.getLegs().getFirst(); + // create a leg from the departure stop to the first stop on the trip + if (connectingLeg.getTrip() != null) { + int stopTimeIndex = findStopTimeIndexInTrip(connectingLeg.getTrip(), connectingLeg.getFromStop(), + connectingLeg.getDepartureTime(), TimeType.DEPARTURE); + StopTime targetStopTime = connectingLeg.getTrip().getStopTimes().get(stopTimeIndex + 1); + connectingLeg = new Leg(connectingLeg.getType(), connectingLeg.getFrom(), + targetStopTime.getStop().getCoordinates(), connectingLeg.getFromStop(), + targetStopTime.getStop(), connectingLeg.getDepartureTime(), targetStopTime.getArrivalTime(), + connectingLeg.getTrip()); + } + } + + if( connectingLeg.getTrip() != null && ! returnConnections ) { + // nullify stop times from trips if connections are not requested (reducing payload) + Trip reducedTrip = new Trip(connectingLeg.getTrip().getHeadSign(), connectingLeg.getTrip().getRoute(), + null); + connectingLeg = new Leg(connectingLeg.getType(), connectingLeg.getFrom(), connectingLeg.getTo(), + connectingLeg.getFromStop(), connectingLeg.getToStop(), connectingLeg.getDepartureTime(), + connectingLeg.getArrivalTime(), reducedTrip); + } + + return new StopConnection(map(stop), connectingLeg, returnConnections ? connection : null); + } + + private static int findStopTimeIndexInTrip(Trip trip, Stop stop, LocalDateTime time, TimeType timeType) { + List stopTimes = trip.getStopTimes(); + for (int i = 0; i < stopTimes.size(); i++) { + StopTime stopTime = stopTimes.get(i); + if (stopTime.getStop().equals(stop)) { + if (timeType == TimeType.DEPARTURE && stopTime.getDepartureTime().equals(time)) { + return i; + } else if (timeType == TimeType.ARRIVAL && stopTime.getArrivalTime().equals(time)) { + return i; + } + } + } + throw new IllegalStateException("Stop time not found in trip."); } private static class LegVisitorImpl implements LegVisitor { diff --git a/src/main/java/ch/naviqore/app/dto/EarliestArrival.java b/src/main/java/ch/naviqore/app/dto/StopConnection.java similarity index 70% rename from src/main/java/ch/naviqore/app/dto/EarliestArrival.java rename to src/main/java/ch/naviqore/app/dto/StopConnection.java index 52c48cd2..48f5c093 100644 --- a/src/main/java/ch/naviqore/app/dto/EarliestArrival.java +++ b/src/main/java/ch/naviqore/app/dto/StopConnection.java @@ -9,11 +9,10 @@ @EqualsAndHashCode @ToString @Getter -public class EarliestArrival { +public class StopConnection { private final Stop stop; - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) - private final LocalDateTime arrivalTime; + private final Leg connectingLeg; private final Connection connection; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 89d50c37..690ad44d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,8 +10,8 @@ logging.level.root=${LOG_LEVEL:INFO} # value is a file path, the GTFS is loaded from the file and the update interval is ignored. Examples: # - gtfs.static.uri=benchmark/input/switzerland.zip # - gtfs.static.uri=https://opentransportdata.swiss/en/dataset/timetable-2024-gtfs2020/permalink -# - gtfs.static.uri=https://connolly.ch/zuerich-trams.zip -gtfs.static.uri=${GTFS_STATIC_URL:src/test/resources/ch/naviqore/gtfs/schedule/sample-feed-1.zip} +gtfs.static.uri=https://connolly.ch/zuerich-trams.zip +# gtfs.static.uri=${GTFS_STATIC_URL:src/test/resources/ch/naviqore/gtfs/schedule/sample-feed-1.zip} # Cron expression for updating the static GTFS feed from the provided URL. Public transit agencies update their static # GTFS data regularly. Set this interval to match the agency's publish schedule. Default is to update the schedule # daily at 4 AM. diff --git a/src/test/java/ch/naviqore/app/controller/RoutingControllerTest.java b/src/test/java/ch/naviqore/app/controller/RoutingControllerTest.java index 1744bd8f..83ccd80f 100644 --- a/src/test/java/ch/naviqore/app/controller/RoutingControllerTest.java +++ b/src/test/java/ch/naviqore/app/controller/RoutingControllerTest.java @@ -1,7 +1,7 @@ package ch.naviqore.app.controller; import ch.naviqore.app.dto.Connection; -import ch.naviqore.app.dto.EarliestArrival; +import ch.naviqore.app.dto.StopConnection; import ch.naviqore.app.dto.TimeType; import ch.naviqore.service.PublicTransitService; import ch.naviqore.service.Stop; @@ -181,8 +181,8 @@ void testGetIsoLines() throws StopNotFoundException { Collections.emptyMap()); // Act - List connections = routingController.getIsolines(sourceStopId, -1.0, -1.0, time, - TimeType.DEPARTURE, 30, 2, 120, 5); + List connections = routingController.getIsolines(sourceStopId, -1.0, -1.0, time, + TimeType.DEPARTURE, 30, 2, 120, 5, false); // Assert assertNotNull(connections); @@ -197,7 +197,7 @@ void testGetIsoLines_InvalidSourceStopId() throws StopNotFoundException { // Act & Assert ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> routingController.getIsolines(invalidStopId, -91.0, -181.0, LocalDateTime.now(), - TimeType.DEPARTURE, 30, 2, 120, 5)); + TimeType.DEPARTURE, 30, 2, 120, 5, false)); assertEquals("Stop not found", exception.getReason()); assertEquals(HttpStatusCode.valueOf(404), exception.getStatusCode()); } @@ -207,7 +207,7 @@ void testGetIsoLines_MissingSourceAndCoordinates() { // Act & Assert ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> routingController.getIsolines(null, -91.0, -181.0, LocalDateTime.now(), TimeType.DEPARTURE, 30, 2, - 120, 5)); + 120, 5, false)); assertEquals("Either sourceStopId or sourceLatitude and sourceLongitude must be provided.", exception.getReason()); assertEquals(HttpStatusCode.valueOf(400), exception.getStatusCode()); @@ -218,7 +218,7 @@ void testGetIsoLines_InvalidCoordinates() { // Act & Assert ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> routingController.getIsolines(null, 91, 181, LocalDateTime.now(), TimeType.DEPARTURE, 30, 2, 120, - 5)); + 5, false)); assertEquals("Coordinates must be valid, Latitude between -90 and 90 and Longitude between -180 and 180.", exception.getReason()); assertEquals(HttpStatusCode.valueOf(400), exception.getStatusCode()); @@ -228,8 +228,8 @@ void testGetIsoLines_InvalidCoordinates() { void testGetIsoLines_InvalidMaxTransferNumber() { // Act & Assert ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> routingController.getIsolines(null, 0, 0, LocalDateTime.now(), TimeType.DEPARTURE, 30, -2, 120, - 5)); + () -> routingController.getIsolines(null, 0, 0, LocalDateTime.now(), TimeType.DEPARTURE, 30, -2, 120, 5, + false)); assertEquals("Max transfer number must be greater than or equal to 0.", exception.getReason()); assertEquals(HttpStatusCode.valueOf(400), exception.getStatusCode()); } @@ -238,8 +238,8 @@ void testGetIsoLines_InvalidMaxTransferNumber() { void testGetIsoLines_InvalidMaxWalkingDuration() { // Act & Assert ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> routingController.getIsolines(null, 0, 0, LocalDateTime.now(), TimeType.DEPARTURE, -30, 2, 120, - 5)); + () -> routingController.getIsolines(null, 0, 0, LocalDateTime.now(), TimeType.DEPARTURE, -30, 2, 120, 5, + false)); assertEquals("Max walking duration must be greater than or equal to 0.", exception.getReason()); assertEquals(HttpStatusCode.valueOf(400), exception.getStatusCode()); } @@ -248,8 +248,8 @@ void testGetIsoLines_InvalidMaxWalkingDuration() { void testGetIsoLines_InvalidMaxTravelTime() { // Act & Assert ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> routingController.getIsolines(null, 0, 0, LocalDateTime.now(), TimeType.DEPARTURE, 30, 2, -120, - 5)); + () -> routingController.getIsolines(null, 0, 0, LocalDateTime.now(), TimeType.DEPARTURE, 30, 2, -120, 5, + false)); assertEquals("Max travel time must be greater than 0.", exception.getReason()); assertEquals(HttpStatusCode.valueOf(400), exception.getStatusCode()); } @@ -258,8 +258,8 @@ void testGetIsoLines_InvalidMaxTravelTime() { void testGetIsoLines_InvalidMinTransferTime() { // Act & Assert ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> routingController.getIsolines(null, 0, 0, LocalDateTime.now(), TimeType.DEPARTURE, 30, 2, 120, - -5)); + () -> routingController.getIsolines(null, 0, 0, LocalDateTime.now(), TimeType.DEPARTURE, 30, 2, 120, -5, + false)); assertEquals("Min transfer time must be greater than or equal to 0.", exception.getReason()); assertEquals(HttpStatusCode.valueOf(400), exception.getStatusCode()); } From d97911b64b533dde070c04ebad356e7439cedac7 Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Fri, 28 Jun 2024 23:17:57 +0200 Subject: [PATCH 4/9] FIX: NAV-104 - Remove FUZZY from TimeTypes defined in openapi.yaml --- src/main/resources/ch.naviqore.app/openapi.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/ch.naviqore.app/openapi.yaml b/src/main/resources/ch.naviqore.app/openapi.yaml index 636c14f1..6d2d5adf 100644 --- a/src/main/resources/ch.naviqore.app/openapi.yaml +++ b/src/main/resources/ch.naviqore.app/openapi.yaml @@ -404,7 +404,6 @@ components: - STARTS_WITH - CONTAINS - ENDS_WITH - - FUZZY TIME_TYPE: type: string enum: From 0dbb756a19b88777ea3b6bd2aae72772b78d8c4e Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Mon, 1 Jul 2024 19:00:51 +0200 Subject: [PATCH 5/9] TEST: NAV-104 - Replace Mockito with DummyServiceClass in RoutingController tests. --- .../naviqore/app/controller/DummyService.java | 368 ++++++++++++++++++ .../app/controller/DummyServiceModels.java | 192 +++++++++ .../app/controller/RoutingControllerTest.java | 40 +- 3 files changed, 567 insertions(+), 33 deletions(-) create mode 100644 src/test/java/ch/naviqore/app/controller/DummyService.java create mode 100644 src/test/java/ch/naviqore/app/controller/DummyServiceModels.java diff --git a/src/test/java/ch/naviqore/app/controller/DummyService.java b/src/test/java/ch/naviqore/app/controller/DummyService.java new file mode 100644 index 00000000..9724c6ac --- /dev/null +++ b/src/test/java/ch/naviqore/app/controller/DummyService.java @@ -0,0 +1,368 @@ +package ch.naviqore.app.controller; + +import ch.naviqore.service.*; +import ch.naviqore.service.config.ConnectionQueryConfig; +import ch.naviqore.service.exception.RouteNotFoundException; +import ch.naviqore.service.exception.StopNotFoundException; +import ch.naviqore.service.exception.TripNotActiveException; +import ch.naviqore.service.exception.TripNotFoundException; +import ch.naviqore.utils.spatial.GeoCoordinate; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.Nullable; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +@NoArgsConstructor +class DummyService implements PublicTransitService { + + static final DummyServiceModels.Stop STOP_A = new DummyServiceModels.Stop("A", "Stop A", new GeoCoordinate(0, 0)); + static final DummyServiceModels.Stop STOP_B = new DummyServiceModels.Stop("B", "Stop B", new GeoCoordinate(1, 1)); + static final DummyServiceModels.Stop STOP_C = new DummyServiceModels.Stop("C", "Stop C", new GeoCoordinate(2, 2)); + static final DummyServiceModels.Stop STOP_D = new DummyServiceModels.Stop("D", "Stop D", new GeoCoordinate(3, 3)); + static final DummyServiceModels.Stop STOP_E = new DummyServiceModels.Stop("E", "Stop E", new GeoCoordinate(4, 4)); + static final DummyServiceModels.Stop STOP_F = new DummyServiceModels.Stop("F", "Stop F", new GeoCoordinate(5, 5)); + static final DummyServiceModels.Stop STOP_G = new DummyServiceModels.Stop("G", "Stop G", new GeoCoordinate(6, 6)); + static final DummyServiceModels.Stop STOP_H = new DummyServiceModels.Stop("H", "Stop H", new GeoCoordinate(7, 7)); + + static final List STOPS = List.of(STOP_A, STOP_B, STOP_C, STOP_D, STOP_E, STOP_F, STOP_G, + STOP_H); + + private record RouteData(DummyServiceModels.Route route, List stops) { + } + + private static final RouteData ROUTE_1 = new RouteData( + new DummyServiceModels.Route("1", "Route 1", "R1", "BUS", "Agency 1"), + List.of(STOP_A, STOP_B, STOP_C, STOP_D, STOP_E, STOP_F, STOP_G)); + private static final RouteData ROUTE_2 = new RouteData( + new DummyServiceModels.Route("2", "Route 2", "R2", "BUS", "Agency 2"), + List.of(STOP_A, STOP_B, STOP_C, STOP_D)); + private static final RouteData ROUTE_3 = new RouteData( + new DummyServiceModels.Route("3", "Route 3", "R3", "BUS", "Agency 3"), + List.of(STOP_D, STOP_E, STOP_F, STOP_G, STOP_H)); + + static final List ROUTES = List.of(ROUTE_1, ROUTE_2, ROUTE_3); + + @Override + public void updateStaticSchedule() { + + } + + @Override + public List getConnections(GeoCoordinate source, GeoCoordinate target, LocalDateTime time, + TimeType timeType, ConnectionQueryConfig config) { + return List.of(DummyConnectionGenerators.getSimpleConnection(source, target, time, timeType)); + } + + @Override + public List getConnections(Stop source, Stop target, LocalDateTime time, TimeType timeType, + ConnectionQueryConfig config) { + List connections = new ArrayList<>(); + connections.add(DummyConnectionGenerators.getSimpleConnection(source, target, time, timeType)); + try { + connections.add( + DummyConnectionGenerators.getConnectionWithSameStopTransfer(source, target, time, timeType)); + } catch (IllegalArgumentException e) { + // ignore + } + return connections; + } + + @Override + public List getConnections(GeoCoordinate source, Stop target, LocalDateTime time, TimeType timeType, + ConnectionQueryConfig config) { + return List.of(DummyConnectionGenerators.getSimpleConnection(source, target, time, timeType)); + } + + @Override + public List getConnections(Stop source, GeoCoordinate target, LocalDateTime time, TimeType timeType, + ConnectionQueryConfig config) { + return List.of(DummyConnectionGenerators.getSimpleConnection(source, target, time, timeType)); + } + + @Override + public Map getIsoLines(GeoCoordinate source, LocalDateTime time, TimeType timeType, + ConnectionQueryConfig config) { + Map connections = new HashMap<>(); + for (Stop stop : STOPS) { + try { + connections.put(stop, DummyConnectionGenerators.getSimpleConnection(source, stop, time, timeType)); + } catch (IllegalArgumentException e) { + // ignore + } + } + return connections; + } + + @Override + public Map getIsoLines(Stop source, LocalDateTime time, TimeType timeType, + ConnectionQueryConfig config) { + Map connections = new HashMap<>(); + for (Stop stop : STOPS) { + if (source == stop) { + continue; + } + try { + connections.put(stop, DummyConnectionGenerators.getSimpleConnection(source, stop, time, timeType)); + } catch (IllegalArgumentException e) { + // ignore + } + } + return connections; + } + + @Override + public List getStops(String like, SearchType searchType) { + return STOPS.stream().map(x -> (Stop) x).toList(); + } + + @Override + public Optional getNearestStop(GeoCoordinate location) { + return Optional.of(STOP_A); + } + + @Override + public List getNearestStops(GeoCoordinate location, int radius, int limit) { + if (radius > 100) { + return List.of(STOP_A, STOP_B, STOP_C); + } else { + return List.of(); + } + } + + @Override + public List getNextDepartures(Stop stop, LocalDateTime from, @Nullable LocalDateTime until, int limit) { + return List.of(); + } + + @Override + public Stop getStopById(String stopId) throws StopNotFoundException { + return STOPS.stream() + .filter(stop -> stop.getId().equals(stopId)) + .findFirst() + .orElseThrow(() -> new StopNotFoundException(stopId)); + } + + @Override + public Trip getTripById(String tripId, LocalDate date) throws TripNotFoundException, TripNotActiveException { + if (tripId.equals("not_existing_trip")) { + throw new TripNotFoundException(tripId); + } else if (date.isEqual(LocalDate.of(2021, 1, 1))) { + throw new TripNotActiveException(tripId, date); + } else { + PublicTransitLeg leg = DummyConnectionGenerators.getPublicTransitLeg(ROUTE_1, STOP_A, STOP_G, + date.atTime(8, 0), TimeType.DEPARTURE); + return leg.getTrip(); + } + } + + @Override + public Route getRouteById(String routeId) throws RouteNotFoundException { + return ROUTES.stream() + .filter(routeData -> routeData.route.getId().equals(routeId)) + .map(routeData -> routeData.route) + .findFirst() + .orElseThrow(() -> new RouteNotFoundException(routeId)); + } + + static class DummyConnectionGenerators { + private static final int SECONDS_BETWEEN_STOPS = 300; + private static final int DISTANCE_BETWEEN_STOPS = 100; + + static DummyServiceModels.Connection getSimpleConnection(Stop startStop, Stop endStop, LocalDateTime date, + TimeType timeType) { + if (startStop == endStop) { + throw new IllegalArgumentException("Start and end stop must be different."); + } else if (endStop == STOP_H) { + // downcast start stop to DummyServiceModels.Stop + return getConnectionWithFinalWalkTransfer((DummyServiceModels.Stop) startStop, + (DummyServiceModels.Stop) endStop, date, timeType); + } + DummyServiceModels.PublicTransitLeg leg = getPublicTransitLeg(ROUTE_1, startStop, endStop, date, timeType); + return new DummyServiceModels.Connection(List.of(leg)); + } + + static DummyServiceModels.Connection getConnectionWithSameStopTransfer(Stop startStop, Stop endStop, + LocalDateTime date, TimeType timeType) { + if (startStop == endStop) { + throw new IllegalArgumentException("Start and end stop must be different."); + } else if (startStop == STOP_D || endStop == STOP_D) { + throw new IllegalArgumentException("Stop D cannot be used for same stop transfer."); + } else if (!ROUTE_3.stops().contains((DummyServiceModels.Stop) endStop)) { + throw new IllegalArgumentException("End stop must be part of Route 3."); + } else if (!ROUTE_2.stops().contains((DummyServiceModels.Stop) startStop)) { + throw new IllegalArgumentException("Start stop must be part of Route 2."); + } + DummyServiceModels.PublicTransitLeg firstLeg; + DummyServiceModels.PublicTransitLeg secondLeg; + if (timeType == TimeType.DEPARTURE) { + firstLeg = getPublicTransitLeg(ROUTE_2, startStop, STOP_D, date, timeType); + LocalDateTime departureSecondLeg = firstLeg.getArrival().getArrivalTime().plusMinutes(5); + secondLeg = getPublicTransitLeg(ROUTE_3, STOP_D, endStop, departureSecondLeg, timeType); + } else { + secondLeg = getPublicTransitLeg(ROUTE_3, STOP_D, endStop, date, timeType); + LocalDateTime departureFirstLeg = secondLeg.getDeparture().getDepartureTime().minusMinutes(5); + firstLeg = getPublicTransitLeg(ROUTE_2, startStop, STOP_D, departureFirstLeg, timeType); + } + + return new DummyServiceModels.Connection(List.of(firstLeg, secondLeg)); + } + + static DummyServiceModels.Connection getConnectionWithFinalWalkTransfer(DummyServiceModels.Stop startStop, + DummyServiceModels.Stop endStop, + LocalDateTime date, TimeType timeType) { + if (startStop == endStop) { + throw new IllegalArgumentException("Start and end stop must be different."); + } + int endStopIndex = STOPS.indexOf(endStop); + if (endStopIndex == -1) { + throw new IllegalArgumentException("End stop not found in stops."); + } + int startStopIndex = STOPS.indexOf(startStop); + if (startStopIndex == -1) { + throw new IllegalArgumentException("Start stop not found in stops."); + } + if (endStopIndex < startStopIndex + 2) { + throw new IllegalArgumentException("End stop must be at least two stops after start stop."); + } + DummyServiceModels.Stop routeEndStop = STOPS.get(endStopIndex - 1); + DummyServiceModels.PublicTransitLeg leg; + DummyServiceModels.Transfer transfer; + if (timeType == TimeType.DEPARTURE) { + leg = getPublicTransitLeg(ROUTE_1, startStop, routeEndStop, date, timeType); + LocalDateTime departureWalk = leg.getArrival().getArrivalTime(); + int duration = 2 * SECONDS_BETWEEN_STOPS; + transfer = new DummyServiceModels.Transfer(DISTANCE_BETWEEN_STOPS, duration, departureWalk, + departureWalk.plusSeconds(duration), routeEndStop, endStop); + } else { + int duration = 2 * SECONDS_BETWEEN_STOPS; + transfer = new DummyServiceModels.Transfer(DISTANCE_BETWEEN_STOPS, duration, + date.minusSeconds(duration), date, startStop, routeEndStop); + leg = getPublicTransitLeg(ROUTE_1, routeEndStop, endStop, date.minusSeconds(duration), timeType); + + } + return new DummyServiceModels.Connection(List.of(leg, transfer)); + } + + static DummyServiceModels.Connection getSimpleConnection(GeoCoordinate startCoordinate, Stop endStop, + LocalDateTime date, TimeType timeType) { + if (!ROUTE_1.stops().contains((DummyServiceModels.Stop) endStop)) { + throw new IllegalArgumentException("End stop must be part of Route 1."); + } else if (endStop == STOP_A) { + throw new IllegalArgumentException("Stop A cannot be used as end stop."); + } + DummyServiceModels.Stop routeStartStop = STOP_A; + DummyServiceModels.Walk walk; + DummyServiceModels.PublicTransitLeg leg; + int walkDuration = 2 * SECONDS_BETWEEN_STOPS; + if (timeType == TimeType.DEPARTURE) { + walk = new DummyServiceModels.Walk(DISTANCE_BETWEEN_STOPS, walkDuration, WalkType.FIRST_MILE, date, + date.plusSeconds(walkDuration), startCoordinate, routeStartStop.getLocation(), routeStartStop); + leg = getPublicTransitLeg(ROUTE_1, routeStartStop, endStop, date.plusSeconds(walkDuration), timeType); + } else { + leg = getPublicTransitLeg(ROUTE_1, routeStartStop, endStop, date, timeType); + LocalDateTime legArrival = leg.getArrival().getArrivalTime(); + walk = new DummyServiceModels.Walk(DISTANCE_BETWEEN_STOPS, walkDuration, WalkType.FIRST_MILE, + legArrival.minusSeconds(walkDuration), legArrival, routeStartStop.getLocation(), + endStop.getLocation(), routeStartStop); + } + return new DummyServiceModels.Connection(List.of(walk, leg)); + + } + + static DummyServiceModels.Connection getSimpleConnection(Stop startStop, GeoCoordinate endCoordinate, + LocalDateTime date, TimeType timeType) { + if (!ROUTE_1.stops().contains((DummyServiceModels.Stop) startStop)) { + throw new IllegalArgumentException("End stop must be part of Route 1."); + } else if (startStop == STOP_G) { + throw new IllegalArgumentException("Stop G cannot be used as start stop."); + } + DummyServiceModels.Stop routeEndStop = STOP_G; + DummyServiceModels.Walk walk; + DummyServiceModels.PublicTransitLeg leg; + int walkDuration = 2 * SECONDS_BETWEEN_STOPS; + if (timeType == TimeType.DEPARTURE) { + leg = getPublicTransitLeg(ROUTE_1, startStop, routeEndStop, date, timeType); + LocalDateTime legArrival = leg.getArrival().getArrivalTime(); + walk = new DummyServiceModels.Walk(DISTANCE_BETWEEN_STOPS, walkDuration, WalkType.LAST_MILE, legArrival, + legArrival.plusSeconds(walkDuration), routeEndStop.getLocation(), endCoordinate, routeEndStop); + } else { + LocalDateTime walkDeparture = date.minusSeconds(walkDuration); + walk = new DummyServiceModels.Walk(DISTANCE_BETWEEN_STOPS, walkDuration, WalkType.LAST_MILE, + walkDeparture, date, routeEndStop.getLocation(), endCoordinate, routeEndStop); + leg = getPublicTransitLeg(ROUTE_1, startStop, routeEndStop, walkDeparture, timeType); + } + return new DummyServiceModels.Connection(List.of(leg, walk)); + } + + static DummyServiceModels.Connection getSimpleConnection(GeoCoordinate startCoordinate, + GeoCoordinate endCoordinate, LocalDateTime date, + TimeType timeType) { + DummyServiceModels.Stop routeStartStop = STOP_A; + DummyServiceModels.Stop routeEndStop = STOP_G; + DummyServiceModels.Walk firstWalk; + DummyServiceModels.Walk lastWalk; + DummyServiceModels.PublicTransitLeg leg; + int walkDuration = 2 * SECONDS_BETWEEN_STOPS; + if (timeType == TimeType.DEPARTURE) { + firstWalk = new DummyServiceModels.Walk(DISTANCE_BETWEEN_STOPS, walkDuration, WalkType.FIRST_MILE, date, + date.plusSeconds(walkDuration), startCoordinate, routeStartStop.getLocation(), routeStartStop); + leg = getPublicTransitLeg(ROUTE_1, routeStartStop, routeEndStop, date.plusSeconds(walkDuration), + timeType); + LocalDateTime legArrival = leg.getArrival().getArrivalTime(); + lastWalk = new DummyServiceModels.Walk(DISTANCE_BETWEEN_STOPS, walkDuration, WalkType.LAST_MILE, + legArrival, legArrival.plusSeconds(walkDuration), routeEndStop.getLocation(), endCoordinate, + routeEndStop); + } else { + LocalDateTime walkDeparture = date.minusSeconds(walkDuration); + lastWalk = new DummyServiceModels.Walk(DISTANCE_BETWEEN_STOPS, walkDuration, WalkType.LAST_MILE, + walkDeparture, date, routeEndStop.getLocation(), endCoordinate, routeEndStop); + leg = getPublicTransitLeg(ROUTE_1, routeStartStop, routeEndStop, walkDeparture, timeType); + LocalDateTime legDeparture = leg.getDeparture().getDepartureTime(); + firstWalk = new DummyServiceModels.Walk(DISTANCE_BETWEEN_STOPS, walkDuration, WalkType.FIRST_MILE, + legDeparture.minusSeconds(walkDuration), legDeparture, startCoordinate, + routeStartStop.getLocation(), routeStartStop); + } + return new DummyServiceModels.Connection(List.of(firstWalk, leg, lastWalk)); + } + + private static DummyServiceModels.PublicTransitLeg getPublicTransitLeg(RouteData route, Stop startStop, + Stop endStop, LocalDateTime startTime, + TimeType timeType) { + // get index of reference stop in route.stops + int startStopIndex = route.stops().indexOf((DummyServiceModels.Stop) startStop); + if (startStopIndex == -1) { + throw new IllegalArgumentException("Start stop not found in route."); + } + int endStopIndex = route.stops().indexOf((DummyServiceModels.Stop) endStop); + if (endStopIndex == -1) { + throw new IllegalArgumentException("End stop not found in route."); + } else if (endStopIndex < startStopIndex) { + throw new IllegalArgumentException("End stop must be after start stop."); + } + + int refIndex = timeType == TimeType.DEPARTURE ? startStopIndex : endStopIndex; + + DummyServiceModels.Trip trip = new DummyServiceModels.Trip(route.route().getId() + "_" + startStop.getId(), + "Head Sign", route.route()); + List stopTimes = new ArrayList<>(); + for (int i = 0; i < route.stops().size(); i++) { + DummyServiceModels.Stop stop = route.stops().get(i); + LocalDateTime arrivalTime = startTime.plusSeconds((long) SECONDS_BETWEEN_STOPS * (i - refIndex)); + stopTimes.add(new DummyServiceModels.StopTime(stop, arrivalTime, arrivalTime, trip)); + } + trip.setStopTimes(stopTimes); + + DummyServiceModels.StopTime departure = (DummyServiceModels.StopTime) stopTimes.get(startStopIndex); + DummyServiceModels.StopTime arrival = (DummyServiceModels.StopTime) stopTimes.get(endStopIndex); + + int duration = SECONDS_BETWEEN_STOPS * (endStopIndex - startStopIndex); + int distance = DISTANCE_BETWEEN_STOPS * (endStopIndex - startStopIndex); + + return new DummyServiceModels.PublicTransitLeg(distance, duration, trip, departure, arrival); + } + + } + +} diff --git a/src/test/java/ch/naviqore/app/controller/DummyServiceModels.java b/src/test/java/ch/naviqore/app/controller/DummyServiceModels.java new file mode 100644 index 00000000..818f5a32 --- /dev/null +++ b/src/test/java/ch/naviqore/app/controller/DummyServiceModels.java @@ -0,0 +1,192 @@ +package ch.naviqore.app.controller; + +import ch.naviqore.service.LegType; +import ch.naviqore.service.LegVisitor; +import ch.naviqore.service.WalkType; +import ch.naviqore.utils.spatial.GeoCoordinate; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.jetbrains.annotations.Nullable; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +class DummyServiceModels { + + @RequiredArgsConstructor + @Getter + static abstract class Leg implements ch.naviqore.service.Leg { + private final LegType legType; + private final int distance; + private final int duration; + + @Override + public abstract T accept(LegVisitor visitor); + } + + @Getter + static class PublicTransitLeg extends Leg implements ch.naviqore.service.PublicTransitLeg { + + private final Trip trip; + private final StopTime departure; + private final StopTime arrival; + + PublicTransitLeg(int distance, int duration, Trip trip, StopTime departure, StopTime arrival) { + super(LegType.PUBLIC_TRANSIT, distance, duration); + this.trip = trip; + this.departure = departure; + this.arrival = arrival; + } + + @Override + public T accept(LegVisitor visitor) { + return visitor.visit(this); + } + } + + @Getter + static class Transfer extends Leg implements ch.naviqore.service.Transfer { + private final LocalDateTime departureTime; + private final LocalDateTime arrivalTime; + private final Stop sourceStop; + private final Stop targetStop; + + Transfer(int distance, int duration, LocalDateTime departureTime, LocalDateTime arrivalTime, Stop sourceStop, + Stop targetStop) { + super(LegType.WALK, distance, duration); + this.departureTime = departureTime; + this.arrivalTime = arrivalTime; + this.sourceStop = sourceStop; + this.targetStop = targetStop; + } + + @Override + public T accept(LegVisitor visitor) { + return visitor.visit(this); + } + } + + @Getter + static class Walk extends Leg implements ch.naviqore.service.Walk { + private final WalkType walkType; + private final LocalDateTime departureTime; + private final LocalDateTime arrivalTime; + private final GeoCoordinate sourceLocation; + private final GeoCoordinate targetLocation; + private final Stop stop; + + Walk(int distance, int duration, WalkType walkType, LocalDateTime departureTime, LocalDateTime arrivalTime, + GeoCoordinate sourceLocation, GeoCoordinate targetLocation, @Nullable Stop stop) { + super(LegType.WALK, distance, duration); + this.walkType = walkType; + this.departureTime = departureTime; + this.arrivalTime = arrivalTime; + this.sourceLocation = sourceLocation; + this.targetLocation = targetLocation; + this.stop = stop; + } + + @Override + public T accept(LegVisitor visitor) { + return visitor.visit(this); + } + + @Override + public Optional getStop() { + return Optional.ofNullable(stop); + } + } + + @RequiredArgsConstructor + @Getter + static class Route implements ch.naviqore.service.Route { + private final String id; + private final String name; + private final String shortName; + private final String routeType; + private final String Agency; + + } + + @RequiredArgsConstructor + @Getter + static class Trip implements ch.naviqore.service.Trip { + private final String id; + private final String headSign; + private final Route route; + @Setter + private List stopTimes; + + } + + @RequiredArgsConstructor + @Getter + static class Stop implements ch.naviqore.service.Stop { + private final String id; + private final String name; + private final GeoCoordinate location; + + } + + @RequiredArgsConstructor + @Getter + static class StopTime implements ch.naviqore.service.StopTime { + private final Stop stop; + private final LocalDateTime arrivalTime; + private final LocalDateTime departureTime; + private final transient Trip trip; + } + + @RequiredArgsConstructor + static class Connection implements ch.naviqore.service.Connection { + + private final List legs; + + @Override + public List getLegs() { + return legs.stream().map(leg -> (ch.naviqore.service.Leg) leg).toList(); + } + + @Override + public LocalDateTime getDepartureTime() { + return legs.getFirst().accept(new LegVisitor<>() { + @Override + public LocalDateTime visit(ch.naviqore.service.PublicTransitLeg publicTransitLeg) { + return publicTransitLeg.getDeparture().getDepartureTime(); + } + + @Override + public LocalDateTime visit(ch.naviqore.service.Transfer transfer) { + return transfer.getDepartureTime(); + } + + @Override + public LocalDateTime visit(ch.naviqore.service.Walk walk) { + return walk.getDepartureTime(); + } + }); + } + + @Override + public LocalDateTime getArrivalTime() { + return legs.getLast().accept(new LegVisitor<>() { + @Override + public LocalDateTime visit(ch.naviqore.service.PublicTransitLeg publicTransitLeg) { + return publicTransitLeg.getArrival().getArrivalTime(); + } + + @Override + public LocalDateTime visit(ch.naviqore.service.Transfer transfer) { + return transfer.getArrivalTime(); + } + + @Override + public LocalDateTime visit(ch.naviqore.service.Walk walk) { + return walk.getArrivalTime(); + } + }); + } + } +} diff --git a/src/test/java/ch/naviqore/app/controller/RoutingControllerTest.java b/src/test/java/ch/naviqore/app/controller/RoutingControllerTest.java index 83ccd80f..49105cce 100644 --- a/src/test/java/ch/naviqore/app/controller/RoutingControllerTest.java +++ b/src/test/java/ch/naviqore/app/controller/RoutingControllerTest.java @@ -25,30 +25,19 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@ExtendWith(MockitoExtension.class) public class RoutingControllerTest { - @Mock - private PublicTransitService publicTransitService; + private final DummyService dummyService = new DummyService(); - @InjectMocks - private RoutingController routingController; + private final RoutingController routingController = new RoutingController(dummyService); @Test void testGetConnections_WithValidSourceAndTargetStopIds() throws StopNotFoundException { // Arrange - String sourceStopId = "sourceStopId"; - String targetStopId = "targetStopId"; + String sourceStopId = "A"; + String targetStopId = "G"; LocalDateTime departureDateTime = LocalDateTime.now(); - Stop sourceStop = mock(Stop.class); - Stop targetStop = mock(Stop.class); - - when(publicTransitService.getStopById(sourceStopId)).thenReturn(sourceStop); - when(publicTransitService.getStopById(targetStopId)).thenReturn(targetStop); - when(publicTransitService.getConnections(eq(sourceStop), eq(targetStop), any(), any(), any())).thenReturn( - Collections.emptyList()); - // Act List connections = routingController.getConnections(sourceStopId, -1.0, -1.0, targetStopId, -1.0, -1.0, departureDateTime, TimeType.DEPARTURE, 30, 2, 120, 5); @@ -62,15 +51,9 @@ void testGetConnections_WithoutSourceStopIdButWithCoordinates() throws StopNotFo // Arrange double sourceLatitude = 46.2044; double sourceLongitude = 6.1432; - String targetStopId = "targetStopId"; + String targetStopId = "G"; LocalDateTime departureDateTime = LocalDateTime.now(); - Stop targetStop = mock(Stop.class); - - when(publicTransitService.getStopById(targetStopId)).thenReturn(targetStop); - when(publicTransitService.getConnections(any(GeoCoordinate.class), eq(targetStop), any(), any(), - any())).thenReturn(Collections.emptyList()); - // Act List connections = routingController.getConnections(null, sourceLatitude, sourceLongitude, targetStopId, -1.0, -1.0, departureDateTime, TimeType.DEPARTURE, 30, 2, 120, 5); @@ -83,9 +66,7 @@ void testGetConnections_WithoutSourceStopIdButWithCoordinates() throws StopNotFo void testGetConnections_InvalidStopId() throws StopNotFoundException { // Arrange String invalidStopId = "invalidStopId"; - String targetStopId = "targetStopId"; - - when(publicTransitService.getStopById(invalidStopId)).thenThrow(new StopNotFoundException(invalidStopId)); + String targetStopId = "G"; // Act & Assert ResponseStatusException exception = assertThrows(ResponseStatusException.class, @@ -171,15 +152,9 @@ void testGetConnections_InvalidMinTransferTime() { @Test void testGetIsoLines() throws StopNotFoundException { // Arrange - String sourceStopId = "sourceStopId"; + String sourceStopId = "A"; LocalDateTime time = LocalDateTime.now(); - Stop sourceStop = mock(Stop.class); - - when(publicTransitService.getStopById(sourceStopId)).thenReturn(sourceStop); - when(publicTransitService.getIsoLines(eq(sourceStop), eq(time), any(), any())).thenReturn( - Collections.emptyMap()); - // Act List connections = routingController.getIsolines(sourceStopId, -1.0, -1.0, time, TimeType.DEPARTURE, 30, 2, 120, 5, false); @@ -192,7 +167,6 @@ void testGetIsoLines() throws StopNotFoundException { void testGetIsoLines_InvalidSourceStopId() throws StopNotFoundException { // Arrange String invalidStopId = "invalidStopId"; - when(publicTransitService.getStopById(invalidStopId)).thenThrow(new StopNotFoundException(invalidStopId)); // Act & Assert ResponseStatusException exception = assertThrows(ResponseStatusException.class, From 37d6a32313343cbeccef524121bf49406d97911e Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Mon, 1 Jul 2024 23:05:06 +0200 Subject: [PATCH 6/9] FIX: NAV-104 - Fix Arrival Type IsoLines in DummyService. --- .../ch/naviqore/app/controller/DummyService.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/test/java/ch/naviqore/app/controller/DummyService.java b/src/test/java/ch/naviqore/app/controller/DummyService.java index 9724c6ac..927841d7 100644 --- a/src/test/java/ch/naviqore/app/controller/DummyService.java +++ b/src/test/java/ch/naviqore/app/controller/DummyService.java @@ -87,7 +87,13 @@ public Map getIsoLines(GeoCoordinate source, LocalDateTime tim Map connections = new HashMap<>(); for (Stop stop : STOPS) { try { - connections.put(stop, DummyConnectionGenerators.getSimpleConnection(source, stop, time, timeType)); + if (timeType == TimeType.DEPARTURE) { + connections.put(stop, + DummyConnectionGenerators.getSimpleConnection(source, stop, time, timeType)); + } else { + connections.put(stop, + DummyConnectionGenerators.getSimpleConnection(stop, source, time, timeType)); + } } catch (IllegalArgumentException e) { // ignore } @@ -104,7 +110,13 @@ public Map getIsoLines(Stop source, LocalDateTime time, TimeTy continue; } try { - connections.put(stop, DummyConnectionGenerators.getSimpleConnection(source, stop, time, timeType)); + if (timeType == TimeType.DEPARTURE) { + connections.put(stop, + DummyConnectionGenerators.getSimpleConnection(source, stop, time, timeType)); + } else { + connections.put(stop, + DummyConnectionGenerators.getSimpleConnection(stop, source, time, timeType)); + } } catch (IllegalArgumentException e) { // ignore } From 1acdde0a0b570726b27465ed0a3a6b409e056373 Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Mon, 1 Jul 2024 23:06:52 +0200 Subject: [PATCH 7/9] TEST: NAV-104 - Add more tests to test StopConnection creation for IsoLines. --- .../app/controller/RoutingControllerTest.java | 246 ++++++++++++++++-- 1 file changed, 223 insertions(+), 23 deletions(-) diff --git a/src/test/java/ch/naviqore/app/controller/RoutingControllerTest.java b/src/test/java/ch/naviqore/app/controller/RoutingControllerTest.java index 49105cce..e83098c6 100644 --- a/src/test/java/ch/naviqore/app/controller/RoutingControllerTest.java +++ b/src/test/java/ch/naviqore/app/controller/RoutingControllerTest.java @@ -1,29 +1,15 @@ package ch.naviqore.app.controller; -import ch.naviqore.app.dto.Connection; -import ch.naviqore.app.dto.StopConnection; -import ch.naviqore.app.dto.TimeType; -import ch.naviqore.service.PublicTransitService; -import ch.naviqore.service.Stop; -import ch.naviqore.service.exception.StopNotFoundException; +import ch.naviqore.app.dto.*; import ch.naviqore.utils.spatial.GeoCoordinate; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatusCode; import org.springframework.web.server.ResponseStatusException; import java.time.LocalDateTime; -import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class RoutingControllerTest { @@ -32,7 +18,7 @@ public class RoutingControllerTest { private final RoutingController routingController = new RoutingController(dummyService); @Test - void testGetConnections_WithValidSourceAndTargetStopIds() throws StopNotFoundException { + void testGetConnections_WithValidSourceAndTargetStopIds() { // Arrange String sourceStopId = "A"; String targetStopId = "G"; @@ -47,7 +33,7 @@ void testGetConnections_WithValidSourceAndTargetStopIds() throws StopNotFoundExc } @Test - void testGetConnections_WithoutSourceStopIdButWithCoordinates() throws StopNotFoundException { + void testGetConnections_WithoutSourceStopIdButWithCoordinates() { // Arrange double sourceLatitude = 46.2044; double sourceLongitude = 6.1432; @@ -63,7 +49,7 @@ void testGetConnections_WithoutSourceStopIdButWithCoordinates() throws StopNotFo } @Test - void testGetConnections_InvalidStopId() throws StopNotFoundException { + void testGetConnections_InvalidStopId() { // Arrange String invalidStopId = "invalidStopId"; String targetStopId = "G"; @@ -150,21 +136,235 @@ void testGetConnections_InvalidMinTransferTime() { } @Test - void testGetIsoLines() throws StopNotFoundException { + void testGetIsoLines_fromStopReturnConnectionsFalse() { // Arrange String sourceStopId = "A"; LocalDateTime time = LocalDateTime.now(); // Act - List connections = routingController.getIsolines(sourceStopId, -1.0, -1.0, time, + List stopConnections = routingController.getIsolines(sourceStopId, -1.0, -1.0, time, TimeType.DEPARTURE, 30, 2, 120, 5, false); - // Assert - assertNotNull(connections); + assertNotNull(stopConnections); + + for (StopConnection stopConnection : stopConnections) { + assertEquals(stopConnection.getStop(), stopConnection.getConnectingLeg().getToStop()); + // because returnConnections == false + assertNull(stopConnection.getConnection()); + Trip trip = stopConnection.getConnectingLeg().getTrip(); + if (trip != null) { + assertNull(trip.getStopTimes()); + } + } + } + + @Test + void testGetIsoLines_fromStopReturnConnectionsTrue() { + // Arrange + String sourceStopId = "A"; + // This tests if the time is set to now if null + LocalDateTime expectedStartTime = LocalDateTime.now(); + + List stopConnections = routingController.getIsolines(sourceStopId, -1.0, -1.0, null, + TimeType.DEPARTURE, 30, 2, 120, 5, true); + + assertNotNull(stopConnections); + + for (StopConnection stopConnection : stopConnections) { + assertEquals(stopConnection.getStop(), stopConnection.getConnectingLeg().getToStop()); + // because returnConnections == true + assertNotNull(stopConnection.getConnection()); + assertEquals(stopConnection.getStop(), stopConnection.getConnection().getLegs().getLast().getToStop()); + Connection connection = stopConnection.getConnection(); + // make sure each connection has a departure time after/equal the expected start time + assertFalse(connection.getLegs().getFirst().getDepartureTime().isBefore(expectedStartTime)); + assertEquals(connection.getLegs().getFirst().getFromStop().getId(), sourceStopId); + + Trip trip = stopConnection.getConnectingLeg().getTrip(); + if (trip != null) { + List stopTimes = trip.getStopTimes(); + assertNotNull(stopTimes); + // find index of the stopConnection.getStop() in the stopTimes + int index = -1; + for (int i = 0; i < stopTimes.size(); i++) { + if (stopTimes.get(i).getStop().equals(stopConnection.getStop())) { + index = i; + break; + } + } + if (index == -1) { + fail("Stop not found in trip stop times"); + } + // check if the previous stop in the connecting leg is the same as the previous stop in the trip + assertEquals(stopTimes.get(index - 1).getStop(), stopConnection.getConnectingLeg().getFromStop()); + } + } + } + + @Test + void testGetIsoLines_fromCoordinatesReturnConnectionsFalse() { + // Arrange + double sourceLatitude = 46.2044; + double sourceLongitude = 6.1432; + LocalDateTime time = LocalDateTime.now(); + + // Act + List stopConnections = routingController.getIsolines(null, sourceLatitude, sourceLongitude, + time, TimeType.DEPARTURE, 30, 2, 120, 5, false); + + assertNotNull(stopConnections); + + for (StopConnection stopConnection : stopConnections) { + assertEquals(stopConnection.getStop(), stopConnection.getConnectingLeg().getToStop()); + // because returnConnections == false + assertNull(stopConnection.getConnection()); + Trip trip = stopConnection.getConnectingLeg().getTrip(); + if (trip != null) { + assertNull(trip.getStopTimes()); + } + } + } + + @Test + void testGetIsoLines_fromCoordinateReturnConnectionsTrue() { + // Arrange + GeoCoordinate sourceCoordinate = new GeoCoordinate(46.2044, 6.1432); + // This tests if the time is set to now if null + LocalDateTime expectedStartTime = LocalDateTime.now(); + + List stopConnections = routingController.getIsolines(null, sourceCoordinate.latitude(), + sourceCoordinate.longitude(), null, TimeType.DEPARTURE, 30, 2, 120, 5, true); + + assertNotNull(stopConnections); + + for (StopConnection stopConnection : stopConnections) { + assertEquals(stopConnection.getStop(), stopConnection.getConnectingLeg().getToStop()); + // because returnConnections == true + assertNotNull(stopConnection.getConnection()); + assertEquals(stopConnection.getStop(), stopConnection.getConnection().getLegs().getLast().getToStop()); + Connection connection = stopConnection.getConnection(); + // make sure each connection has a departure time after/equal the expected start time + assertFalse(connection.getLegs().getFirst().getDepartureTime().isBefore(expectedStartTime)); + assertNull(connection.getLegs().getFirst().getFromStop()); + assertEquals(connection.getLegs().getFirst().getFrom(), sourceCoordinate); + + Trip trip = stopConnection.getConnectingLeg().getTrip(); + if (trip != null) { + List stopTimes = trip.getStopTimes(); + assertNotNull(stopTimes); + // find index of the stopConnection.getStop() in the stopTimes + int index = -1; + for (int i = 0; i < stopTimes.size(); i++) { + if (stopTimes.get(i).getStop().equals(stopConnection.getStop())) { + index = i; + break; + } + } + if (index == -1) { + fail("Stop not found in trip stop times"); + } + // check if the previous stop in the connecting leg is the same as the previous stop in the trip + assertEquals(stopTimes.get(index - 1).getStop(), stopConnection.getConnectingLeg().getFromStop()); + } + } + } + + @Test + void testGetIsoLines_fromStopReturnConnectionsTrueTimeTypeArrival() { + // Arrange + String sourceStopId = "G"; + + List stopConnections = routingController.getIsolines(sourceStopId, -1.0, -1.0, null, + TimeType.ARRIVAL, 30, 2, 120, 5, true); + + // This tests if the time is set to now if null + LocalDateTime expectedArrivalTime = LocalDateTime.now(); + assertNotNull(stopConnections); + + for (StopConnection stopConnection : stopConnections) { + Leg connectingLeg = stopConnection.getConnectingLeg(); + Connection connection = stopConnection.getConnection(); + + assertEquals(stopConnection.getStop(), connectingLeg.getFromStop()); + // because returnConnections == true + assertNotNull(connection); + assertEquals(stopConnection.getStop(), connection.getLegs().getFirst().getFromStop()); + + // make sure each connection has an arrival time after/equal the expected start time + assertFalse(connection.getLegs().getLast().getArrivalTime().isAfter(expectedArrivalTime)); + assertEquals(connection.getLegs().getLast().getToStop().getId(), sourceStopId); + + Trip trip = connectingLeg.getTrip(); + if (trip != null) { + List stopTimes = trip.getStopTimes(); + assertNotNull(stopTimes); + // find index of the stopConnection.getStop() in the stopTimes + int index = -1; + for (int i = 0; i < stopTimes.size(); i++) { + if (stopTimes.get(i).getStop().equals(stopConnection.getStop())) { + index = i; + break; + } + } + if (index == -1) { + fail("Stop not found in trip stop times"); + } + // check if the target stop in the connecting leg is the same as the next stop in the trip + assertEquals(stopTimes.get(index + 1).getStop(), connectingLeg.getToStop()); + } + } + } + + @Test + void testGetIsoLines_fromCoordinateReturnConnectionsTrueTimeTypeArrival() { + // Arrange + GeoCoordinate sourceCoordinate = new GeoCoordinate(46.2044, 6.1432); + + List stopConnections = routingController.getIsolines(null, sourceCoordinate.latitude(), + sourceCoordinate.longitude(), null, TimeType.ARRIVAL, 30, 2, 120, 5, true); + + // This tests if the time is set to now if null + LocalDateTime expectedArrivalTime = LocalDateTime.now(); + assertNotNull(stopConnections); + + for (StopConnection stopConnection : stopConnections) { + Leg connectingLeg = stopConnection.getConnectingLeg(); + Connection connection = stopConnection.getConnection(); + + assertEquals(stopConnection.getStop(), connectingLeg.getFromStop()); + // because returnConnections == true + assertNotNull(connection); + assertEquals(stopConnection.getStop(), connection.getLegs().getFirst().getFromStop()); + assertEquals(sourceCoordinate, connection.getLegs().getLast().getTo()); + // should be walk transfer from location without stop object + assertNull(connection.getLegs().getLast().getToStop()); + + // make sure each connection has an arrival time before/equal the expected start time + assertFalse(connection.getLegs().getLast().getArrivalTime().isAfter(expectedArrivalTime)); + + Trip trip = connectingLeg.getTrip(); + if (trip != null) { + List stopTimes = trip.getStopTimes(); + assertNotNull(stopTimes); + // find index of the stopConnection.getStop() in the stopTimes + int index = -1; + for (int i = 0; i < stopTimes.size(); i++) { + if (stopTimes.get(i).getStop().equals(stopConnection.getStop())) { + index = i; + break; + } + } + if (index == -1) { + fail("Stop not found in trip stop times"); + } + // check if the target stop in the connecting leg is the same as the next stop in the trip + assertEquals(stopTimes.get(index + 1).getStop(), connectingLeg.getToStop()); + } + } } @Test - void testGetIsoLines_InvalidSourceStopId() throws StopNotFoundException { + void testGetIsoLines_InvalidSourceStopId() { // Arrange String invalidStopId = "invalidStopId"; From 4be8412dbb998ba411861e72461f6cb01ec96fcc Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Mon, 1 Jul 2024 23:07:31 +0200 Subject: [PATCH 8/9] REFACTOR: NAV-104 - Move StopConnection logic to object itself instead of DtoMapper. --- .../app/controller/RoutingController.java | 2 +- .../java/ch/naviqore/app/dto/DtoMapper.java | 59 ---------- .../ch/naviqore/app/dto/StopConnection.java | 106 +++++++++++++++++- 3 files changed, 104 insertions(+), 63 deletions(-) diff --git a/src/main/java/ch/naviqore/app/controller/RoutingController.java b/src/main/java/ch/naviqore/app/controller/RoutingController.java index 97df658d..8451fbe0 100644 --- a/src/main/java/ch/naviqore/app/controller/RoutingController.java +++ b/src/main/java/ch/naviqore/app/controller/RoutingController.java @@ -169,7 +169,7 @@ public List getIsolines(@RequestParam(required = false) String s for (Map.Entry entry : connections.entrySet()) { Stop stop = entry.getKey(); ch.naviqore.service.Connection connection = entry.getValue(); - arrivals.add(map(stop, connection, map(timeType), returnConnections)); + arrivals.add(new StopConnection(stop, connection, timeType, returnConnections)); } return arrivals; diff --git a/src/main/java/ch/naviqore/app/dto/DtoMapper.java b/src/main/java/ch/naviqore/app/dto/DtoMapper.java index 4aa64da0..c4a27683 100644 --- a/src/main/java/ch/naviqore/app/dto/DtoMapper.java +++ b/src/main/java/ch/naviqore/app/dto/DtoMapper.java @@ -6,7 +6,6 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; import java.util.List; @NoArgsConstructor(access = AccessLevel.PRIVATE) @@ -61,64 +60,6 @@ public static Connection map(ch.naviqore.service.Connection connection) { return new Connection(legs); } - public static StopConnection map(ch.naviqore.service.Stop stop, ch.naviqore.service.Connection serviceConnection, - TimeType timeType, boolean returnConnections) { - - Connection connection = map(serviceConnection); - Leg connectingLeg; - - if (timeType == TimeType.DEPARTURE) { - connectingLeg = connection.getLegs().getLast(); - // create a leg from stop before arrival stop (on trip) to the arrival stop - if (connectingLeg.getTrip() != null) { - int stopTimeIndex = findStopTimeIndexInTrip(connectingLeg.getTrip(), connectingLeg.getToStop(), - connectingLeg.getArrivalTime(), TimeType.ARRIVAL); - StopTime sourceStopTime = connectingLeg.getTrip().getStopTimes().get(stopTimeIndex - 1); - connectingLeg = new Leg(connectingLeg.getType(), sourceStopTime.getStop().getCoordinates(), - connectingLeg.getTo(), sourceStopTime.getStop(), connectingLeg.getToStop(), - sourceStopTime.getDepartureTime(), connectingLeg.getArrivalTime(), connectingLeg.getTrip()); - } - } else { - connectingLeg = connection.getLegs().getFirst(); - // create a leg from the departure stop to the first stop on the trip - if (connectingLeg.getTrip() != null) { - int stopTimeIndex = findStopTimeIndexInTrip(connectingLeg.getTrip(), connectingLeg.getFromStop(), - connectingLeg.getDepartureTime(), TimeType.DEPARTURE); - StopTime targetStopTime = connectingLeg.getTrip().getStopTimes().get(stopTimeIndex + 1); - connectingLeg = new Leg(connectingLeg.getType(), connectingLeg.getFrom(), - targetStopTime.getStop().getCoordinates(), connectingLeg.getFromStop(), - targetStopTime.getStop(), connectingLeg.getDepartureTime(), targetStopTime.getArrivalTime(), - connectingLeg.getTrip()); - } - } - - if( connectingLeg.getTrip() != null && ! returnConnections ) { - // nullify stop times from trips if connections are not requested (reducing payload) - Trip reducedTrip = new Trip(connectingLeg.getTrip().getHeadSign(), connectingLeg.getTrip().getRoute(), - null); - connectingLeg = new Leg(connectingLeg.getType(), connectingLeg.getFrom(), connectingLeg.getTo(), - connectingLeg.getFromStop(), connectingLeg.getToStop(), connectingLeg.getDepartureTime(), - connectingLeg.getArrivalTime(), reducedTrip); - } - - return new StopConnection(map(stop), connectingLeg, returnConnections ? connection : null); - } - - private static int findStopTimeIndexInTrip(Trip trip, Stop stop, LocalDateTime time, TimeType timeType) { - List stopTimes = trip.getStopTimes(); - for (int i = 0; i < stopTimes.size(); i++) { - StopTime stopTime = stopTimes.get(i); - if (stopTime.getStop().equals(stop)) { - if (timeType == TimeType.DEPARTURE && stopTime.getDepartureTime().equals(time)) { - return i; - } else if (timeType == TimeType.ARRIVAL && stopTime.getArrivalTime().equals(time)) { - return i; - } - } - } - throw new IllegalStateException("Stop time not found in trip."); - } - private static class LegVisitorImpl implements LegVisitor { @Override public Leg visit(PublicTransitLeg publicTransitLeg) { diff --git a/src/main/java/ch/naviqore/app/dto/StopConnection.java b/src/main/java/ch/naviqore/app/dto/StopConnection.java index 48f5c093..6ee81b59 100644 --- a/src/main/java/ch/naviqore/app/dto/StopConnection.java +++ b/src/main/java/ch/naviqore/app/dto/StopConnection.java @@ -1,10 +1,14 @@ package ch.naviqore.app.dto; import lombok.*; -import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; +import java.util.List; +/** + * This class represents a connection between a stop and a spawn source (iso-line) in a transportation network. It + * contains information about the stop, the leg closest to the target stop, and the connection itself. + */ @RequiredArgsConstructor(access = AccessLevel.PACKAGE) @EqualsAndHashCode @ToString @@ -12,8 +16,104 @@ public class StopConnection { private final Stop stop; - private final Leg connectingLeg; - private final Connection connection; + private Leg connectingLeg; + private Connection connection; + + /** + * Constructs a new StopConnection object. + * + * @param serviceStop The stop from the service. + * @param serviceConnection The connection from the service. + * @param timeType The type of time (DEPARTURE or ARRIVAL), needed to construct the connecting leg. + * @param returnConnections A boolean indicating whether to return connections and trip stop times. + */ + public StopConnection(ch.naviqore.service.Stop serviceStop, ch.naviqore.service.Connection serviceConnection, + TimeType timeType, boolean returnConnections) { + + this.stop = DtoMapper.map(serviceStop); + this.connection = DtoMapper.map(serviceConnection); + if (timeType == TimeType.DEPARTURE) { + prepareDepartureConnectingLeg(); + } else { + prepareArrivalConnectingLeg(); + } + if (!returnConnections) { + reduceData(); + } + } + + /** + * Finds the index of a stop time in a trip for a given stop and time. + * + * @param trip The trip to search in. + * @param stop The stop to find. + * @param time The time to match. + * @param timeType The type of time to match (DEPARTURE or ARRIVAL). + * @return The index of the stop time in the trip. + */ + private static int findStopTimeIndexInTrip(Trip trip, Stop stop, LocalDateTime time, TimeType timeType) { + List stopTimes = trip.getStopTimes(); + for (int i = 0; i < stopTimes.size(); i++) { + StopTime stopTime = stopTimes.get(i); + if (stopTime.getStop().equals(stop)) { + if (timeType == TimeType.DEPARTURE && stopTime.getDepartureTime().equals(time)) { + return i; + } else if (timeType == TimeType.ARRIVAL && stopTime.getArrivalTime().equals(time)) { + return i; + } + } + } + throw new IllegalStateException("Stop time not found in trip."); + } + + /** + * Prepares the connecting leg for a departure connection (i.e. builds a leg from the second last to the last stop + * in the connection). + */ + private void prepareDepartureConnectingLeg() { + connectingLeg = this.connection.getLegs().getLast(); + if (connectingLeg.getTrip() == null) { + return; + } + int stopTimeIndex = findStopTimeIndexInTrip(connectingLeg.getTrip(), connectingLeg.getToStop(), + connectingLeg.getArrivalTime(), TimeType.ARRIVAL); + StopTime sourceStopTime = connectingLeg.getTrip().getStopTimes().get(stopTimeIndex - 1); + connectingLeg = new Leg(connectingLeg.getType(), sourceStopTime.getStop().getCoordinates(), + connectingLeg.getTo(), sourceStopTime.getStop(), connectingLeg.getToStop(), + sourceStopTime.getDepartureTime(), connectingLeg.getArrivalTime(), connectingLeg.getTrip()); + } + + /** + * Prepares the connecting leg for an arrival connection (i.e. builds a leg from the first to the second stop in the + * connection). + */ + private void prepareArrivalConnectingLeg() { + connectingLeg = this.connection.getLegs().getFirst(); + if (connectingLeg.getTrip() == null) { + return; + } + int stopTimeIndex = findStopTimeIndexInTrip(connectingLeg.getTrip(), connectingLeg.getFromStop(), + connectingLeg.getDepartureTime(), TimeType.DEPARTURE); + StopTime targetStopTime = connectingLeg.getTrip().getStopTimes().get(stopTimeIndex + 1); + connectingLeg = new Leg(connectingLeg.getType(), connectingLeg.getFrom(), + targetStopTime.getStop().getCoordinates(), connectingLeg.getFromStop(), targetStopTime.getStop(), + connectingLeg.getDepartureTime(), targetStopTime.getArrivalTime(), connectingLeg.getTrip()); + } + + /** + * Reduces the data of the StopConnection object by setting the connection to null and nullifying the stop times in + * the trip of the connecting leg. + */ + private void reduceData() { + connection = null; + if (connectingLeg.getTrip() == null) { + return; + } + Trip reducedTrip = new Trip(connectingLeg.getTrip().getHeadSign(), connectingLeg.getTrip().getRoute(), null); + connectingLeg = new Leg(connectingLeg.getType(), connectingLeg.getFrom(), connectingLeg.getTo(), + connectingLeg.getFromStop(), connectingLeg.getToStop(), connectingLeg.getDepartureTime(), + connectingLeg.getArrivalTime(), reducedTrip); + } } From e7aac3004c3e41394cf32b4d8bc2243320ef0d04 Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Thu, 4 Jul 2024 20:15:09 +0200 Subject: [PATCH 9/9] REFACTOR: NAV-104 - Reset default gtfs.static.uri in application properties. --- src/main/resources/application.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 690ad44d..89d50c37 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,8 +10,8 @@ logging.level.root=${LOG_LEVEL:INFO} # value is a file path, the GTFS is loaded from the file and the update interval is ignored. Examples: # - gtfs.static.uri=benchmark/input/switzerland.zip # - gtfs.static.uri=https://opentransportdata.swiss/en/dataset/timetable-2024-gtfs2020/permalink -gtfs.static.uri=https://connolly.ch/zuerich-trams.zip -# gtfs.static.uri=${GTFS_STATIC_URL:src/test/resources/ch/naviqore/gtfs/schedule/sample-feed-1.zip} +# - gtfs.static.uri=https://connolly.ch/zuerich-trams.zip +gtfs.static.uri=${GTFS_STATIC_URL:src/test/resources/ch/naviqore/gtfs/schedule/sample-feed-1.zip} # Cron expression for updating the static GTFS feed from the provided URL. Public transit agencies update their static # GTFS data regularly. Set this interval to match the agency's publish schedule. Default is to update the schedule # daily at 4 AM.