diff --git a/src/main/java/ch/naviqore/app/dto/DtoDummyData.java b/src/main/java/ch/naviqore/app/dto/DtoDummyData.java index 140442d3..512ec2df 100644 --- a/src/main/java/ch/naviqore/app/dto/DtoDummyData.java +++ b/src/main/java/ch/naviqore/app/dto/DtoDummyData.java @@ -210,7 +210,7 @@ private static Leg buildFootpathLeg(Stop from, Stop to, LocalDateTime departureT int footpathSpeed = 100; // meters per minute int footpathTravelTime = footpathDistance / footpathSpeed; // in minutes - return new Leg(from.getCoordinates(), to.getCoordinates(), from, to, LegType.WALK, departureTime, + return new Leg(LegType.WALK, from.getCoordinates(), to.getCoordinates(), from, to, departureTime, departureTime.plusMinutes(footpathTravelTime), null); } @@ -268,7 +268,7 @@ private static Leg buildTripDummyLeg(Stop from, Stop to, LocalDateTime departure Trip trip = new Trip(to.getName(), route, stopTimes); - return new Leg(from.getCoordinates(), to.getCoordinates(), from, to, LegType.ROUTE, tripDepartureTime, + return new Leg(LegType.ROUTE, from.getCoordinates(), to.getCoordinates(), from, to, tripDepartureTime, tripArrivalTime, trip); } diff --git a/src/main/java/ch/naviqore/app/dto/DtoMapper.java b/src/main/java/ch/naviqore/app/dto/DtoMapper.java index 78414b21..4dd00a7b 100644 --- a/src/main/java/ch/naviqore/app/dto/DtoMapper.java +++ b/src/main/java/ch/naviqore/app/dto/DtoMapper.java @@ -53,37 +53,43 @@ public static Connection map(ch.naviqore.service.Connection connection) { } public static EarliestArrival map(ch.naviqore.service.Stop stop, ch.naviqore.service.Connection connection) { - return new EarliestArrival(map(stop), connection.getLegs().getLast().getArrivalTime(), map(connection)); + return new EarliestArrival(map(stop), connection.getArrivalTime(), map(connection)); } private static class LegVisitorImpl implements LegVisitor { @Override public Leg visit(PublicTransitLeg publicTransitLeg) { - // TODO: Trip id in raptor has to be passed first. - return new Leg( - // map(publicTransitLeg.getDeparture().getStop().getLocation()), - // map(publicTransitLeg.getArrival().getStop().getLocation()), - null, null, - // map(publicTransitLeg.getDeparture().getStop()), - // map(publicTransitLeg.getArrival().getStop()), - null, null, LegType.ROUTE, - // publicTransitLeg.getDeparture().getDepartureTime(), - // publicTransitLeg.getArrival().getArrivalTime(), - null, null, - // map(publicTransitLeg.getTrip()) - null); + return new Leg(LegType.ROUTE, publicTransitLeg.getDeparture().getStop().getLocation(), + publicTransitLeg.getArrival().getStop().getLocation(), + map(publicTransitLeg.getDeparture().getStop()), map(publicTransitLeg.getArrival().getStop()), + publicTransitLeg.getDeparture().getDepartureTime(), publicTransitLeg.getArrival().getArrivalTime(), + map(publicTransitLeg.getTrip())); } @Override public Leg visit(Transfer transfer) { - return new Leg(transfer.getSourceStop().getLocation(), transfer.getTargetStop().getLocation(), - map(transfer.getSourceStop()), map(transfer.getTargetStop()), LegType.WALK, - transfer.getDepartureTime(), transfer.getArrivalTime(), null); + return new Leg(LegType.WALK, transfer.getSourceStop().getLocation(), transfer.getTargetStop().getLocation(), + map(transfer.getSourceStop()), map(transfer.getTargetStop()), transfer.getDepartureTime(), + transfer.getArrivalTime(), null); } @Override public Leg visit(Walk walk) { - return new Leg(walk.getSourceLocation(), walk.getTargetLocation(), null, null, LegType.WALK, + Stop sourceStop = null; + Stop targetStop = null; + + // set stop depending on walk type + if (walk.getStop().isPresent()) { + switch (walk.getWalkType()) { + case WalkType.LAST_MILE -> sourceStop = map(walk.getStop().get()); + case WalkType.FIRST_MILE -> targetStop = map(walk.getStop().get()); + // a walk between two stations is a TransferLeg and not a Walk, therefore this case should not occur + case DIRECT -> throw new IllegalStateException( + "No stop should be present in a direct walk between two location."); + } + } + + return new Leg(LegType.WALK, walk.getSourceLocation(), walk.getTargetLocation(), sourceStop, targetStop, walk.getDepartureTime(), walk.getArrivalTime(), null); } } diff --git a/src/main/java/ch/naviqore/app/dto/Leg.java b/src/main/java/ch/naviqore/app/dto/Leg.java index f182965b..446d28f8 100644 --- a/src/main/java/ch/naviqore/app/dto/Leg.java +++ b/src/main/java/ch/naviqore/app/dto/Leg.java @@ -12,11 +12,11 @@ @Getter public class Leg { + private final LegType type; private final GeoCoordinate from; private final GeoCoordinate to; private final Stop fromStop; private final Stop toStop; - private final LegType type; @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) private final LocalDateTime departureTime; @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) diff --git a/src/main/java/ch/naviqore/raptor/Connection.java b/src/main/java/ch/naviqore/raptor/Connection.java index b9c2796a..f64ecd81 100644 --- a/src/main/java/ch/naviqore/raptor/Connection.java +++ b/src/main/java/ch/naviqore/raptor/Connection.java @@ -4,6 +4,7 @@ import lombok.NoArgsConstructor; import lombok.ToString; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; @@ -74,11 +75,15 @@ public int getDuration() { return getArrivalTime() - getDepartureTime(); } - public int getNumFootPathTransfers() { - return (int) legs.stream().filter(l -> l.type == LegType.FOOTPATH).count(); + public List getWalkTransfers() { + return legs.stream().filter(l -> l.type == LegType.WALK_TRANSFER).toList(); } - public int getNumSameStationTransfers() { + public List getRouteLegs() { + return legs.stream().filter(l -> l.type == LegType.ROUTE).toList(); + } + + public int getNumberOfSameStationTransfers() { int transferCounter = 0; for (int i = 0; i < legs.size() - 1; i++) { Leg current = legs.get(i); @@ -90,27 +95,23 @@ public int getNumSameStationTransfers() { return transferCounter; } - public int getNumTransfers() { - return getNumFootPathTransfers() + getNumSameStationTransfers(); - } - - public int getNumRouteLegs() { - return (int) legs.stream().filter(l -> l.type == LegType.ROUTE).count(); + public int getNumberOfTotalTransfers() { + return getWalkTransfers().size() + getNumberOfSameStationTransfers(); } /** * Types of legs in a connection. */ public enum LegType { - FOOTPATH, + WALK_TRANSFER, ROUTE } /** * A leg is a part of a connection that is travelled on the same route and transport mode, without a transfer. */ - public record Leg(String routeId, String fromStopId, String toStopId, int departureTime, int arrivalTime, - LegType type) implements Comparable { + public record Leg(String routeId, @Nullable String tripId, String fromStopId, String toStopId, int departureTime, + int arrivalTime, LegType type) implements Comparable { @Override public int compareTo(@NotNull Connection.Leg other) { diff --git a/src/main/java/ch/naviqore/raptor/Raptor.java b/src/main/java/ch/naviqore/raptor/Raptor.java index 77966ae9..bb576c7a 100644 --- a/src/main/java/ch/naviqore/raptor/Raptor.java +++ b/src/main/java/ch/naviqore/raptor/Raptor.java @@ -46,6 +46,7 @@ public List routeEarliestArrival(String sourceStopId, String targetS return routeEarliestArrival(createStopMap(sourceStopId, departureTime), createStopMap(targetStopId, 0)); } + // TODO: Do we still need this? There are no usages... public List routeEarliestArrival(Collection sourceStopIds, Collection targetStopIds, int departureTime) { Map sourceStops = createStopMap(sourceStopIds, departureTime); @@ -65,6 +66,7 @@ private Map createStopMap(Collection stopIds, int value return stopMap; } + // TODO: Do we still need this? There are no usages... private Map createStopMap(List stopIds, List values) { if (stopIds.size() != values.size()) { throw new IllegalArgumentException("Stop IDs and values must have the same size."); @@ -158,7 +160,7 @@ private List spawnFromSourceStop(int[] sourceStopIdxs, int[] targetStopId // subtract same stop transfer time, as this will be added by default before scanning routes earliestArrivals[sourceStopIdxs[i]] = departureTimes[i] - SAME_STOP_TRANSFER_TIME; earliestArrivalsPerRound.getFirst()[sourceStopIdxs[i]] = new Leg(0, departureTimes[i], ArrivalType.INITIAL, - NO_INDEX, sourceStopIdxs[i], null); + NO_INDEX, NO_INDEX, sourceStopIdxs[i], null); markedStops.add(sourceStopIdxs[i]); } @@ -247,9 +249,10 @@ private List spawnFromSourceStop(int[] sourceStopIdxs, int[] targetStopId continue; } + // create a route leg earliestArrivals[stopIdx] = stopTime.arrival(); earliestArrivalsThisRound[stopIdx] = new Leg(tripEntryTime, stopTime.arrival(), - ArrivalType.ROUTE, currentRouteIdx, stopIdx, enteredAtArrival); + ArrivalType.ROUTE, currentRouteIdx, tripOffset, stopIdx, enteredAtArrival); // mark stop improvement for next round markedStopsNext.add(stopIdx); // check if this was a target stop @@ -315,7 +318,7 @@ private List spawnFromSourceStop(int[] sourceStopIdxs, int[] targetStopId stops[stopIdx].id()); earliestArrivals[transfer.targetStopIdx()] = newTargetStopArrivalTime; earliestArrivalsThisRound[transfer.targetStopIdx()] = new Leg(earliestArrivals[stopIdx], - newTargetStopArrivalTime, ArrivalType.TRANSFER, i, transfer.targetStopIdx(), + newTargetStopArrivalTime, ArrivalType.TRANSFER, i, NO_INDEX, transfer.targetStopIdx(), earliestArrivalsThisRound[stopIdx]); newStops.add(transfer.targetStopIdx()); } @@ -374,25 +377,36 @@ private List reconstructParetoOptimalSolutions(List earliestA private @Nullable Connection reconstructConnectionFromLeg(Leg leg) { Connection connection = new Connection(); + + // start from destination leg and follow legs back until the initial leg is reached while (leg.type != ArrivalType.INITIAL) { - String id; + String routeId; + String tripId = null; + assert leg.previous != null; String fromStopId = stops[leg.previous.stopIdx].id(); String toStopId = stops[leg.stopIdx].id(); Connection.LegType type; int departureTime = leg.departureTime; int arrivalTime = leg.arrivalTime; + if (leg.type == ArrivalType.ROUTE) { - id = routes[leg.routeOrTransferIdx].id(); + Route route = routes[leg.routeOrTransferIdx]; + routeId = route.id(); + tripId = route.tripIds()[leg.tripOffset]; type = Connection.LegType.ROUTE; + } else if (leg.type == ArrivalType.TRANSFER) { - id = String.format("transfer_%s_%s", fromStopId, toStopId); - type = Connection.LegType.FOOTPATH; + routeId = String.format("transfer_%s_%s", fromStopId, toStopId); + type = Connection.LegType.WALK_TRANSFER; // include same stop transfer time (which is subtracted before scanning routes) arrivalTime += SAME_STOP_TRANSFER_TIME; + } else { throw new IllegalStateException("Unknown arrival type"); } - connection.addLeg(new Connection.Leg(id, fromStopId, toStopId, departureTime, arrivalTime, type)); + + connection.addLeg( + new Connection.Leg(routeId, tripId, fromStopId, toStopId, departureTime, arrivalTime, type)); leg = leg.previous; } @@ -411,6 +425,7 @@ private void expandFootpathsForSourceStop(int[] earliestArrivals, List ea if (stops[sourceStopIdx].numberOfTransfers() == 0) { return; } + // mark all transfer stops, no checks needed for since all transfers will improve arrival time and can be // marked Stop sourceStop = stops[sourceStopIdx]; @@ -422,20 +437,45 @@ private void expandFootpathsForSourceStop(int[] earliestArrivals, List ea } earliestArrivals[transfer.targetStopIdx()] = newTargetStopArrivalTime; earliestArrivalsPerRound.getFirst()[transfer.targetStopIdx()] = new Leg(departureTime, - newTargetStopArrivalTime, ArrivalType.TRANSFER, i, transfer.targetStopIdx(), + newTargetStopArrivalTime, ArrivalType.TRANSFER, i, NO_INDEX, transfer.targetStopIdx(), earliestArrivalsPerRound.getFirst()[sourceStopIdx]); markedStops.add(transfer.targetStopIdx()); } } + /** + * Arrival type of the leg. + */ private enum ArrivalType { + + /** + * First leg in the connection, so there is no previous leg set. + */ INITIAL, + /** + * A route leg uses a public transit trip in the network. + */ ROUTE, + /** + * Uses a transfer between station (no same station transfers). + */ TRANSFER + } - private record Leg(int departureTime, int arrivalTime, ArrivalType type, int routeOrTransferIdx, int stopIdx, - Leg previous) { + /** + * A leg is a part of a connection in the same mode (PT or walk). + * + * @param departureTime the departure time of the leg in seconds after midnight. + * @param arrivalTime the arrival time of the leg in seconds after midnight. + * @param type the type of the leg, can be INITIAL, ROUTE or TRANSFER. + * @param routeOrTransferIdx the index of the route or of the transfer, see arrival type (or NO_INDEX). + * @param tripOffset the trip offset on the current route (or NO_INDEX). + * @param stopIdx the arrival stop of the leg. + * @param previous the previous leg, null if it is the previous leg. + */ + private record Leg(int departureTime, int arrivalTime, ArrivalType type, int routeOrTransferIdx, int tripOffset, + int stopIdx, @Nullable Leg previous) { } /** @@ -444,6 +484,7 @@ private record Leg(int departureTime, int arrivalTime, ArrivalType type, int rou private class InputValidator { private static final int MIN_DEPARTURE_TIME = 0; private static final int MAX_DEPARTURE_TIME = 48 * 60 * 60; // 48 hours + private static final int MIN_WALKING_TIME_TO_TARGET = 0; private static void validateStopPermutations(Map sourceStops, Map targetStops) { @@ -453,14 +494,17 @@ private static void validateStopPermutations(Map sourceStops, if (targetStops.isEmpty()) { throw new IllegalArgumentException("At least one target stop must be provided."); } + sourceStops.values().forEach(InputValidator::validateDepartureTime); targetStops.values().forEach(InputValidator::validateWalkingTimeToTarget); - Set targetStopIds = targetStops.keySet(); - for (String sourceStopId : sourceStops.keySet()) { - if (targetStopIds.contains(sourceStopId)) { - throw new IllegalArgumentException("Source and target stop IDs must not be the same."); - } + + // ensure departure and arrival stops are not the same + Set intersection = new HashSet<>(sourceStops.keySet()); + intersection.retainAll(targetStops.keySet()); + if (!intersection.isEmpty()) { + throw new IllegalArgumentException("Source and target stop IDs must not be the same."); } + } private static void validateDepartureTime(int departureTime) { @@ -471,8 +515,9 @@ private static void validateDepartureTime(int departureTime) { } private static void validateWalkingTimeToTarget(int walkingDurationToTarget) { - if (walkingDurationToTarget < 0) { - throw new IllegalArgumentException("Walking duration to target must be greater or equal to 0."); + if (walkingDurationToTarget < MIN_WALKING_TIME_TO_TARGET) { + throw new IllegalArgumentException( + "Walking duration to target must be greater or equal to " + MIN_WALKING_TIME_TO_TARGET + "seconds."); } } @@ -489,16 +534,19 @@ private Map validateStops(Map stops) { if (stops.isEmpty()) { throw new IllegalArgumentException("At least one stop ID must be provided."); } - // Loop over all stop pairs + + // loop over all stop pairs and check if stop exists in raptor, then validate departure time Map validStopIds = new HashMap<>(); for (Map.Entry entry : stops.entrySet()) { String stopId = entry.getKey(); int time = entry.getValue(); + if (stopsToIdx.containsKey(stopId)) { validateDepartureTime(time); validStopIds.put(stopsToIdx.get(stopId), time); + } else { + log.warn("Stop ID {} not found in lookup removing from query.", entry.getKey()); } - log.warn("Stop ID {} not found in lookup removing from query.", entry.getKey()); } if (validStopIds.isEmpty()) { diff --git a/src/main/java/ch/naviqore/raptor/RaptorBuilder.java b/src/main/java/ch/naviqore/raptor/RaptorBuilder.java index f6ecd2a0..5695c861 100644 --- a/src/main/java/ch/naviqore/raptor/RaptorBuilder.java +++ b/src/main/java/ch/naviqore/raptor/RaptorBuilder.java @@ -190,8 +190,8 @@ private RouteTraversal buildRouteTraversal(List rou // add route entry to route array final int numberOfStops = routeContainer.stopSequence().size(); final int numberOfTrips = routeContainer.trips().size(); - routeArr[routeIdx] = new Route(routeContainer.id(), routeStopCnt, numberOfStops, stopTimeCnt, - numberOfTrips); + routeArr[routeIdx] = new Route(routeContainer.id(), routeStopCnt, numberOfStops, stopTimeCnt, numberOfTrips, + routeContainer.trips().keySet().toArray(new String[0])); // add stops to route stop array Map stopSequence = routeContainer.stopSequence(); diff --git a/src/main/java/ch/naviqore/raptor/Route.java b/src/main/java/ch/naviqore/raptor/Route.java index 7f4d504d..5721430c 100644 --- a/src/main/java/ch/naviqore/raptor/Route.java +++ b/src/main/java/ch/naviqore/raptor/Route.java @@ -1,4 +1,5 @@ package ch.naviqore.raptor; -record Route(String id, int firstRouteStopIdx, int numberOfStops, int firstStopTimeIdx, int numberOfTrips) { +record Route(String id, int firstRouteStopIdx, int numberOfStops, int firstStopTimeIdx, int numberOfTrips, + String[] tripIds) { } diff --git a/src/main/java/ch/naviqore/service/Connection.java b/src/main/java/ch/naviqore/service/Connection.java index 49fa8a42..8519bd2b 100644 --- a/src/main/java/ch/naviqore/service/Connection.java +++ b/src/main/java/ch/naviqore/service/Connection.java @@ -1,5 +1,6 @@ package ch.naviqore.service; +import java.time.LocalDateTime; import java.util.List; /** @@ -9,4 +10,8 @@ public interface Connection { List getLegs(); + LocalDateTime getDepartureTime(); + + LocalDateTime getArrivalTime(); + } diff --git a/src/main/java/ch/naviqore/service/Leg.java b/src/main/java/ch/naviqore/service/Leg.java index 139a0041..ac7050fe 100644 --- a/src/main/java/ch/naviqore/service/Leg.java +++ b/src/main/java/ch/naviqore/service/Leg.java @@ -1,7 +1,5 @@ package ch.naviqore.service; -import java.time.LocalDateTime; - public interface Leg { LegType getLegType(); @@ -12,7 +10,4 @@ public interface Leg { int getDuration(); - LocalDateTime getDepartureTime(); - - LocalDateTime getArrivalTime(); } diff --git a/src/main/java/ch/naviqore/service/Walk.java b/src/main/java/ch/naviqore/service/Walk.java index 7146063e..cdaa2535 100644 --- a/src/main/java/ch/naviqore/service/Walk.java +++ b/src/main/java/ch/naviqore/service/Walk.java @@ -2,12 +2,17 @@ import ch.naviqore.utils.spatial.GeoCoordinate; +import java.time.LocalDateTime; import java.util.Optional; public interface Walk extends Leg { WalkType getWalkType(); + LocalDateTime getDepartureTime(); + + LocalDateTime getArrivalTime(); + GeoCoordinate getSourceLocation(); GeoCoordinate getTargetLocation(); diff --git a/src/main/java/ch/naviqore/service/impl/ConnectionImpl.java b/src/main/java/ch/naviqore/service/impl/ConnectionImpl.java index 2c4fd196..ff95ee67 100644 --- a/src/main/java/ch/naviqore/service/impl/ConnectionImpl.java +++ b/src/main/java/ch/naviqore/service/impl/ConnectionImpl.java @@ -1,12 +1,12 @@ package ch.naviqore.service.impl; -import ch.naviqore.service.Connection; -import ch.naviqore.service.Leg; +import ch.naviqore.service.*; import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.ToString; +import java.time.LocalDateTime; import java.util.List; @RequiredArgsConstructor(access = AccessLevel.PACKAGE) @@ -16,4 +16,43 @@ public class ConnectionImpl implements Connection { private final List legs; + @Override + public LocalDateTime getDepartureTime() { + return legs.getFirst().accept(new LegVisitor<>() { + @Override + public LocalDateTime visit(PublicTransitLeg publicTransitLeg) { + return publicTransitLeg.getDeparture().getDepartureTime(); + } + + @Override + public LocalDateTime visit(Transfer transfer) { + return transfer.getDepartureTime(); + } + + @Override + public LocalDateTime visit(Walk walk) { + return walk.getDepartureTime(); + } + }); + } + + @Override + public LocalDateTime getArrivalTime() { + return legs.getLast().accept(new LegVisitor<>() { + @Override + public LocalDateTime visit(PublicTransitLeg publicTransitLeg) { + return publicTransitLeg.getArrival().getArrivalTime(); + } + + @Override + public LocalDateTime visit(Transfer transfer) { + return transfer.getArrivalTime(); + } + + @Override + public LocalDateTime visit(Walk walk) { + return walk.getArrivalTime(); + } + }); + } } diff --git a/src/main/java/ch/naviqore/service/impl/PublicTransitLegImpl.java b/src/main/java/ch/naviqore/service/impl/PublicTransitLegImpl.java index 20515b85..3e30957c 100644 --- a/src/main/java/ch/naviqore/service/impl/PublicTransitLegImpl.java +++ b/src/main/java/ch/naviqore/service/impl/PublicTransitLegImpl.java @@ -4,21 +4,19 @@ import lombok.Getter; import lombok.ToString; -import java.time.LocalDateTime; - @Getter @ToString(callSuper = true) public class PublicTransitLegImpl extends LegImpl implements PublicTransitLeg { private final Trip trip; - private final StopTime arrival; private final StopTime departure; + private final StopTime arrival; - PublicTransitLegImpl(int distance, int duration, Trip trip, StopTime arrival, StopTime departure) { + PublicTransitLegImpl(int distance, int duration, Trip trip, StopTime departure, StopTime arrival) { super(LegType.PUBLIC_TRANSIT, distance, duration); this.trip = trip; - this.arrival = arrival; this.departure = departure; + this.arrival = arrival; } @Override @@ -26,14 +24,4 @@ public T accept(LegVisitor visitor) { return visitor.visit(this); } - @Override - public LocalDateTime getDepartureTime() { - return departure.getDepartureTime(); - } - - @Override - public LocalDateTime getArrivalTime() { - return arrival.getArrivalTime(); - } - } diff --git a/src/main/java/ch/naviqore/service/impl/PublicTransitServiceImpl.java b/src/main/java/ch/naviqore/service/impl/PublicTransitServiceImpl.java index 805b6b92..1a46e877 100644 --- a/src/main/java/ch/naviqore/service/impl/PublicTransitServiceImpl.java +++ b/src/main/java/ch/naviqore/service/impl/PublicTransitServiceImpl.java @@ -43,7 +43,6 @@ public class PublicTransitServiceImpl implements PublicTransitService { private static final int MIN_WALK_DURATION = 120; private final ServiceConfig config; private final GtfsSchedule schedule; - // private final Map> parentStops; private final KDTree spatialStopIndex; private final SearchIndex stopSearchIndex; private final WalkCalculator walkCalculator; @@ -86,6 +85,7 @@ public Optional getNearestStop(GeoCoordinate location) { if (stop != null && stop.getParent().isPresent() && !stop.getParent().get().equals(stop)) { stop = stop.getParent().get(); } + return Optional.ofNullable(map(stop)); } diff --git a/src/main/java/ch/naviqore/service/impl/StopTimeImpl.java b/src/main/java/ch/naviqore/service/impl/StopTimeImpl.java index 01cea4b4..efa051b6 100644 --- a/src/main/java/ch/naviqore/service/impl/StopTimeImpl.java +++ b/src/main/java/ch/naviqore/service/impl/StopTimeImpl.java @@ -10,13 +10,14 @@ @AllArgsConstructor(access = AccessLevel.PACKAGE) @RequiredArgsConstructor(access = AccessLevel.PACKAGE) @Getter -@ToString +@ToString(exclude = "trip") public class StopTimeImpl implements StopTime { private final Stop stop; private final LocalDateTime arrivalTime; private final LocalDateTime departureTime; @Setter(AccessLevel.PACKAGE) - private Trip trip; + // cyclical dependency, therefore avoid serialization and use in toString representation + private transient Trip trip; } diff --git a/src/main/java/ch/naviqore/service/impl/TypeMapper.java b/src/main/java/ch/naviqore/service/impl/TypeMapper.java index 1645f92a..8bd41b87 100644 --- a/src/main/java/ch/naviqore/service/impl/TypeMapper.java +++ b/src/main/java/ch/naviqore/service/impl/TypeMapper.java @@ -89,19 +89,53 @@ public static Connection map(ch.naviqore.raptor.Connection connection, @Nullable } public static Leg map(ch.naviqore.raptor.Connection.Leg leg, LocalDate date, GtfsSchedule schedule) { - // TODO: Distance is needed on Footpaths, distance of trip can be estimated based on trip and beeline distance? - int distance = 0; int duration = leg.arrivalTime() - leg.departureTime(); Stop sourceStop = map(schedule.getStops().get(leg.fromStopId())); Stop targetStop = map(schedule.getStops().get(leg.toStopId())); + int distance = (int) Math.round(sourceStop.getLocation().distanceTo(targetStop.getLocation())); + return switch (leg.type()) { - case FOOTPATH -> new TransferImpl(distance, duration, toLocalDateTime(leg.departureTime(), date), + case WALK_TRANSFER -> new TransferImpl(distance, duration, toLocalDateTime(leg.departureTime(), date), toLocalDateTime(leg.arrivalTime(), date), sourceStop, targetStop); - // TODO: Refactor Raptor and extract interfaces. Put Trip id on leg, then stop time can be found. - case ROUTE -> new PublicTransitLegImpl(distance, duration, null, null, null); + case ROUTE -> createPublicTransitLeg(leg, date, schedule, distance); }; } + private static Leg createPublicTransitLeg(ch.naviqore.raptor.Connection.Leg leg, LocalDate date, + GtfsSchedule schedule, int distance) { + int duration = leg.arrivalTime() - leg.departureTime(); + ch.naviqore.gtfs.schedule.model.Trip gtfsTrip = schedule.getTrips().get(leg.tripId()); + Trip trip = map(gtfsTrip, date); + + assert gtfsTrip.getStopTimes().size() == trip.getStopTimes() + .size() : "GTFS trip and trip implementation in service must have the same number of stop times."; + + // find departure and arrival stop time in stop sequence of trip + StopTime departure = null; + StopTime arrival = null; + for (int i = 0; i < gtfsTrip.getStopTimes().size(); i++) { + var gtfsStopTime = gtfsTrip.getStopTimes().get(i); + // if the from stop id and the departure time matches, set the departure stop time + if (gtfsStopTime.stop().getId().equals(leg.fromStopId()) && gtfsStopTime.departure() + .getTotalSeconds() == leg.departureTime()) { + departure = trip.getStopTimes().get(i); + continue; + } + + // if the to stop id and the arrival time matches, set the arrival stop time + if (gtfsStopTime.stop().getId().equals(leg.toStopId()) && gtfsStopTime.arrival() + .getTotalSeconds() == leg.arrivalTime()) { + arrival = trip.getStopTimes().get(i); + break; + } + } + + assert departure != null : "Departure stop time cannot be null"; + assert arrival != null : "Arrival stop time cannot be null"; + + return new PublicTransitLegImpl(distance, duration, trip, departure, arrival); + } + private static LocalDateTime toLocalDateTime(int secondsOfDay, LocalDate date) { return new ServiceDayTime(secondsOfDay).toLocalDateTime(date); } diff --git a/src/main/java/ch/naviqore/utils/spatial/CartesianCoordinate.java b/src/main/java/ch/naviqore/utils/spatial/CartesianCoordinate.java index 020ee444..15d986e2 100644 --- a/src/main/java/ch/naviqore/utils/spatial/CartesianCoordinate.java +++ b/src/main/java/ch/naviqore/utils/spatial/CartesianCoordinate.java @@ -61,7 +61,7 @@ public boolean equals(Object obj) { @Override public String toString() { - return "[" + this.getClass().getSimpleName() + ": " + x + ", " + y + "]"; + return this.getClass().getSimpleName() + "(x=" + x + ", y=" + y + ")"; } } diff --git a/src/main/java/ch/naviqore/utils/spatial/GeoCoordinate.java b/src/main/java/ch/naviqore/utils/spatial/GeoCoordinate.java index 4e57c803..85a527c7 100644 --- a/src/main/java/ch/naviqore/utils/spatial/GeoCoordinate.java +++ b/src/main/java/ch/naviqore/utils/spatial/GeoCoordinate.java @@ -91,6 +91,6 @@ public int compareTo(GeoCoordinate other) { @Override public String toString() { - return "[" + this.getClass().getSimpleName() + ": " + latitude + "°, " + longitude + "°]"; + return this.getClass().getSimpleName() + "(lat=" + latitude + "°, lon=" + longitude + "°)"; } } diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java index 85bf677f..e889b287 100644 --- a/src/test/java/ch/naviqore/Benchmark.java +++ b/src/test/java/ch/naviqore/Benchmark.java @@ -160,8 +160,14 @@ private static RoutingResult toResult(int id, RouteRequest request, List= departureTime, "Departure time should be greater equal than searched for departure time"); // check that transfers make sense - assertEquals(1, connection1.getNumFootPathTransfers()); - assertEquals(1, connection1.getNumTransfers()); - assertEquals(0, connection1.getNumSameStationTransfers()); + assertEquals(1, connection1.getWalkTransfers().size()); + assertEquals(1, connection1.getNumberOfTotalTransfers()); + assertEquals(0, connection1.getNumberOfSameStationTransfers()); // check second connection Connection connection2 = connections.get(1); @@ -58,14 +58,14 @@ void shouldFindConnectionsBetweenIntersectingRoutes(RaptorTestBuilder builder) { assertTrue(connection2.getDepartureTime() >= departureTime, "Departure time should be greater equal than searched for departure time"); // check that transfers make sense - assertEquals(0, connection2.getNumFootPathTransfers()); - assertEquals(2, connection2.getNumTransfers()); - assertEquals(2, connection2.getNumSameStationTransfers()); + assertEquals(0, connection2.getWalkTransfers().size()); + assertEquals(2, connection2.getNumberOfTotalTransfers()); + assertEquals(2, connection2.getNumberOfSameStationTransfers()); // compare two connections (make sure they are pareto optimal) assertTrue(connection1.getDuration() > connection2.getDuration(), "First connection should be slower than second connection"); - assertTrue(connection1.getNumRouteLegs() < connection2.getNumRouteLegs(), + assertTrue(connection1.getRouteLegs().size() < connection2.getRouteLegs().size(), "First connection should have fewer route legs than second connection"); } @@ -83,9 +83,9 @@ void routeBetweenTwoStopsOnSameRoute(RaptorTestBuilder builder) { assertEquals(targetStop, connection.getToStopId()); assertTrue(connection.getDepartureTime() >= departureTime, "Departure time should be greater equal than searched for departure time"); - assertEquals(0, connection.getNumFootPathTransfers()); - assertEquals(0, connection.getNumTransfers()); - assertEquals(0, connection.getNumSameStationTransfers()); + assertEquals(0, connection.getWalkTransfers().size()); + assertEquals(0, connection.getNumberOfTotalTransfers()); + assertEquals(0, connection.getNumberOfSameStationTransfers()); } @Test @@ -120,10 +120,10 @@ void shouldFindConnectionBetweenOnlyFootpath(RaptorTestBuilder builder) { assertEquals(targetStop, connection.getToStopId()); assertTrue(connection.getDepartureTime() >= departureTime, "Departure time should be greater equal than searched for departure time"); - assertEquals(1, connection.getNumFootPathTransfers()); - assertEquals(1, connection.getNumTransfers()); - assertEquals(0, connection.getNumSameStationTransfers()); - assertEquals(0, connection.getNumRouteLegs()); + assertEquals(1, connection.getWalkTransfers().size()); + assertEquals(1, connection.getNumberOfTotalTransfers()); + assertEquals(0, connection.getNumberOfSameStationTransfers()); + assertEquals(0, connection.getRouteLegs().size()); } @Nested diff --git a/src/test/resources/ch/naviqore/app/ApplicationTest.rest b/src/test/resources/ch/naviqore/app/ApplicationTest.rest index b1092d89..2536b258 100644 --- a/src/test/resources/ch/naviqore/app/ApplicationTest.rest +++ b/src/test/resources/ch/naviqore/app/ApplicationTest.rest @@ -1,4 +1,4 @@ -### Nearest stop +### Nearest stops GET http://localhost:8080/schedule/stops/nearest?latitude=36&longitude=-116&maxDistance=500000 Accept: application/json