From 42b9c66e79dd2c9dbb888a1e68c7d5761ba31b58 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Mon, 6 May 2024 23:42:33 +0200 Subject: [PATCH 1/7] ENH: NAV-18 - Read optional transfers - Support transfer types 0, 1, 2 and 3. - Adjust builder, parser and reader to enable transfers. - Add jetbrains annotations dependency for marking nullable fields and parameters. --- pom.xml | 6 +++ .../gtfs/schedule/GtfsScheduleFile.java | 4 +- .../gtfs/schedule/GtfsScheduleParser.java | 7 +++ .../schedule/model/GtfsScheduleBuilder.java | 18 ++++++++ .../ch/naviqore/gtfs/schedule/model/Stop.java | 5 +++ .../gtfs/schedule/model/Transfer.java | 43 +++++++++++++++++++ .../gtfs/schedule/type/ExceptionType.java | 12 +++--- .../gtfs/schedule/type/TransferType.java | 38 ++++++++++++++++ .../naviqore/raptor/GtfsRoutePartitioner.java | 2 +- 9 files changed, 126 insertions(+), 9 deletions(-) create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/model/Transfer.java create mode 100644 src/main/java/ch/naviqore/gtfs/schedule/type/TransferType.java diff --git a/pom.xml b/pom.xml index eaa7aab8..ca94c082 100644 --- a/pom.xml +++ b/pom.xml @@ -107,6 +107,12 @@ 1.18.32 provided + + org.jetbrains + annotations + 24.1.0 + compile + org.apache.logging.log4j diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java index ac61b7cc..eb844318 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java @@ -21,8 +21,8 @@ enum GtfsScheduleFile { ROUTES("routes.txt", Presence.REQUIRED), // SHAPES("shapes.txt", Presence.OPTIONAL), TRIPS("trips.txt", Presence.REQUIRED), - STOP_TIMES("stop_times.txt", Presence.REQUIRED); - // TRANSFERS("transfers.txt", Presence.OPTIONAL); + STOP_TIMES("stop_times.txt", Presence.REQUIRED), + TRANSFERS("transfers.txt", Presence.OPTIONAL); private final String fileName; private final Presence presence; diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java index e3ff25cd..4cea2e69 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java @@ -4,6 +4,7 @@ import ch.naviqore.gtfs.schedule.type.ExceptionType; import ch.naviqore.gtfs.schedule.type.RouteType; import ch.naviqore.gtfs.schedule.type.ServiceDayTime; +import ch.naviqore.gtfs.schedule.type.TransferType; import lombok.extern.log4j.Log4j2; import org.apache.commons.csv.CSVRecord; @@ -50,6 +51,7 @@ private void initializeParsers() { parsers.put(GtfsScheduleFile.ROUTES, this::parseRoute); parsers.put(GtfsScheduleFile.TRIPS, this::parseTrips); parsers.put(GtfsScheduleFile.STOP_TIMES, this::parseStopTimes); + parsers.put(GtfsScheduleFile.TRANSFERS, this::parseTransfers); } private void parseAgency(CSVRecord record) { @@ -107,4 +109,9 @@ private void parseStopTimes(CSVRecord record) { e.getMessage()); } } + + private void parseTransfers(CSVRecord record) { + builder.addTransfer(record.get("from_stop_id"), record.get("to_stop_id"), + TransferType.parse(record.get("transfer_type")), Integer.parseInt(record.get("min_transfer_time"))); + } } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java index 9decff7a..77a69ed0 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java @@ -4,9 +4,11 @@ import ch.naviqore.gtfs.schedule.type.ExceptionType; import ch.naviqore.gtfs.schedule.type.RouteType; import ch.naviqore.gtfs.schedule.type.ServiceDayTime; +import ch.naviqore.gtfs.schedule.type.TransferType; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.jetbrains.annotations.Nullable; import java.time.DayOfWeek; import java.time.LocalDate; @@ -128,6 +130,22 @@ public GtfsScheduleBuilder addStopTime(String tripId, String stopId, ServiceDayT return this; } + public GtfsScheduleBuilder addTransfer(String fromStopId, String toStopId, TransferType transferType, + @Nullable Integer minTransferTime) { + Stop fromStop = stops.get(fromStopId); + if (fromStop == null) { + throw new IllegalArgumentException("Stop " + fromStopId + " does not exist"); + } + Stop toStop = stops.get(toStopId); + if (toStop == null) { + throw new IllegalArgumentException("Stop " + toStopId + " does not exist"); + } + log.debug("Adding transfer {}-{} of type {} {}", fromStopId, toStopId, transferType, minTransferTime); + // TODO: Handle case when minTransferTime is missing, add transfers.txt to test data. + fromStop.addTransfer(new Transfer(fromStop, toStop, transferType, minTransferTime)); + return this; + } + public GtfsSchedule build() { log.info("Building schedule with {} stops, {} routes and {} trips", stops.size(), routes.size(), trips.size()); trips.values().parallelStream().forEach(Trip::initialize); diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java index c08f5743..5012f85e 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java @@ -17,11 +17,16 @@ public final class Stop implements Initializable { private final String name; private final Coordinate coordinate; private final List stopTimes = new ArrayList<>(); + private final List transfers = new ArrayList<>(); void addStopTime(StopTime stopTime) { stopTimes.add(stopTime); } + void addTransfer(Transfer transfer) { + transfers.add(transfer); + } + @Override public void initialize() { Collections.sort(stopTimes); diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Transfer.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Transfer.java new file mode 100644 index 00000000..5b618e5b --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Transfer.java @@ -0,0 +1,43 @@ +package ch.naviqore.gtfs.schedule.model; + +import ch.naviqore.gtfs.schedule.type.TransferType; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.Optional; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +@Getter +public class Transfer { + private final Stop fromStop; + private final Stop toStop; + private final TransferType transferType; + @Nullable + private final Integer minTransferTime; + + public Optional getMinTransferTime() { + return Optional.ofNullable(minTransferTime); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + Transfer that = (Transfer) obj; + return Objects.equals(fromStop, that.fromStop) && Objects.equals(toStop, that.toStop) && Objects.equals( + transferType, that.transferType); + } + + @Override + public int hashCode() { + return Objects.hash(fromStop, toStop, transferType); + } + + @Override + public String toString() { + return "Transfer[" + "fromStopId='" + fromStop.getId() + '\'' + ", toStopId='" + toStop.getId() + '\'' + ", transferType=" + transferType + ']'; + } +} \ No newline at end of file diff --git a/src/main/java/ch/naviqore/gtfs/schedule/type/ExceptionType.java b/src/main/java/ch/naviqore/gtfs/schedule/type/ExceptionType.java index 6bd88e71..80f9c8c3 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/type/ExceptionType.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/ExceptionType.java @@ -10,19 +10,19 @@ public enum ExceptionType { ADDED(1, "Service has been added for the specified date."), REMOVED(2, "Service has been removed for the specified date."); - private final int value; + private final int code; private final String description; - public static ExceptionType parse(String value) { - return parse(Integer.parseInt(value)); + public static ExceptionType parse(String code) { + return parse(Integer.parseInt(code)); } - public static ExceptionType parse(int value) { + public static ExceptionType parse(int code) { for (ExceptionType type : ExceptionType.values()) { - if (type.value == value) { + if (type.code == code) { return type; } } - throw new IllegalArgumentException("No exception type with value " + value + " found"); + throw new IllegalArgumentException("No exception type with code " + code + " found"); } } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/type/TransferType.java b/src/main/java/ch/naviqore/gtfs/schedule/type/TransferType.java new file mode 100644 index 00000000..53275aa6 --- /dev/null +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/TransferType.java @@ -0,0 +1,38 @@ +package ch.naviqore.gtfs.schedule.type; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Defines the types of transfers between routes as specified in the GTFS feed standards. + *

+ * For more information on transfer types, see GTFS Transfers. + * + * @author munterfi + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public enum TransferType { + RECOMMENDED(0, "Recommended transfer point between two routes."), + TIMED(1, "Timed transfer between two routes. The departing vehicle is expected to wait for the arriving one."), + MINIMUM_TIME(2, "Transfer requires a minimum amount of time between arrival and departure to ensure a connection."), + NOT_POSSIBLE(3, "Transfer is not possible between routes at this location."); + + private final int code; + private final String description; + + public static TransferType parse(String code) { + return parse(Integer.parseInt(code)); + } + + public static TransferType parse(int code) { + for (TransferType type : TransferType.values()) { + if (type.code == code) { + return type; + } + } + throw new IllegalArgumentException("No transfer type with code " + code + " found"); + } +} diff --git a/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java b/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java index a1576642..466b1217 100644 --- a/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java +++ b/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java @@ -97,7 +97,7 @@ public boolean equals(Object obj) { public int hashCode() { return Objects.hash(id); } - + public String toString() { return "SubRoute[" + "id=" + id + ", " + "route=" + route + ", " + "stopSequence=" + stopSequenceKey + ']'; } From ed925b44e3b326dc140d40db3a3b119ca9ec172a Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Tue, 7 May 2024 15:08:44 +0200 Subject: [PATCH 2/7] ENH: NAV-18 - Parse min transfer time also if empty --- .../gtfs/schedule/GtfsScheduleParser.java | 4 +- .../raptor/GtfsToRaptorConverter.java | 62 +++++++++++++++---- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java index 4cea2e69..d1f1751b 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java @@ -111,7 +111,9 @@ private void parseStopTimes(CSVRecord record) { } private void parseTransfers(CSVRecord record) { + String minTransferTime = record.get("min_transfer_time"); builder.addTransfer(record.get("from_stop_id"), record.get("to_stop_id"), - TransferType.parse(record.get("transfer_type")), Integer.parseInt(record.get("min_transfer_time"))); + TransferType.parse(record.get("transfer_type")), + minTransferTime.isEmpty() ? null : Integer.parseInt(record.get("min_transfer_time"))); } } diff --git a/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java b/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java index 2934678a..88fb3754 100644 --- a/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java +++ b/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java @@ -1,9 +1,7 @@ package ch.naviqore.raptor; -import ch.naviqore.gtfs.schedule.model.GtfsSchedule; -import ch.naviqore.gtfs.schedule.model.Stop; -import ch.naviqore.gtfs.schedule.model.StopTime; -import ch.naviqore.gtfs.schedule.model.Trip; +import ch.naviqore.gtfs.schedule.model.*; +import ch.naviqore.gtfs.schedule.type.TransferType; import ch.naviqore.raptor.model.Raptor; import ch.naviqore.raptor.model.RaptorBuilder; import lombok.extern.log4j.Log4j2; @@ -15,6 +13,11 @@ /** * Maps GTFS schedule to Raptor + *

+ * For each sub-route in a GTFS route a route in Raptor is created. Only the "minimum time" transfers between different + * stations are considered as Raptor transfers. In the Raptor model, transfers are treated exclusively as pedestrian + * paths between stations, reflecting necessary walking connections. Thus, other types of GTFS transfers are omitted + * from the mapping process to align with Raptor's conceptual model. * * @author munterfi */ @@ -35,26 +38,61 @@ public GtfsToRaptorConverter(GtfsSchedule schedule) { public Raptor convert(LocalDate date) { List activeTrips = schedule.getActiveTrips(date); log.info("Converting {} active trips from GTFS schedule to Raptor model", activeTrips.size()); + for (Trip trip : activeTrips) { - // Route route = trip.getRoute(); GtfsRoutePartitioner.SubRoute subRoute = partitioner.getSubRoute(trip); + if (!subRoutes.contains(subRoute)) { subRoutes.add(subRoute); builder.addRoute(subRoute.getId()); - for (StopTime stopTime : trip.getStopTimes()) { - if (!stops.contains(stopTime.stop())) { - stops.add(stopTime.stop()); - builder.addStop(stopTime.stop().getId()); - } - builder.addRouteStop(stopTime.stop().getId(), subRoute.getId()); - } + addRouteStops(trip, subRoute); } + for (StopTime stopTime : trip.getStopTimes()) { builder.addStopTime(stopTime.stop().getId(), subRoute.getId(), stopTime.arrival().getTotalSeconds(), stopTime.departure().getTotalSeconds()); } } + addTransfers(); + return builder.build(); } + + private void addRouteStops(Trip trip, GtfsRoutePartitioner.SubRoute subRoute) { + for (StopTime stopTime : trip.getStopTimes()) { + Stop stop = stopTime.stop(); + + if (!stops.contains(stop)) { + stops.add(stop); + builder.addStop(stop.getId()); + } + + builder.addRouteStop(stop.getId(), subRoute.getId()); + } + } + + private void addTransfers() { + for (Stop stop : stops) { + for (Transfer transfer : stop.getTransfers()) { + if (transfer.getTransferType() == TransferType.MINIMUM_TIME && stop != transfer.getToStop()) { + if (transfer.getMinTransferTime().isEmpty()) { + throw new IllegalStateException( + "Minimal transfer time is not present for transfer of type " + transfer.getTransferType() + " from stop " + stop.getId() + " to stop " + stop.getId()); + } + + try { + builder.addTransfer(stop.getId(), transfer.getToStop().getId(), + transfer.getMinTransferTime().get()); + } catch (IllegalArgumentException e) { + // TODO: Problem is that with active trips we already filtered some stops which have no active + // trip anymore, so they are not added. Maybe we should build the Raptor always for the + // complete schedule, and add use masking array for the stop times of when we want to create + // routes at a specific date. This would also be more efficient. + log.warn("Omit adding transfer: {}", e.getMessage()); + } + } + } + } + } } From 608a788d9642f444babbcefad4ec1d82a9eb5f60 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Tue, 7 May 2024 15:10:27 +0200 Subject: [PATCH 3/7] TEST: NAV-18 - Add transfers to tests - Include a transfers.txt file in the GTFS example feed. - Add transfers to the GTFS test builder. --- .../gtfs/schedule/model/GtfsScheduleTest.java | 1 + .../model/GtfsScheduleTestBuilder.java | 15 +++++++++++++++ src/test/resources/gtfs/schedule/SOURCE.md | 12 ++++++++++++ .../resources/gtfs/schedule/sample-feed-1.zip | Bin 3217 -> 3073 bytes 4 files changed, 28 insertions(+) diff --git a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java index b8d9e993..cbb0a6c7 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java @@ -26,6 +26,7 @@ void setUp(GtfsScheduleTestBuilder builder) { .withAddInterCity() .withAddUnderground() .withAddBus() + .withAddTransfers() .build(); } diff --git a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java index c52d74db..d695c9eb 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java @@ -45,6 +45,13 @@ *

  • u5 - South-West (47.4, 8.4)
  • *
  • u6 - West (47.5, 8.4)
  • * + * Transfers: + *
      + *
    • u6-u5 - Minimum time transfer requiring 540 seconds, reflecting a downhill walking path between these stations.
    • + *
    • u5-u6 - Minimum time transfer requiring 600 seconds, reflecting an uphill walking path between these stations.
    • + *
    • u6 - Marking transfer at u6 as not possible, emphasizing route planning around unavailable transfers.
    • + *
    • u3 - Recommendation to transfer at u3 instead of u6 when traveling from East to West.
    • + *
    * * @author munterfi */ @@ -105,6 +112,14 @@ public GtfsScheduleTestBuilder withAddBus() { return this; } + public GtfsScheduleTestBuilder withAddTransfers() { + builder.addTransfer("u6", "u5", TransferType.MINIMUM_TIME, 9 * 60); + builder.addTransfer("u5", "u6", TransferType.MINIMUM_TIME, 10 * 60); + builder.addTransfer("u6", "u6", TransferType.NOT_POSSIBLE, null); + builder.addTransfer("u3", "u6", TransferType.NOT_POSSIBLE, null); + return this; + } + public GtfsSchedule build() { return builder.build(); } diff --git a/src/test/resources/gtfs/schedule/SOURCE.md b/src/test/resources/gtfs/schedule/SOURCE.md index 416b7a1c..348917e9 100644 --- a/src/test/resources/gtfs/schedule/SOURCE.md +++ b/src/test/resources/gtfs/schedule/SOURCE.md @@ -7,3 +7,15 @@ For a detailed description of the GTFS sample feed and its components, visit the The actual data can be downloaded from the following link provided by Google's GTFS specification: [GTFS Sample Feed 1](https://github.com/google/transit/blob/master/gtfs/spec/en/examples/sample-feed-1.zip?raw=true) + +Notes: + +- The `transfers.txt` file was added manually. + ```text + from_stop_id,to_stop_id,transfer_type,min_transfer_time + EMSI,STAGECOACH,2,120 // Downhill walk, less time required + STAGECOACH,EMSI,2,180 // Uphill walk, more time required + BEATTY_AIRPORT,BEATTY_AIRPORT,0, // Recommended transfer at the airport + FUR_CREEK_RES,BULLFROG,3, // Transfer not possible, steep terrain prevents it + BULLFROG,FUR_CREEK_RES,3, // Transfer not possible, steep terrain prevents it + ``` diff --git a/src/test/resources/gtfs/schedule/sample-feed-1.zip b/src/test/resources/gtfs/schedule/sample-feed-1.zip index 79819e21ad51d7b0dcf0aeb51a298437df4bdfc3..0586eb2aa2f795b120f6c49834e6f3f92e88ec40 100644 GIT binary patch delta 1485 zcmbOz*(fnVhKGS6F+DXexl*sBqGY0NB_9Jra$-(uUP@vSSmb7=jtBz?@FsScDc4+I zV*q4>un-W#RK}+ymZTPg)K6|^^kHO}e1TD9@=wMh0S1P&#G=&rqS73w^aQ48=?b9E zNJa(*ejuGzlv-E{v_2CmFU_3Fny6>E<-p_?W-XxGit0OnuUP@=xm1KlKg`BlFVF)bnoQo{3Zv1*88)6_SL< zuV}HFyxjkuX><1i&upJ_Cw|Vbv`(0)s_CC3lp0yo?YGBcA4{jJRO(Sv;jicS>Q z^wclRJ#uKnwa*(wn-X&~`f|1kN*}&;_}n)Gqv8gh!tGwx=b7JmItZ;|wwQf1>DBsl zE$dy#M#6KXQAaJykqZ*z!a`P;-y9^p?L9z8Bi7W(paIX4(=ULYCyPw}NUM`Vwx={6hgUt7t?@pWTcKNx}ilc7f z-5(Du4tGl)h`6{o#A)7wHs{#FZ>jfOFCJeMEpqLif(Os9Ra;u^*EDR-yQE_JXr_r& z#`3~9TV5^Hyv|-Xn=Ni~EQbs?$Vr(6kaRzj<06v?!{i)J2?d~2fq;=g0Zb$3uK;gG zkfM#867>){h@w^?2bPsUnFyi`ceVrRh3FIkh9FEQA`>F?b1*mpvu&nei%S?#PX-VR zgETWRa6t_yfn?TtWG!t#EngX$>^y<$;(!>Gw?F`5Xr7Yyn9aMhP8e%u`q1-UHNP4YY>~q!kDtHj6SNmj@7yllxgv!}2Fk5u%Jh zcG@DK;n6ErD<3g&S$4y2S8;LQrOje&s&2oD0oQwx-P7y#K`)T#gg delta 1580 zcmZpam?$|xMp2X@F+DXexl*sBq9inglYyDtTGGwaUeYbJf`#ELqX@%9jY?%vhUCPY z)V!3$BDgYhTQp_sGbOngK!7)~!%Vs6`Wgcu8-#@zL?;_E3X6hGUj@|Whh+L>7e*gO z(a94TMIiE=c4+cT8H-d!8PXDqQsawCb5e`p4(I`@^hY*HohjO&9B5x8BLf3J15i&< zYGEnRYne!z-U2lRAZwb-l*)4U!MkIV*_pL~VNsM{T7slpzy?jZE%Obas^W~q0wh(N zfrbVm8>-0Sq~8m4MKucp10T>O#U=R#@ga@=OX+A4w{@Bxb zn(_TS7m3u5w_8;{KKwiF!yoO5{mU=ylYN!s=H(-l%Y+*z9}!^*V=))?J?y`2TCHw3OcIxKMjr(wbPV zjMj!wMd5AAY21>DMj9nb6Gb=9;&`iUq;n-Y@7a-coU7*kyJ2F&=zHkMhGz@%oM(5m zCa#@-cp69B)`j;rU3JOf$>xu~eE7r}Mx!G)wQ~3JSEw+0YM4LB5$|5Pf8NSB^=B3? zktoW1)w*nfW8TtL77+%9$wF+R3Xs@BB(rQ_b_ha>+|5R8sf z2_0si>VG<)1*W&9z?kL)#&StfW&uK912BS-^-YfDxX2{VJlTL#qMjG1i-7^CjbTY6 zh=rU3Ss^(vw1R;lz?%`M1L!aY4u%Ur3vIZ34yFQmi-4Gi0ojmQKoQg&3N!|kLlMRx zQ z%Et)Zlm9cK`r4m~0V7WX?F8j%gf57kJArmu6(&R?vNp1dZ-NX$&*(stKp7p?q{$1o zM0An0PGV+2$?rg2p!|-|HTf?ys;j$!`58Iy19gD%K0*h?#fyM(6}@7$BBBUDw)O|e z5VQgTXb`ACKsP8VzGJck8$U{VImQOj4ozu5ouCwot`jqTB0C|Lg8@0U0?h!WR)`s_ UK=T Date: Tue, 7 May 2024 15:11:05 +0200 Subject: [PATCH 4/7] DOC: NAV-18 - Adjust javadoc --- .../naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java | 7 +++---- src/test/java/ch/naviqore/Benchmark.java | 3 +++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java index 77a69ed0..608e66aa 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java @@ -23,10 +23,10 @@ * Provides a builder pattern implementation for constructing a {@link GtfsSchedule} instance. This class encapsulates * the complexity of assembling a GTFS schedule by incrementally adding components such as agencies, stops, routes, * trips, and calendars. The builder ensures that all components are added in a controlled manner and that the resulting - * schedule is consistent and ready for use. + * schedule is consistent. * - *

    Instances of this class should be obtained through the static {@code builder()} method. This class uses - * a private constructor to enforce the use of the builder pattern.

    + *

    Instances of this class should be obtained through the static {@code GtfsSchedule.builder()} method. This class + * uses a private constructor to enforce the use of the builder pattern.

    * * @author munterfi */ @@ -141,7 +141,6 @@ public GtfsScheduleBuilder addTransfer(String fromStopId, String toStopId, Trans throw new IllegalArgumentException("Stop " + toStopId + " does not exist"); } log.debug("Adding transfer {}-{} of type {} {}", fromStopId, toStopId, transferType, minTransferTime); - // TODO: Handle case when minTransferTime is missing, add transfers.txt to test data. fromStop.addTransfer(new Transfer(fromStop, toStop, transferType, minTransferTime)); return this; } diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java index 85660ee2..fed21c7b 100644 --- a/src/test/java/ch/naviqore/Benchmark.java +++ b/src/test/java/ch/naviqore/Benchmark.java @@ -25,6 +25,9 @@ * Benchmark for Raptor routing algorithm. *

    * Measures the time it takes to route a number of requests using Raptor algorithm on large GTFS datasets. + *

    + * Note: To run this benchmark, ensure that the log level is set to INFO in the + * {@code src/test/resources/log4j2.properties} file. * * @author munterfi */ From 45e1ed1890fb7462b8529b0f5724b15c8a12f2ff Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Tue, 7 May 2024 15:11:27 +0200 Subject: [PATCH 5/7] STYLE: NAV-18 - Add blank lines after class headers --- .../java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java | 2 ++ src/main/java/ch/naviqore/gtfs/schedule/model/Route.java | 1 + src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java | 1 + src/main/java/ch/naviqore/gtfs/schedule/model/Transfer.java | 1 + src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java | 1 + src/main/java/ch/naviqore/gtfs/schedule/type/RouteType.java | 1 - src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java | 4 ++++ src/test/java/ch/naviqore/BenchmarkData.java | 1 + 8 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java index d8aa3908..cb8e4480 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleReader.java @@ -38,6 +38,7 @@ public class GtfsScheduleReader { private static void readFromDirectory(File directory, GtfsScheduleParser parser) throws IOException { for (GtfsScheduleFile fileType : GtfsScheduleFile.values()) { File csvFile = new File(directory, fileType.getFileName()); + if (csvFile.exists()) { log.info("Reading GTFS CSV file: {}", csvFile.getAbsolutePath()); readCsvFile(csvFile, parser, fileType); @@ -51,6 +52,7 @@ private static void readFromZip(File zipFile, GtfsScheduleParser parser) throws try (ZipFile zf = new ZipFile(zipFile, StandardCharsets.UTF_8)) { for (GtfsScheduleFile fileType : GtfsScheduleFile.values()) { ZipEntry entry = zf.getEntry(fileType.getFileName()); + if (entry != null) { log.info("Reading GTFS file from ZIP: {}", entry.getName()); try (InputStreamReader reader = new InputStreamReader(BOMInputStream.builder() diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java index c78b083b..1202d357 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java @@ -13,6 +13,7 @@ @RequiredArgsConstructor(access = AccessLevel.PACKAGE) @Getter public final class Route implements Initializable { + private final String id; private final Agency agency; private final String shortName; diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java index 5012f85e..57918631 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java @@ -13,6 +13,7 @@ @RequiredArgsConstructor(access = AccessLevel.PACKAGE) @Getter public final class Stop implements Initializable { + private final String id; private final String name; private final Coordinate coordinate; diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Transfer.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Transfer.java index 5b618e5b..dcafa1d5 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/Transfer.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Transfer.java @@ -12,6 +12,7 @@ @RequiredArgsConstructor(access = AccessLevel.PACKAGE) @Getter public class Transfer { + private final Stop fromStop; private final Stop toStop; private final TransferType transferType; diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java index 3735c3bb..b2d24da5 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java @@ -11,6 +11,7 @@ @RequiredArgsConstructor @Getter public final class Trip implements Comparable, Initializable { + private final String id; private final Route route; private final Calendar calendar; diff --git a/src/main/java/ch/naviqore/gtfs/schedule/type/RouteType.java b/src/main/java/ch/naviqore/gtfs/schedule/type/RouteType.java index 043fe6ee..a5b8456f 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/type/RouteType.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/RouteType.java @@ -17,7 +17,6 @@ public interface RouteType { * @throws NumberFormatException if the code is not a valid integer * @throws IllegalArgumentException if the code is negative or invalid */ - static RouteType parse(String code) { return parse(Integer.parseInt(code)); } diff --git a/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java b/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java index 466b1217..d1dd2810 100644 --- a/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java +++ b/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java @@ -18,6 +18,7 @@ */ @Log4j2 public class GtfsRoutePartitioner { + private final Map> subRoutes = new HashMap<>(); public GtfsRoutePartitioner(GtfsSchedule schedule) { @@ -48,6 +49,7 @@ private List extractStopSequence(Trip trip) { for (StopTime stopTime : trip.getStopTimes()) { sequence.add(stopTime.stop()); } + return sequence; } @@ -56,6 +58,7 @@ public List getSubRoutes(Route route) { if (currentSubRoutes == null) { throw new IllegalArgumentException("Route " + route.getId() + " not found in schedule"); } + return new ArrayList<>(currentSubRoutes.values()); } @@ -65,6 +68,7 @@ public SubRoute getSubRoute(Trip trip) { throw new IllegalArgumentException("Trip " + trip.getId() + " not found in schedule"); } String key = generateStopSequenceKey(trip); + return currentSubRoutes.get(key); } diff --git a/src/test/java/ch/naviqore/BenchmarkData.java b/src/test/java/ch/naviqore/BenchmarkData.java index acaada2e..d468c42b 100644 --- a/src/test/java/ch/naviqore/BenchmarkData.java +++ b/src/test/java/ch/naviqore/BenchmarkData.java @@ -22,6 +22,7 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) @Log4j2 final class BenchmarkData { + private static final Path DATA_DIRECTORY = Path.of("benchmark/input"); private static final HttpClient httpClient = HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.ALWAYS) From ee402265cff73053bfd8a5e69ea15f823bd1051f Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Wed, 8 May 2024 16:17:01 +0200 Subject: [PATCH 6/7] ENH: NAV-18 - Reduce GTFS memory consumption - Copy list and maps into immutable collections. This optimizes lookups on maps (load factor) and in case of array lists it frees unused capacity in the backing array. - Add tests for immutability. - Introduce a state in the GTFS builder, to ensure build is only called once, without a reset of the builder. --- .../gtfs/schedule/model/Calendar.java | 13 +- .../gtfs/schedule/model/GtfsSchedule.java | 43 +- .../schedule/model/GtfsScheduleBuilder.java | 76 ++- .../naviqore/gtfs/schedule/model/Route.java | 3 +- .../ch/naviqore/gtfs/schedule/model/Stop.java | 6 +- .../ch/naviqore/gtfs/schedule/model/Trip.java | 3 +- .../naviqore/raptor/model/RaptorBuilder.java | 6 +- .../gtfs/schedule/model/GtfsScheduleTest.java | 521 +++++++++++------- .../model/GtfsScheduleTestBuilder.java | 2 + 9 files changed, 421 insertions(+), 252 deletions(-) diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Calendar.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Calendar.java index 696e2902..bebd1970 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/Calendar.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Calendar.java @@ -11,14 +11,14 @@ @RequiredArgsConstructor(access = AccessLevel.PACKAGE) @Getter -public final class Calendar { +public final class Calendar implements Initializable { private final String id; private final EnumSet serviceDays; private final LocalDate startDate; private final LocalDate endDate; - private final Map calendarDates = new HashMap<>(); - private final List trips = new ArrayList<>(); + private Map calendarDates = new HashMap<>(); + private List trips = new ArrayList<>(); /** * Determines if the service is operational on a specific day, considering both regular service days and @@ -38,6 +38,13 @@ public boolean isServiceAvailable(LocalDate date) { return serviceDays.contains(date.getDayOfWeek()); } + @Override + public void initialize() { + Collections.sort(trips); + trips = List.copyOf(trips); + calendarDates = Map.copyOf(calendarDates); + } + void addCalendarDate(CalendarDate calendarDate) { calendarDates.put(calendarDate.date(), calendarDate); } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java index cccf5588..c77ceb33 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java @@ -1,12 +1,10 @@ package ch.naviqore.gtfs.schedule.model; import ch.naviqore.gtfs.schedule.spatial.Coordinate; -import lombok.AccessLevel; -import lombok.RequiredArgsConstructor; +import lombok.Getter; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -14,11 +12,12 @@ /** * General Transit Feed Specification (GTFS) schedule *

    - * Use the {@link GtfsScheduleBuilder} to construct a GTFS schedule instance. + * This is an immutable class, meaning that once an instance is created, it cannot be modified. Use the + * {@link GtfsScheduleBuilder} to construct a GTFS schedule instance. * * @author munterfi */ -@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +@Getter public class GtfsSchedule { private final Map agencies; @@ -27,6 +26,21 @@ public class GtfsSchedule { private final Map routes; private final Map trips; + /** + * Constructs an immutable GTFS schedule. + *

    + * Each map passed to this constructor is copied into an immutable map to prevent further modification and to + * enhance memory efficiency and thread-safety in a concurrent environment. + */ + GtfsSchedule(Map agencies, Map calendars, Map stops, + Map routes, Map trips) { + this.agencies = Map.copyOf(agencies); + this.calendars = Map.copyOf(calendars); + this.stops = Map.copyOf(stops); + this.routes = Map.copyOf(routes); + this.trips = Map.copyOf(trips); + } + /** * Creates a new GTFS schedule builder. * @@ -88,23 +102,4 @@ public List getActiveTrips(LocalDate date) { .collect(Collectors.toList()); } - public Map getAgencies() { - return Collections.unmodifiableMap(agencies); - } - - public Map getCalendars() { - return Collections.unmodifiableMap(calendars); - } - - public Map getStops() { - return Collections.unmodifiableMap(stops); - } - - public Map getRoutes() { - return Collections.unmodifiableMap(routes); - } - - public Map getTrips() { - return Collections.unmodifiableMap(trips); - } } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java index 608e66aa..b1c9a2dd 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java @@ -18,15 +18,11 @@ import java.util.concurrent.ConcurrentHashMap; /** - * General Transit Feed Specification (GTFS) schedule builder + * Implements a builder pattern for constructing instances of {@link GtfsSchedule}. This builder helps assemble a GTFS + * schedule by adding components like agencies, stops, routes, trips, and calendars in a controlled and consistent + * manner. *

    - * Provides a builder pattern implementation for constructing a {@link GtfsSchedule} instance. This class encapsulates - * the complexity of assembling a GTFS schedule by incrementally adding components such as agencies, stops, routes, - * trips, and calendars. The builder ensures that all components are added in a controlled manner and that the resulting - * schedule is consistent. - * - *

    Instances of this class should be obtained through the static {@code GtfsSchedule.builder()} method. This class - * uses a private constructor to enforce the use of the builder pattern.

    + * Use {@link GtfsSchedule#builder()} to obtain an instance. * * @author munterfi */ @@ -41,7 +37,10 @@ public class GtfsScheduleBuilder { private final Map routes = new HashMap<>(); private final Map trips = new HashMap<>(); + private boolean built = false; + public GtfsScheduleBuilder addAgency(String id, String name, String url, String timezone) { + checkNotBuilt(); if (agencies.containsKey(id)) { throw new IllegalArgumentException("Agency " + id + " already exists"); } @@ -51,6 +50,7 @@ public GtfsScheduleBuilder addAgency(String id, String name, String url, String } public GtfsScheduleBuilder addStop(String id, String name, double lat, double lon) { + checkNotBuilt(); if (stops.containsKey(id)) { throw new IllegalArgumentException("Agency " + id + " already exists"); } @@ -60,6 +60,7 @@ public GtfsScheduleBuilder addStop(String id, String name, double lat, double lo } public GtfsScheduleBuilder addRoute(String id, String agencyId, String shortName, String longName, RouteType type) { + checkNotBuilt(); if (routes.containsKey(id)) { throw new IllegalArgumentException("Route " + id + " already exists"); } @@ -74,6 +75,7 @@ public GtfsScheduleBuilder addRoute(String id, String agencyId, String shortName public GtfsScheduleBuilder addCalendar(String id, EnumSet serviceDays, LocalDate startDate, LocalDate endDate) { + checkNotBuilt(); if (calendars.containsKey(id)) { throw new IllegalArgumentException("Calendar " + id + " already exists"); } @@ -83,6 +85,7 @@ public GtfsScheduleBuilder addCalendar(String id, EnumSet serviceDays } public GtfsScheduleBuilder addCalendarDate(String calendarId, LocalDate date, ExceptionType type) { + checkNotBuilt(); Calendar calendar = calendars.get(calendarId); if (calendar == null) { throw new IllegalArgumentException("Calendar " + calendarId + " does not exist"); @@ -94,6 +97,7 @@ public GtfsScheduleBuilder addCalendarDate(String calendarId, LocalDate date, Ex } public GtfsScheduleBuilder addTrip(String id, String routeId, String serviceId) { + checkNotBuilt(); if (trips.containsKey(id)) { throw new IllegalArgumentException("Trip " + id + " already exists"); } @@ -115,6 +119,7 @@ public GtfsScheduleBuilder addTrip(String id, String routeId, String serviceId) public GtfsScheduleBuilder addStopTime(String tripId, String stopId, ServiceDayTime arrival, ServiceDayTime departure) { + checkNotBuilt(); Trip trip = trips.get(tripId); if (trip == null) { throw new IllegalArgumentException("Trip " + tripId + " does not exist"); @@ -123,7 +128,7 @@ public GtfsScheduleBuilder addStopTime(String tripId, String stopId, ServiceDayT if (stop == null) { throw new IllegalArgumentException("Stop " + stopId + " does not exist"); } - log.debug("Adding stop {} to trip {} ({}-{})", stopId, tripId, arrival, departure); + log.debug("Adding stop time at {} to trip {} ({}-{})", stopId, tripId, arrival, departure); StopTime stopTime = new StopTime(stop, trip, cache.getOrAdd(arrival), cache.getOrAdd(departure)); stop.addStopTime(stopTime); trip.addStopTime(stopTime); @@ -132,6 +137,7 @@ public GtfsScheduleBuilder addStopTime(String tripId, String stopId, ServiceDayT public GtfsScheduleBuilder addTransfer(String fromStopId, String toStopId, TransferType transferType, @Nullable Integer minTransferTime) { + checkNotBuilt(); Stop fromStop = stops.get(fromStopId); if (fromStop == null) { throw new IllegalArgumentException("Stop " + fromStopId + " does not exist"); @@ -145,13 +151,52 @@ public GtfsScheduleBuilder addTransfer(String fromStopId, String toStopId, Trans return this; } + /** + * Constructs and returns a {@link GtfsSchedule} using the current builder state. + *

    + * This method finalizes the schedule and initializes all components. After this method is called, the builder is + * cleared and cannot be used to build another schedule without being reset. + * + * @return The constructed {@link GtfsSchedule}. + * @throws IllegalStateException if the builder has already built a schedule. + */ public GtfsSchedule build() { + checkNotBuilt(); log.info("Building schedule with {} stops, {} routes and {} trips", stops.size(), routes.size(), trips.size()); - trips.values().parallelStream().forEach(Trip::initialize); - stops.values().parallelStream().forEach(Stop::initialize); - routes.values().parallelStream().forEach(Route::initialize); + trips.values().parallelStream().forEach(Initializable::initialize); + stops.values().parallelStream().forEach(Initializable::initialize); + routes.values().parallelStream().forEach(Initializable::initialize); + calendars.values().parallelStream().forEach(Initializable::initialize); // TODO: Build k-d tree for spatial indexing - return new GtfsSchedule(agencies, calendars, stops, routes, trips); + GtfsSchedule schedule = new GtfsSchedule(agencies, calendars, stops, routes, trips); + clear(); + built = true; + return schedule; + } + + /** + * Resets the builder to its initial state, allowing it to be reused. + */ + public void reset() { + log.debug("Resetting builder"); + clear(); + built = false; + } + + private void clear() { + log.debug("Clearing maps and cache of the builder"); + agencies.clear(); + calendars.clear(); + stops.clear(); + routes.clear(); + trips.clear(); + cache.clear(); + } + + private void checkNotBuilt() { + if (built) { + throw new IllegalStateException("Cannot modify builder after build() has been called."); + } } /** @@ -168,5 +213,10 @@ public LocalDate getOrAdd(LocalDate value) { public ServiceDayTime getOrAdd(ServiceDayTime value) { return serviceDayTimes.computeIfAbsent(value, k -> value); } + + public void clear() { + localDates.clear(); + serviceDayTimes.clear(); + } } } diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java index 1202d357..dcfd1eee 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Route.java @@ -19,7 +19,7 @@ public final class Route implements Initializable { private final String shortName; private final String longName; private final RouteType type; - private final List trips = new ArrayList<>(); + private List trips = new ArrayList<>(); void addTrip(Trip trip) { trips.add(trip); @@ -28,6 +28,7 @@ void addTrip(Trip trip) { @Override public void initialize() { Collections.sort(trips); + trips = List.copyOf(trips); } @Override diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java index 57918631..7076c1d0 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java @@ -17,8 +17,8 @@ public final class Stop implements Initializable { private final String id; private final String name; private final Coordinate coordinate; - private final List stopTimes = new ArrayList<>(); - private final List transfers = new ArrayList<>(); + private List stopTimes = new ArrayList<>(); + private List transfers = new ArrayList<>(); void addStopTime(StopTime stopTime) { stopTimes.add(stopTime); @@ -31,6 +31,8 @@ void addTransfer(Transfer transfer) { @Override public void initialize() { Collections.sort(stopTimes); + stopTimes = List.copyOf(stopTimes); + transfers = List.copyOf(transfers); } @Override diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java b/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java index b2d24da5..5c7d5751 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/Trip.java @@ -15,7 +15,7 @@ public final class Trip implements Comparable, Initializable { private final String id; private final Route route; private final Calendar calendar; - private final List stopTimes = new ArrayList<>(); + private List stopTimes = new ArrayList<>(); void addStopTime(StopTime stopTime) { stopTimes.add(stopTime); @@ -24,6 +24,7 @@ void addStopTime(StopTime stopTime) { @Override public void initialize() { Collections.sort(stopTimes); + stopTimes = List.copyOf(stopTimes); } @Override diff --git a/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java b/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java index 86c3f40f..ddbd5474 100644 --- a/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java +++ b/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java @@ -105,12 +105,12 @@ public Raptor build() { } private Lookup buildLookup() { - log.info("Building lookup with {} stops and {} routes", stopSize, routeSize); + log.debug("Building lookup with {} stops and {} routes", stopSize, routeSize); return new Lookup(new HashMap<>(stops), new HashMap<>(routes)); } private StopContext buildStopContext() { - log.info("Building stop context with {} stops and {} transfers", stopSize, transferSize); + log.debug("Building stop context with {} stops and {} transfers", stopSize, transferSize); Stop[] stopArr = new Stop[stopSize]; int[] stopRouteArr = new int[stopRoutes.values().stream().mapToInt(Set::size).sum()]; Transfer[] transferArr = new Transfer[transferSize]; @@ -145,7 +145,7 @@ private StopContext buildStopContext() { } private RouteTraversal buildRouteTraversal() { - log.info("Building route traversal with {} routes, {} route stops, {} stop times", routeSize, routeStopSize, + log.debug("Building route traversal with {} routes, {} route stops, {} stop times", routeSize, routeStopSize, stopTimeSize); Route[] routeArr = new Route[routeSize]; RouteStop[] routeStopArr = new RouteStop[routeStopSize]; diff --git a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java index cbb0a6c7..78105514 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java @@ -1,11 +1,13 @@ package ch.naviqore.gtfs.schedule.model; +import ch.naviqore.gtfs.schedule.type.ExceptionType; import ch.naviqore.gtfs.schedule.type.ServiceDayTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.Set; @@ -16,226 +18,335 @@ @ExtendWith(GtfsScheduleTestExtension.class) class GtfsScheduleTest { - private GtfsSchedule schedule; + private GtfsScheduleBuilder builder; @BeforeEach - void setUp(GtfsScheduleTestBuilder builder) { - schedule = builder.withAddAgency() + void setUp(GtfsScheduleTestBuilder testBuilder) { + builder = testBuilder.withAddAgency() .withAddCalendars() .withAddCalendarDates() .withAddInterCity() .withAddUnderground() .withAddBus() .withAddTransfers() - .build(); + .getBuilder(); } - @Nested - class Builder { - - @Test - void shouldCorrectlyCountAgencies() { - assertThat(schedule.getAgencies()).hasSize(2); - } - - @Test - void shouldCorrectlyCountRoutes() { - assertThat(schedule.getRoutes()).hasSize(3); - } - - @Test - void shouldCorrectlyCountStops() { - assertThat(schedule.getStops()).hasSize(9); - } - - @Test - void shouldCorrectlyCountTrips() { - assertThat(schedule.getTrips()).hasSize(671); - } - - @Test - void shouldCorrectlyCountCalendars() { - assertThat(schedule.getCalendars()).hasSize(2); - } + @Test + void shouldPreventMultipleBuildCalls() { + builder.build(); + assertThatThrownBy(() -> builder.build()).isInstanceOf(IllegalStateException.class); } - @Nested - class NearestStops { - - @Test - void shouldFindStopWithin1Meter() { - assertThat(schedule.getNearestStops(47.5, 8.5, 1)).hasSize(1).extracting("id").containsOnly("s2"); - } - - @Test - void shouldFindStopsWithin10000Meters() { - assertThat(schedule.getNearestStops(47.5, 8.5, 10000)).hasSize(3) - .extracting("id") - .containsOnly("u6", "s2", "u3"); - } - - @Test - void shouldFindAllStops() { - assertThat(schedule.getNearestStops(47.5, 8.5, Integer.MAX_VALUE)).hasSize(9) - .extracting("id") - .containsOnly("s1", "s2", "s3", "u1", "u2", "u3", "u4", "u5", "u6"); - } - - @Test - void shouldFindNoStopsWhenNoneAreCloseEnough() { - assertThat(schedule.getNearestStops(47.6, 8.5, 100)).isEmpty(); - } + @Test + void shouldAllowBuildAfterReset() { + builder.build(); + builder.reset(); + GtfsSchedule schedule = builder.build(); + assertThat(schedule.getAgencies()).isEmpty(); + assertThat(schedule.getCalendars()).isEmpty(); + assertThat(schedule.getStops()).isEmpty(); + assertThat(schedule.getRoutes()).isEmpty(); + assertThat(schedule.getTrips()).isEmpty(); } @Nested - class NextDepartures { - - private static final String STOP_ID = "s2"; - private static final int LIMIT = 5; - - private static void assertWeekendAndHoliday(List departures) { - // assert departures times are correct - List expectedDepartures = List.of(ServiceDayTime.parse("08:15:00"), - ServiceDayTime.parse("08:15:00"), ServiceDayTime.parse("09:15:00"), - ServiceDayTime.parse("09:15:00"), ServiceDayTime.parse("10:15:00")); - assertThat(departures).hasSize(LIMIT) - .extracting(StopTime::departure) - .containsExactlyElementsOf(expectedDepartures); - - // assert trips are correct - List expectedTripIds = List.of("route1_we_f_4", "route1_we_r_4", "route1_we_f_5", "route1_we_r_5", - "route1_we_f_6"); - List tripIds = departures.stream().map(stopTime -> stopTime.trip().getId()).toList(); - assertThat(tripIds).containsExactlyElementsOf(expectedTripIds); - - // assert routes are correct - Set expectedRouteIds = Set.of("route1"); - List routeIds = departures.stream().map(stopTime -> stopTime.trip().getRoute().getId()).toList(); - assertThat(routeIds).allMatch(expectedRouteIds::contains); - } - - @Test - void shouldReturnNextDeparturesOnWeekday() { - List departures = schedule.getNextDepartures(STOP_ID, - GtfsScheduleTestBuilder.Moments.WEEKDAY_8_AM, LIMIT); - - // assert departures times are correct - List expectedDepartures = List.of(ServiceDayTime.parse("08:00:00"), - ServiceDayTime.parse("08:03:00"), ServiceDayTime.parse("08:09:00"), - ServiceDayTime.parse("08:09:00"), ServiceDayTime.parse("08:15:00")); - assertThat(departures).hasSize(LIMIT) - .extracting(StopTime::departure) - .containsExactlyElementsOf(expectedDepartures); - - // assert trips are correct - List expectedTripIds = List.of("route3_wd_f_16", "route3_wd_r_17", "route3_wd_f_17", - "route3_wd_r_17", "route1_wd_f_7"); - List tripIds = departures.stream().map(stopTime -> stopTime.trip().getId()).toList(); - assertThat(tripIds).containsExactlyElementsOf(expectedTripIds); - - // assert routes are correct - Set expectedRouteIds = Set.of("route1", "route3"); - List routeIds = departures.stream().map(stopTime -> stopTime.trip().getRoute().getId()).toList(); - assertThat(routeIds).allMatch(expectedRouteIds::contains); - } - - @Test - void shouldReturnNextDeparturesOnWeekend() { - List departures = schedule.getNextDepartures(STOP_ID, - GtfsScheduleTestBuilder.Moments.WEEKEND_8_AM, LIMIT); - - assertWeekendAndHoliday(departures); - } - - @Test - void shouldReturnNextDeparturesOnHoliday() { - List departures = schedule.getNextDepartures(STOP_ID, - GtfsScheduleTestBuilder.Moments.HOLIDAY.atTime(8, 0), LIMIT); - - assertWeekendAndHoliday(departures); - } - - @Test - void shouldReturnNextDeparturesAfterMidnight() { - List departures = schedule.getNextDepartures(STOP_ID, - GtfsScheduleTestBuilder.Moments.WEEKDAY_12_PM, LIMIT); - - // assert departures times are correct - List expectedDepartures = List.of(ServiceDayTime.parse("24:00:00"), - ServiceDayTime.parse("24:03:00"), ServiceDayTime.parse("24:09:00"), - ServiceDayTime.parse("24:09:00"), ServiceDayTime.parse("24:15:00")); - assertThat(departures).hasSize(LIMIT) - .extracting(StopTime::departure) - .containsExactlyElementsOf(expectedDepartures); - - // assert trips are correct - List expectedTripIds = List.of("route3_wd_f_80", "route3_wd_r_81", "route3_wd_f_81", - "route3_wd_r_81", "route1_wd_f_39"); - List tripIds = departures.stream().map(stopTime -> stopTime.trip().getId()).toList(); - assertThat(tripIds).containsExactlyElementsOf(expectedTripIds); - - // assert routes are correct - Set expectedRouteIds = Set.of("route1", "route3"); - List routeIds = departures.stream().map(stopTime -> stopTime.trip().getRoute().getId()).toList(); - assertThat(routeIds).allMatch(expectedRouteIds::contains); - } - - @Test - void shouldReturnNoNextDeparturesOnNoServiceDay() { - assertThat(schedule.getNextDepartures(STOP_ID, GtfsScheduleTestBuilder.Moments.NO_SERVICE.atTime(8, 0), - Integer.MAX_VALUE)).isEmpty(); - } - - @Test - void shouldReturnNoDeparturesFromUnknownStop() { - assertThatThrownBy(() -> schedule.getNextDepartures("unknown", LocalDateTime.now(), 1)).isInstanceOf( - IllegalArgumentException.class).hasMessage("Stop unknown not found"); - } - } - - @Nested - class ActiveTrips { - - private static void assertWeekendAndHoliday(List activeTrips) { - assertThat(activeTrips).hasSize(168) - .extracting(trip -> trip.getRoute().getId()) - .containsAll(Set.of("route1", "route2")); - } - - @Test - void shouldReturnActiveTripsOnWeekday() { - List activeTrips = schedule.getActiveTrips( - GtfsScheduleTestBuilder.Moments.WEEKDAY_8_AM.toLocalDate()); - - assertThat(activeTrips).hasSize(503) - .extracting(trip -> trip.getRoute().getId()) - .containsAll(Set.of("route1", "route2", "route3")); - } - - @Test - void shouldReturnActiveTripsOnWeekend() { - List activeTrips = schedule.getActiveTrips( - GtfsScheduleTestBuilder.Moments.WEEKEND_8_AM.toLocalDate()); - - assertWeekendAndHoliday(activeTrips); - } - - @Test - void shouldReturnActiveTripsOnHoliday() { - List activeTrips = schedule.getActiveTrips(GtfsScheduleTestBuilder.Moments.HOLIDAY); - - assertWeekendAndHoliday(activeTrips); - } - - @Test - void shouldReturnNoActiveTripsForDaysOutsideValidity() { - assertThat(schedule.getActiveTrips(GtfsScheduleTestBuilder.Validity.PERIOD_START.minusDays(1))).isEmpty(); - assertThat(schedule.getActiveTrips(GtfsScheduleTestBuilder.Validity.PERIOD_END.plusDays(1))).isEmpty(); - } + class Builder { - @Test - void shouldReturnNoActiveTripsForNonServiceDay() { - assertThat(schedule.getActiveTrips(GtfsScheduleTestBuilder.Moments.NO_SERVICE)).isEmpty(); + @Nested + class Schedule { + + private GtfsSchedule schedule; + + @BeforeEach + void setUp() { + schedule = builder.build(); + } + + @Test + void shouldCorrectlyCountAgencies() { + assertThat(schedule.getAgencies()).hasSize(2); + } + + @Test + void shouldCorrectlyCountRoutes() { + assertThat(schedule.getRoutes()).hasSize(3); + } + + @Test + void shouldCorrectlyCountStops() { + assertThat(schedule.getStops()).hasSize(9); + } + + @Test + void shouldCorrectlyCountTrips() { + assertThat(schedule.getTrips()).hasSize(671); + } + + @Test + void shouldCorrectlyCountCalendars() { + assertThat(schedule.getCalendars()).hasSize(2); + } + + @Nested + class Immutability { + + @Test + void shouldPreventModificationsToRoutes() { + assertThatThrownBy(() -> schedule.getRoutes().put("", null)).isInstanceOf( + UnsupportedOperationException.class); + } + + @Test + void shouldPreventModificationsToStops() { + assertThatThrownBy(() -> schedule.getStops().put("", null)).isInstanceOf( + UnsupportedOperationException.class); + } + + @Test + void shouldPreventModificationsToTrips() { + assertThatThrownBy(() -> schedule.getTrips().put("", null)).isInstanceOf( + UnsupportedOperationException.class); + } + + @Test + void shouldPreventModificationsToAgencies() { + assertThatThrownBy(() -> schedule.getAgencies().put("", null)).isInstanceOf( + UnsupportedOperationException.class); + } + + @Test + void shouldPreventModificationsToCalendars() { + assertThatThrownBy(() -> schedule.getCalendars().put("", null)).isInstanceOf( + UnsupportedOperationException.class); + } + + @Test + void shouldPreventModificationOfTripsOnRoute() { + assertThatThrownBy( + () -> schedule.getRoutes().values().iterator().next().addTrip(null)).isInstanceOf( + UnsupportedOperationException.class); + } + + @Test + void shouldPreventModificationOfTransfersOnStop() { + assertThatThrownBy( + () -> schedule.getStops().values().iterator().next().addTransfer(null)).isInstanceOf( + UnsupportedOperationException.class); + } + + @Test + void shouldPreventModificationOfStopTimesOnStop() { + assertThatThrownBy( + () -> schedule.getStops().values().iterator().next().addStopTime(null)).isInstanceOf( + UnsupportedOperationException.class); + } + + @Test + void shouldPreventModificationOfStopTimesOnTrip() { + assertThatThrownBy( + () -> schedule.getTrips().values().iterator().next().addStopTime(null)).isInstanceOf( + UnsupportedOperationException.class); + } + + @Test + void shouldPreventModificationOfCalendarDatesOnCalendar() { + Calendar calendar = schedule.getCalendars().values().iterator().next(); + assertThatThrownBy(() -> calendar.addCalendarDate( + new CalendarDate(calendar, LocalDate.now(), ExceptionType.ADDED))).isInstanceOf( + UnsupportedOperationException.class); + } + } + + @Nested + class NearestStops { + + @Test + void shouldFindStopWithin1Meter() { + assertThat(schedule.getNearestStops(47.5, 8.5, 1)).hasSize(1).extracting("id").containsOnly("s2"); + } + + @Test + void shouldFindStopsWithin10000Meters() { + assertThat(schedule.getNearestStops(47.5, 8.5, 10000)).hasSize(3) + .extracting("id") + .containsOnly("u6", "s2", "u3"); + } + + @Test + void shouldFindAllStops() { + assertThat(schedule.getNearestStops(47.5, 8.5, Integer.MAX_VALUE)).hasSize(9) + .extracting("id") + .containsOnly("s1", "s2", "s3", "u1", "u2", "u3", "u4", "u5", "u6"); + } + + @Test + void shouldFindNoStopsWhenNoneAreCloseEnough() { + assertThat(schedule.getNearestStops(47.6, 8.5, 100)).isEmpty(); + } + } + + @Nested + class NextDepartures { + + private static final String STOP_ID = "s2"; + private static final int LIMIT = 5; + + private static void assertWeekendAndHoliday(List departures) { + // assert departures times are correct + List expectedDepartures = List.of(ServiceDayTime.parse("08:15:00"), + ServiceDayTime.parse("08:15:00"), ServiceDayTime.parse("09:15:00"), + ServiceDayTime.parse("09:15:00"), ServiceDayTime.parse("10:15:00")); + assertThat(departures).hasSize(LIMIT) + .extracting(StopTime::departure) + .containsExactlyElementsOf(expectedDepartures); + + // assert trips are correct + List expectedTripIds = List.of("route1_we_f_4", "route1_we_r_4", "route1_we_f_5", + "route1_we_r_5", "route1_we_f_6"); + List tripIds = departures.stream().map(stopTime -> stopTime.trip().getId()).toList(); + assertThat(tripIds).containsExactlyElementsOf(expectedTripIds); + + // assert routes are correct + Set expectedRouteIds = Set.of("route1"); + List routeIds = departures.stream() + .map(stopTime -> stopTime.trip().getRoute().getId()) + .toList(); + assertThat(routeIds).allMatch(expectedRouteIds::contains); + } + + @Test + void shouldReturnNextDeparturesOnWeekday() { + List departures = schedule.getNextDepartures(STOP_ID, + GtfsScheduleTestBuilder.Moments.WEEKDAY_8_AM, LIMIT); + + // assert departures times are correct + List expectedDepartures = List.of(ServiceDayTime.parse("08:00:00"), + ServiceDayTime.parse("08:03:00"), ServiceDayTime.parse("08:09:00"), + ServiceDayTime.parse("08:09:00"), ServiceDayTime.parse("08:15:00")); + assertThat(departures).hasSize(LIMIT) + .extracting(StopTime::departure) + .containsExactlyElementsOf(expectedDepartures); + + // assert trips are correct + List expectedTripIds = List.of("route3_wd_f_16", "route3_wd_r_17", "route3_wd_f_17", + "route3_wd_r_17", "route1_wd_f_7"); + List tripIds = departures.stream().map(stopTime -> stopTime.trip().getId()).toList(); + assertThat(tripIds).containsExactlyElementsOf(expectedTripIds); + + // assert routes are correct + Set expectedRouteIds = Set.of("route1", "route3"); + List routeIds = departures.stream() + .map(stopTime -> stopTime.trip().getRoute().getId()) + .toList(); + assertThat(routeIds).allMatch(expectedRouteIds::contains); + } + + @Test + void shouldReturnNextDeparturesOnWeekend() { + List departures = schedule.getNextDepartures(STOP_ID, + GtfsScheduleTestBuilder.Moments.WEEKEND_8_AM, LIMIT); + + assertWeekendAndHoliday(departures); + } + + @Test + void shouldReturnNextDeparturesOnHoliday() { + List departures = schedule.getNextDepartures(STOP_ID, + GtfsScheduleTestBuilder.Moments.HOLIDAY.atTime(8, 0), LIMIT); + + assertWeekendAndHoliday(departures); + } + + @Test + void shouldReturnNextDeparturesAfterMidnight() { + List departures = schedule.getNextDepartures(STOP_ID, + GtfsScheduleTestBuilder.Moments.WEEKDAY_12_PM, LIMIT); + + // assert departures times are correct + List expectedDepartures = List.of(ServiceDayTime.parse("24:00:00"), + ServiceDayTime.parse("24:03:00"), ServiceDayTime.parse("24:09:00"), + ServiceDayTime.parse("24:09:00"), ServiceDayTime.parse("24:15:00")); + assertThat(departures).hasSize(LIMIT) + .extracting(StopTime::departure) + .containsExactlyElementsOf(expectedDepartures); + + // assert trips are correct + List expectedTripIds = List.of("route3_wd_f_80", "route3_wd_r_81", "route3_wd_f_81", + "route3_wd_r_81", "route1_wd_f_39"); + List tripIds = departures.stream().map(stopTime -> stopTime.trip().getId()).toList(); + assertThat(tripIds).containsExactlyElementsOf(expectedTripIds); + + // assert routes are correct + Set expectedRouteIds = Set.of("route1", "route3"); + List routeIds = departures.stream() + .map(stopTime -> stopTime.trip().getRoute().getId()) + .toList(); + assertThat(routeIds).allMatch(expectedRouteIds::contains); + } + + @Test + void shouldReturnNoNextDeparturesOnNoServiceDay() { + assertThat( + schedule.getNextDepartures(STOP_ID, GtfsScheduleTestBuilder.Moments.NO_SERVICE.atTime(8, 0), + Integer.MAX_VALUE)).isEmpty(); + } + + @Test + void shouldReturnNoDeparturesFromUnknownStop() { + assertThatThrownBy( + () -> schedule.getNextDepartures("unknown", LocalDateTime.now(), 1)).isInstanceOf( + IllegalArgumentException.class).hasMessage("Stop unknown not found"); + } + } + + @Nested + class ActiveTrips { + + private static void assertWeekendAndHoliday(List activeTrips) { + assertThat(activeTrips).hasSize(168) + .extracting(trip -> trip.getRoute().getId()) + .containsAll(Set.of("route1", "route2")); + } + + @Test + void shouldReturnActiveTripsOnWeekday() { + List activeTrips = schedule.getActiveTrips( + GtfsScheduleTestBuilder.Moments.WEEKDAY_8_AM.toLocalDate()); + + assertThat(activeTrips).hasSize(503) + .extracting(trip -> trip.getRoute().getId()) + .containsAll(Set.of("route1", "route2", "route3")); + } + + @Test + void shouldReturnActiveTripsOnWeekend() { + List activeTrips = schedule.getActiveTrips( + GtfsScheduleTestBuilder.Moments.WEEKEND_8_AM.toLocalDate()); + + assertWeekendAndHoliday(activeTrips); + } + + @Test + void shouldReturnActiveTripsOnHoliday() { + List activeTrips = schedule.getActiveTrips(GtfsScheduleTestBuilder.Moments.HOLIDAY); + + assertWeekendAndHoliday(activeTrips); + } + + @Test + void shouldReturnNoActiveTripsForDaysOutsideValidity() { + assertThat(schedule.getActiveTrips( + GtfsScheduleTestBuilder.Validity.PERIOD_START.minusDays(1))).isEmpty(); + assertThat( + schedule.getActiveTrips(GtfsScheduleTestBuilder.Validity.PERIOD_END.plusDays(1))).isEmpty(); + } + + @Test + void shouldReturnNoActiveTripsForNonServiceDay() { + assertThat(schedule.getActiveTrips(GtfsScheduleTestBuilder.Moments.NO_SERVICE)).isEmpty(); + } + } } } -} +} \ No newline at end of file diff --git a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java index d695c9eb..83a7474e 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTestBuilder.java @@ -1,6 +1,7 @@ package ch.naviqore.gtfs.schedule.model; import ch.naviqore.gtfs.schedule.type.*; +import lombok.Getter; import lombok.NoArgsConstructor; import java.time.DayOfWeek; @@ -72,6 +73,7 @@ public class GtfsScheduleTestBuilder { new Route("route3", "agency2", "BUS", DefaultRouteType.BUS, 15, NO_HEADWAY, 3, 5, 1, List.of("u5", "s2", "s2"))); private final Set addedStops = new HashSet<>(); + @Getter private final GtfsScheduleBuilder builder = GtfsSchedule.builder(); public GtfsScheduleTestBuilder withAddAgency() { From 2129c23c831801da8a08d25d4b97955e09e95080 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Wed, 15 May 2024 15:01:02 +0200 Subject: [PATCH 7/7] ENH: NAV-18 - Move input check from GtfsToRaptorConverter to GtfsScheduleBuilder --- .../naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java | 4 ++++ .../java/ch/naviqore/raptor/GtfsRoutePartitioner.java | 2 +- .../java/ch/naviqore/raptor/GtfsToRaptorConverter.java | 8 ++------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java index b1c9a2dd..dd348af0 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java @@ -146,6 +146,10 @@ public GtfsScheduleBuilder addTransfer(String fromStopId, String toStopId, Trans if (toStop == null) { throw new IllegalArgumentException("Stop " + toStopId + " does not exist"); } + if (transferType == TransferType.MINIMUM_TIME && minTransferTime == null) { + throw new IllegalArgumentException( + "Minimal transfer time is not present for transfer of type " + transferType.name() + " from stop " + fromStopId + " to stop " + toStopId); + } log.debug("Adding transfer {}-{} of type {} {}", fromStopId, toStopId, transferType, minTransferTime); fromStop.addTransfer(new Transfer(fromStop, toStop, transferType, minTransferTime)); return this; diff --git a/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java b/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java index d1dd2810..d664d093 100644 --- a/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java +++ b/src/main/java/ch/naviqore/raptor/GtfsRoutePartitioner.java @@ -24,7 +24,7 @@ public class GtfsRoutePartitioner { public GtfsRoutePartitioner(GtfsSchedule schedule) { log.info("Partitioning GTFS schedule with {} routes into sub-routes", schedule.getRoutes().size()); schedule.getRoutes().values().forEach(this::processRoute); - log.info("Found {} sub-routes in schedule", subRoutes.values().stream().mapToInt(Map::size).sum()); + log.info("Got {} sub-routes in schedule", subRoutes.values().stream().mapToInt(Map::size).sum()); } private void processRoute(Route route) { diff --git a/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java b/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java index 88fb3754..78d5ecf7 100644 --- a/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java +++ b/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java @@ -75,12 +75,8 @@ private void addRouteStops(Trip trip, GtfsRoutePartitioner.SubRoute subRoute) { private void addTransfers() { for (Stop stop : stops) { for (Transfer transfer : stop.getTransfers()) { - if (transfer.getTransferType() == TransferType.MINIMUM_TIME && stop != transfer.getToStop()) { - if (transfer.getMinTransferTime().isEmpty()) { - throw new IllegalStateException( - "Minimal transfer time is not present for transfer of type " + transfer.getTransferType() + " from stop " + stop.getId() + " to stop " + stop.getId()); - } - + if (transfer.getTransferType() == TransferType.MINIMUM_TIME && stop != transfer.getToStop() && transfer.getMinTransferTime() + .isPresent()) { try { builder.addTransfer(stop.getId(), transfer.getToStop().getId(), transfer.getMinTransferTime().get());