diff --git a/doc-templates/StopConsolidation.md b/doc-templates/StopConsolidation.md new file mode 100644 index 00000000000..690fe0b98e6 --- /dev/null +++ b/doc-templates/StopConsolidation.md @@ -0,0 +1,56 @@ + +# Stop consolidation + +## Contact Info + +- [Jon Campbell](mailto:jon.campbell@arcadis.com), Arcadis, USA + +## Feature explanation + +This sandbox feature allows you to "combine" equivalent stops from across several feeds into a single, +consolidated one. + +It is achieved by defining a "primary" stop and one or more "secondary" stops. During the graph +build all trip patterns are modified so that the secondary ones are swapped out for their +primary equivalent. + +## Effects + +This has the following consequences + +- When you query the departures for a primary stop you see a consolidated view of all the equivalent departures. +- When transferring at a consolidated stop you no longer get instructions like "walk 5 meters to stop X" + +!!! warning "Downsides" + + However, this feature has also severe downsides: + + - It makes realtime trip updates referencing a stop id much more complicated and in many cases + impossible to resolve. + You can only reference a stop by its sequence, which only works in GTFS-RT, not Siri. + - Fare calculation and transfers are unlikely to work as expected. + + +## Configuration + +To enable this feature you need to add a file to OTP's working directory and configure +its name like this: + + + +The additional config file must look like the following: + + + +The column names mean the following: + +- `stop_group_id`: id to group several rows in the file together +- `feed_id`: feed id of the stop +- `stop_id`: id of the stop +- `is_primary`: whether the row represents a primary stop, `1` means yes and `0` means no + diff --git a/docs/BuildConfiguration.md b/docs/BuildConfiguration.md index 6f754d7a26d..52c5e821e82 100644 --- a/docs/BuildConfiguration.md +++ b/docs/BuildConfiguration.md @@ -41,6 +41,7 @@ Sections follow that describe particular settings in more depth. | [readCachedElevations](#readCachedElevations) | `boolean` | Whether to read cached elevation data. | *Optional* | `true` | 2.0 | | staticBikeParkAndRide | `boolean` | Whether we should create bike P+R stations from OSM data. | *Optional* | `false` | 1.5 | | staticParkAndRide | `boolean` | Whether we should create car P+R stations from OSM data. | *Optional* | `true` | 1.5 | +| stopConsolidationFile | `string` | Name of the CSV-formatted file in the build directory which contains the configuration for stop consolidation. | *Optional* | | 2.5 | | [streetGraph](#streetGraph) | `uri` | URI to the street graph object file for reading and writing. | *Optional* | | 2.0 | | [subwayAccessTime](#subwayAccessTime) | `double` | Minutes necessary to reach stops served by trips on routes of route_type=1 (subway) from the street. | *Optional* | `2.0` | 1.5 | | [transitModelTimeZone](#transitModelTimeZone) | `time-zone` | Time zone for the graph. | *Optional* | | 2.2 | @@ -1165,6 +1166,7 @@ case where this is not the case. } } ], + "stopConsolidationFile" : "consolidated-stops.csv", "emissions" : { "carAvgCo2PerKm" : 170, "carAvgOccupancy" : 1.3 diff --git a/docs/examples/ibi/seattle/build-config.json b/docs/examples/ibi/seattle/build-config.json new file mode 100644 index 00000000000..3876037ba3e --- /dev/null +++ b/docs/examples/ibi/seattle/build-config.json @@ -0,0 +1,5 @@ +{ + "transitModelTimeZone": "America/Los_Angeles", + "fares": "orca", + "stopConsolidationFile": "consolidated-stops.csv" +} \ No newline at end of file diff --git a/docs/sandbox/StopConsolidation.md b/docs/sandbox/StopConsolidation.md new file mode 100644 index 00000000000..fa3ff822e88 --- /dev/null +++ b/docs/sandbox/StopConsolidation.md @@ -0,0 +1,78 @@ + +# Stop consolidation + +## Contact Info + +- [Jon Campbell](mailto:jon.campbell@arcadis.com), Arcadis, USA + +## Feature explanation + +This sandbox feature allows you to "combine" equivalent stops from across several feeds into a single, +consolidated one. + +It is achieved by defining a "primary" stop and one or more "secondary" stops. During the graph +build all trip patterns are modified so that the secondary ones are swapped out for their +primary equivalent. + +## Effects + +This has the following consequences + +- When you query the departures for a primary stop you see a consolidated view of all the equivalent departures. +- When transferring at a consolidated stop you no longer get instructions like "walk 5 meters to stop X" + +!!! warning "Downsides" + + However, this feature has also severe downsides: + + - It makes realtime trip updates referencing a stop id much more complicated and in many cases + impossible to resolve. + You can only reference a stop by its sequence, which only works in GTFS-RT, not Siri. + - Fare calculation and transfers are unlikely to work as expected. + + +## Configuration + +To enable this feature you need to add a file to OTP's working directory and configure +its name like this: + + + + +```JSON +// build-config.json +{ + "stopConsolidationFile" : "consolidated-stops.csv" +} +``` + + + +The additional config file must look like the following: + + + + +``` +stop_group_id,feed_id,stop_id,is_primary +1,pierce,1705867009,0 +1,kcm,10225,1 +2,pierce,1569,0 +2,commtrans,473,0 +2,kcm,1040,1 +``` + + + +The column names mean the following: + +- `stop_group_id`: id to group several rows in the file together +- `feed_id`: feed id of the stop +- `stop_id`: id of the stop +- `is_primary`: whether the row represents a primary stop, `1` means yes and `0` means no + diff --git a/mkdocs.yml b/mkdocs.yml index 52124247c20..81d3e1c2052 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,6 +37,7 @@ markdown_extensions: - pymdownx.inlinehilite - pymdownx.snippets - pymdownx.superfences + - admonition # MkDocs will automatically discover pages if you don't list them here. # In that case subdirectories can be used to organize pages. @@ -112,3 +113,4 @@ nav: - Fares: 'sandbox/Fares.md' - Ride Hailing: 'sandbox/RideHailing.md' - Emissions: 'sandbox/Emissions.md' + - Stop Consolidation: 'sandbox/StopConsolidation.md' diff --git a/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/ConsolidatedStopNameFilterTest.java b/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/ConsolidatedStopNameFilterTest.java new file mode 100644 index 00000000000..d88e8a5879e --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/ConsolidatedStopNameFilterTest.java @@ -0,0 +1,46 @@ +package org.opentripplanner.ext.stopconsolidation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.opentripplanner.ext.stopconsolidation.TestStopConsolidationModel.STOP_C; +import static org.opentripplanner.ext.stopconsolidation.TestStopConsolidationModel.STOP_D; +import static org.opentripplanner.model.plan.PlanTestConstants.T11_05; +import static org.opentripplanner.model.plan.PlanTestConstants.T11_12; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationRepository; +import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationService; +import org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup; +import org.opentripplanner.model.plan.Place; +import org.opentripplanner.model.plan.PlanTestConstants; +import org.opentripplanner.model.plan.TestItineraryBuilder; + +class ConsolidatedStopNameFilterTest { + + @Test + void changeNames() { + var transitModel = TestStopConsolidationModel.buildTransitModel(); + + var groups = List.of(new ConsolidatedStopGroup(STOP_C.getId(), List.of(STOP_D.getId()))); + var repo = new DefaultStopConsolidationRepository(); + repo.addGroups(groups); + + var service = new DefaultStopConsolidationService(repo, transitModel); + var filter = new ConsolidatedStopNameFilter(service); + + var itinerary = TestItineraryBuilder + .newItinerary(Place.forStop(STOP_C)) + .bus(TestStopConsolidationModel.ROUTE, 1, T11_05, T11_12, Place.forStop(STOP_C)) + .bus(1, T11_05, T11_12, PlanTestConstants.E) + .bus(1, T11_05, T11_12, PlanTestConstants.F) + .build(); + + var filtered = filter.filter(List.of(itinerary)); + assertFalse(filtered.isEmpty()); + + var updatedLeg = filtered.get(0).getLegs().get(0); + assertEquals(STOP_D.getName(), updatedLeg.getFrom().name); + assertEquals(STOP_D.getName(), updatedLeg.getTo().name); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationModuleTest.java b/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationModuleTest.java new file mode 100644 index 00000000000..0df495d63d0 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationModuleTest.java @@ -0,0 +1,45 @@ +package org.opentripplanner.ext.stopconsolidation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.opentripplanner.ext.stopconsolidation.TestStopConsolidationModel.PATTERN; +import static org.opentripplanner.ext.stopconsolidation.TestStopConsolidationModel.STOP_A; +import static org.opentripplanner.ext.stopconsolidation.TestStopConsolidationModel.STOP_B; +import static org.opentripplanner.ext.stopconsolidation.TestStopConsolidationModel.STOP_C; +import static org.opentripplanner.ext.stopconsolidation.TestStopConsolidationModel.STOP_D; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationRepository; +import org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup; +import org.opentripplanner.transit.model.network.TripPattern; + +class StopConsolidationModuleTest { + + @Test + void replacePatterns() { + var groups = List.of(new ConsolidatedStopGroup(STOP_D.getId(), List.of(STOP_B.getId()))); + + var transitModel = TestStopConsolidationModel.buildTransitModel(); + transitModel.addTripPattern(PATTERN.getId(), PATTERN); + var repo = new DefaultStopConsolidationRepository(); + var module = new StopConsolidationModule(transitModel, repo, groups); + module.buildGraph(); + + var modifiedPattern = transitModel.getTripPatternForId(PATTERN.getId()); + assertFalse(modifiedPattern.getRoutingTripPattern().getPattern().sameAs(PATTERN)); + assertFalse(modifiedPattern.sameAs(PATTERN)); + + var modifiedStop = modifiedPattern + .getRoutingTripPattern() + .getPattern() + .getStopPattern() + .getStop(1); + assertEquals(modifiedStop, STOP_D); + + var patterns = List.copyOf(transitModel.getAllTripPatterns()); + + var stops = patterns.stream().map(TripPattern::getStops).toList(); + assertEquals(List.of(List.of(STOP_A, STOP_D, STOP_C)), stops); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationParserTest.java b/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationParserTest.java new file mode 100644 index 00000000000..9e3e0bc0370 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationParserTest.java @@ -0,0 +1,29 @@ +package org.opentripplanner.ext.stopconsolidation; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.test.support.ResourceLoader; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +class StopConsolidationParserTest { + + @Test + void parse() { + try (var file = ResourceLoader.of(this).inputStream("consolidated-stops.csv")) { + var groups = StopConsolidationParser.parseGroups(file); + assertEquals(20, groups.size()); + + var first = groups.get(0); + assertEquals("kcm:10225", first.primary().toString()); + assertEquals( + List.of("pierce:1705867009"), + first.secondaries().stream().map(FeedScopedId::toString).toList() + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/TestStopConsolidationModel.java b/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/TestStopConsolidationModel.java new file mode 100644 index 00000000000..da0c1d6f61d --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/TestStopConsolidationModel.java @@ -0,0 +1,53 @@ +package org.opentripplanner.ext.stopconsolidation; + +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; + +import java.util.List; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.framework.Deduplicator; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.network.StopPattern; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.organization.Agency; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.service.TransitModel; + +class TestStopConsolidationModel { + + private static final TransitModelForTest testModel = TransitModelForTest.of(); + public static final RegularStop STOP_A = testModel.stop("A").withCoordinate(1, 1).build(); + public static final RegularStop STOP_B = testModel.stop("B").withCoordinate(1.1, 1.1).build(); + public static final RegularStop STOP_C = testModel.stop("C").withCoordinate(1.2, 1.2).build(); + public static final StopPattern STOP_PATTERN = TransitModelForTest.stopPattern( + STOP_A, + STOP_B, + STOP_C + ); + static final String SECONDARY_FEED_ID = "secondary"; + static final Agency AGENCY = TransitModelForTest + .agency("agency") + .copy() + .withId(new FeedScopedId(SECONDARY_FEED_ID, "agency")) + .build(); + static final Route ROUTE = TransitModelForTest + .route(new FeedScopedId(SECONDARY_FEED_ID, "route-33")) + .withAgency(AGENCY) + .build(); + static final RegularStop STOP_D = testModel + .stop("D") + .withId(new FeedScopedId(SECONDARY_FEED_ID, "secondary-stop-D")) + .build(); + + static final TripPattern PATTERN = TripPattern + .of(id("123")) + .withRoute(ROUTE) + .withStopPattern(STOP_PATTERN) + .build(); + + static TransitModel buildTransitModel() { + var stopModelBuilder = testModel.stopModelBuilder(); + List.of(STOP_A, STOP_B, STOP_C, STOP_D).forEach(stopModelBuilder::withRegularStop); + return new TransitModel(stopModelBuilder.build(), new Deduplicator()); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationRepositoryTest.java b/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationRepositoryTest.java new file mode 100644 index 00000000000..ac5eb6d66e4 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationRepositoryTest.java @@ -0,0 +1,32 @@ +package org.opentripplanner.ext.stopconsolidation.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup; + +class DefaultStopConsolidationRepositoryTest { + + private static final ConsolidatedStopGroup GROUP = new ConsolidatedStopGroup( + id("123"), + List.of(id("456")) + ); + + @Test + void add() { + var subject = new DefaultStopConsolidationRepository(); + assertEquals(List.of(), subject.groups()); + subject.addGroups(List.of(GROUP)); + + assertEquals(List.of(GROUP), subject.groups()); + } + + @Test + void groupsAreImmutable() { + var subject = new DefaultStopConsolidationRepository(); + assertThrows(Exception.class, () -> subject.groups().add(GROUP)); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationServiceTest.java b/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationServiceTest.java new file mode 100644 index 00000000000..b2fc356d111 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationServiceTest.java @@ -0,0 +1,18 @@ +package org.opentripplanner.ext.stopconsolidation.internal; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.service.TransitModel; + +class DefaultStopConsolidationServiceTest { + + @Test + void isActive() { + var service = new DefaultStopConsolidationService( + new DefaultStopConsolidationRepository(), + new TransitModel() + ); + assertFalse(service.isActive()); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/transmodelapi/mapping/TripRequestMapperTest.java b/src/ext-test/java/org/opentripplanner/ext/transmodelapi/mapping/TripRequestMapperTest.java index 35b66de7bf0..e429d59c92c 100644 --- a/src/ext-test/java/org/opentripplanner/ext/transmodelapi/mapping/TripRequestMapperTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/transmodelapi/mapping/TripRequestMapperTest.java @@ -131,6 +131,7 @@ public class TripRequestMapperTest implements PlanTestConstants { new DefaultEmissionsService(new EmissionsDataModel()), RouterConfig.DEFAULT.flexConfig(), List.of(), + null, null ), null, diff --git a/src/ext-test/resources/org/opentripplanner/ext/stopconsolidation/consolidated-stops.csv b/src/ext-test/resources/org/opentripplanner/ext/stopconsolidation/consolidated-stops.csv new file mode 100644 index 00000000000..233bcf94e7f --- /dev/null +++ b/src/ext-test/resources/org/opentripplanner/ext/stopconsolidation/consolidated-stops.csv @@ -0,0 +1,45 @@ +stop_group_id,feed_id,stop_id,is_primary +1,pierce,1705867009,0 +1,kcm,10225,1 +2,pierce,1569,0 +2,commtrans,473,0 +2,kcm,1040,1 +3,pierce,7157,0 +3,kcm,10911,1 +4,pierce,8783,0 +4,kcm,10914,1 +5,commtrans,40,0 +5,kcm,16140,1 +6,commtrans,41,0 +6,kcm,16770,1 +7,commtrans,546,0 +7,kcm,1710,1 +8,kcm,1907,0 +8,kcm,1889,1 +9,pierce,27940,0 +9,kcm,21280,1 +10,pierce,28354,0 +10,kcm,22256,1 +11,pierce,15742,0 +11,commtrans,1079,0 +11,kcm,280,1 +12,pierce,4705,0 +12,kcm,29240,1 +13,pierce,19473,0 +13,kcm,29247,1 +14,pierce,23561,0 +14,kcm,29865,1 +15,pierce,52843,0 +15,kcm,300,1 +16,pierce,33993,0 +16,kcm,31731,1 +17,pierce,3886,0 +17,commtrans,2909,0 +17,kcm,320,1 +18,pierce,20510,0 +18,kcm,340,1 +19,pierce,223,0 +19,commtrans,2631,0 +19,kcm,360,1 +20,pierce,12914,0 +20,kcm,361,1 diff --git a/src/ext/java/org/opentripplanner/ext/fares/FaresToItineraryMapper.java b/src/ext/java/org/opentripplanner/ext/fares/FaresToItineraryMapper.java index c253b730611..a2b0984e4a3 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/FaresToItineraryMapper.java +++ b/src/ext/java/org/opentripplanner/ext/fares/FaresToItineraryMapper.java @@ -6,7 +6,6 @@ import org.opentripplanner.model.fare.ItineraryFares; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.model.plan.Leg; -import org.opentripplanner.model.plan.ScheduledTransitLeg; /** * Takes fares and applies them to the legs of an itinerary. @@ -14,7 +13,7 @@ public class FaresToItineraryMapper { public static void addFaresToLegs(ItineraryFares fares, Itinerary i) { - var itineraryInstances = fares + var itineraryFareUses = fares .getItineraryProducts() .stream() .map(fp -> { @@ -25,15 +24,12 @@ public static void addFaresToLegs(ItineraryFares fares, Itinerary i) { final Multimap legProductsFromComponents = fares.legProductsFromComponents(); - i - .getLegs() - .stream() - .filter(ScheduledTransitLeg.class::isInstance) - .forEach(l -> { - var legInstances = fares.getLegProducts().get(l); - l.setFareProducts( - ListUtils.combine(itineraryInstances, legProductsFromComponents.get(l), legInstances) - ); - }); + i.transformTransitLegs(leg -> { + var legInstances = fares.getLegProducts().get(leg); + leg.setFareProducts( + ListUtils.combine(itineraryFareUses, legProductsFromComponents.get(leg), legInstances) + ); + return leg; + }); } } diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/ConsolidatedStopNameFilter.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/ConsolidatedStopNameFilter.java new file mode 100644 index 00000000000..c28d1de91b6 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/ConsolidatedStopNameFilter.java @@ -0,0 +1,52 @@ +package org.opentripplanner.ext.stopconsolidation; + +import java.util.List; +import java.util.Objects; +import org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopLeg; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.model.plan.ScheduledTransitLeg; +import org.opentripplanner.routing.algorithm.filterchain.ItineraryListFilter; + +/** + * A decorating filter that checks if a transit leg contains any primary stops and if it does, + * then replaces it with the secondary, agency-specific stop name. This is so that the in-vehicle + * display matches what OTP returns as a board/alight stop name. + */ +public class ConsolidatedStopNameFilter implements ItineraryListFilter { + + private final StopConsolidationService service; + + public ConsolidatedStopNameFilter(StopConsolidationService service) { + this.service = Objects.requireNonNull(service); + } + + @Override + public List filter(List itineraries) { + return itineraries.stream().map(this::replacePrimaryNamesWithSecondary).toList(); + } + + /** + * If the itinerary has a from/to that is the primary stop of a {@link org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup} + * then we replace its name with the secondary name of the agency that is + * operating the route, so that the name in the result matches the name in the in-vehicle + * display. + */ + private Itinerary replacePrimaryNamesWithSecondary(Itinerary i) { + return i.transformTransitLegs(leg -> { + if (leg instanceof ScheduledTransitLeg stl && needsToRenameStops(stl)) { + var agency = leg.getAgency(); + return new ConsolidatedStopLeg( + stl, + service.agencySpecificName(stl.getFrom().stop, agency), + service.agencySpecificName(stl.getTo().stop, agency) + ); + } else { + return leg; + } + }); + } + + private boolean needsToRenameStops(ScheduledTransitLeg stl) { + return (service.isPrimaryStop(stl.getFrom().stop) || service.isPrimaryStop(stl.getTo().stop)); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationModule.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationModule.java new file mode 100644 index 00000000000..c70e91fca52 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationModule.java @@ -0,0 +1,88 @@ +package org.opentripplanner.ext.stopconsolidation; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.opentripplanner.datastore.api.DataSource; +import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationService; +import org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup; +import org.opentripplanner.ext.stopconsolidation.model.StopReplacement; +import org.opentripplanner.graph_builder.model.GraphBuilderModule; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.service.TransitModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A graph build module that takes a list of "consolidated" stops (stops from several feeds + * that represent the same stop place) and swaps the "secondary" stops in patterns with their + * "primary" equivalent. + *

+ * NOTE: This will make realtime trip updates for a modified pattern a lot harder. For Arcadis' + * initial implementation this is acceptable and will serve as encouragement for the data producers to + * produce a consolidated transit feed rather than relying on this feature. + */ +public class StopConsolidationModule implements GraphBuilderModule { + + private static final Logger LOG = LoggerFactory.getLogger(TripPattern.class); + + private final StopConsolidationRepository repository; + private final TransitModel transitModel; + private final Collection groups; + + public StopConsolidationModule( + TransitModel transitModel, + StopConsolidationRepository repository, + Collection groups + ) { + this.transitModel = Objects.requireNonNull(transitModel); + this.repository = Objects.requireNonNull(repository); + this.groups = Objects.requireNonNull(groups); + } + + @Override + public void buildGraph() { + repository.addGroups(groups); + + var service = new DefaultStopConsolidationService(repository, transitModel); + + var stopsToReplace = service.secondaryStops(); + var replacements = service.replacements(); + + transitModel + .getAllTripPatterns() + .stream() + .filter(pattern -> pattern.containsAnyStopId(stopsToReplace)) + .forEach(pattern -> { + LOG.info("Replacing stop(s) in pattern {}", pattern); + var modifiedPattern = modifyStopsInPattern(pattern, replacements); + transitModel.addTripPattern(modifiedPattern.getId(), modifiedPattern); + }); + } + + @Nonnull + private TripPattern modifyStopsInPattern( + TripPattern pattern, + List replacements + ) { + var updatedStopPattern = pattern.getStopPattern().mutate(); + replacements.forEach(r -> updatedStopPattern.replaceStop(r.secondary(), r.primary())); + return pattern.copy().withStopPattern(updatedStopPattern.build()).build(); + } + + public static StopConsolidationModule of( + TransitModel transitModel, + StopConsolidationRepository repo, + DataSource ds + ) { + LOG.info("Reading stop consolidation information from '{}'", ds); + try (var inputStream = ds.asInputStream()) { + var groups = StopConsolidationParser.parseGroups(inputStream); + return new StopConsolidationModule(transitModel, repo, groups); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationParser.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationParser.java new file mode 100644 index 00000000000..b55a84db040 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationParser.java @@ -0,0 +1,71 @@ +package org.opentripplanner.ext.stopconsolidation; + +import com.csvreader.CsvReader; +import com.google.common.collect.ImmutableListMultimap; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.lang3.BooleanUtils; +import org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class StopConsolidationParser { + + private record StopGroupEntry(String groupId, FeedScopedId stopId, boolean isPrimary) {} + + public static List parseGroups(InputStream is) { + try { + var reader = new CsvReader(is, StandardCharsets.UTF_8); + reader.setDelimiter(','); + + reader.readHeaders(); + + var entries = new ArrayList(); + while (reader.readRecord()) { + var id = reader.get("stop_group_id"); + var feedId = reader.get("feed_id"); + var stopId = reader.get("stop_id"); + var isPrimary = BooleanUtils.toBoolean(Integer.parseInt(reader.get("is_primary"))); + var entry = new StopGroupEntry(id, new FeedScopedId(feedId, stopId), isPrimary); + entries.add(entry); + } + + var groups = entries + .stream() + .collect( + ImmutableListMultimap.flatteningToImmutableListMultimap( + x -> x.groupId, + Stream::of + ) + ); + + return groups + .keys() + .stream() + .map(key -> { + var group = groups.get(key); + + var primaryId = group.stream().filter(e -> e.isPrimary).findAny().orElseThrow().stopId; + var secondaries = group + .stream() + .filter(e -> !e.isPrimary) + .map(e -> e.stopId) + .collect(Collectors.toSet()); + + return new ConsolidatedStopGroup(primaryId, secondaries); + }) + .distinct() + .toList(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationRepository.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationRepository.java new file mode 100644 index 00000000000..750e8c519e4 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationRepository.java @@ -0,0 +1,22 @@ +package org.opentripplanner.ext.stopconsolidation; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup; + +/** + * A writeable repository which contains the source data for the consolidation of stops. + * This repository is built during graph build and then serialized into the graph. + */ +public interface StopConsolidationRepository extends Serializable { + /** + * Add groups to this repository. + */ + void addGroups(Collection group); + + /** + * Returns the list of consolidated stop groups. + */ + List groups(); +} diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationService.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationService.java new file mode 100644 index 00000000000..2418d8f5625 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationService.java @@ -0,0 +1,36 @@ +package org.opentripplanner.ext.stopconsolidation; + +import java.util.List; +import org.opentripplanner.ext.stopconsolidation.model.StopReplacement; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.organization.Agency; +import org.opentripplanner.transit.model.site.StopLocation; + +public interface StopConsolidationService { + /** + * A flat list of pairs of stops that should be replaced. + */ + List replacements(); + + /** + * Returns the list of secondary stops that need to be replaced in TripPatterns with their + * primary equivalent. + */ + List secondaryStops(); + + /** + * Is the given stop a primary stop as defined by the stop consolidation configuration? + */ + boolean isPrimaryStop(StopLocation stop); + + /** + * Are any stop consolidations defined? + */ + boolean isActive(); + + /** + * For a given primary stop look up the name as it was originally defined in the agency's feed. + */ + I18NString agencySpecificName(StopLocation stop, Agency agency); +} diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/configure/StopConsolidationRepositoryModule.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/configure/StopConsolidationRepositoryModule.java new file mode 100644 index 00000000000..75cd6cb868f --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/configure/StopConsolidationRepositoryModule.java @@ -0,0 +1,18 @@ +package org.opentripplanner.ext.stopconsolidation.configure; + +import dagger.Binds; +import dagger.Module; +import javax.annotation.Nullable; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; +import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationRepository; + +/** + * The repository is used during application loading phase, so we need to provide + * a module for the repository. + */ +@Module +public interface StopConsolidationRepositoryModule { + @Binds + @Nullable + StopConsolidationRepository bindRepository(@Nullable DefaultStopConsolidationRepository repo); +} diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/configure/StopConsolidationServiceModule.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/configure/StopConsolidationServiceModule.java new file mode 100644 index 00000000000..3851435641c --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/configure/StopConsolidationServiceModule.java @@ -0,0 +1,25 @@ +package org.opentripplanner.ext.stopconsolidation.configure; + +import dagger.Module; +import dagger.Provides; +import jakarta.inject.Singleton; +import javax.annotation.Nullable; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; +import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationService; +import org.opentripplanner.transit.service.TransitModel; + +@Module +public class StopConsolidationServiceModule { + + @Provides + @Singleton + @Nullable + StopConsolidationService service(@Nullable StopConsolidationRepository repo, TransitModel tm) { + if (repo == null) { + return null; + } else { + return new DefaultStopConsolidationService(repo, tm); + } + } +} diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationRepository.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationRepository.java new file mode 100644 index 00000000000..4c9552b9a1e --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationRepository.java @@ -0,0 +1,28 @@ +package org.opentripplanner.ext.stopconsolidation.internal; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; +import org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup; + +@Singleton +public class DefaultStopConsolidationRepository + implements Serializable, StopConsolidationRepository { + + private final List groups = new ArrayList<>(); + + @Inject + public DefaultStopConsolidationRepository() {} + + public void addGroups(Collection group) { + groups.addAll(group); + } + + public List groups() { + return List.copyOf(groups); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java new file mode 100644 index 00000000000..54bd960d078 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java @@ -0,0 +1,86 @@ +package org.opentripplanner.ext.stopconsolidation.internal; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; +import org.opentripplanner.ext.stopconsolidation.model.StopReplacement; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.organization.Agency; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.service.TransitModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DefaultStopConsolidationService implements StopConsolidationService { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultStopConsolidationService.class); + + private final StopConsolidationRepository repo; + private final TransitModel transitModel; + + public DefaultStopConsolidationService( + StopConsolidationRepository repo, + TransitModel transitModel + ) { + this.repo = Objects.requireNonNull(repo); + this.transitModel = Objects.requireNonNull(transitModel); + } + + @Override + public List replacements() { + return repo + .groups() + .stream() + .flatMap(group -> { + var primaryStop = transitModel.getStopModel().getRegularStop(group.primary()); + if (primaryStop == null) { + LOG.error( + "Could not find primary stop with id {}. Ignoring stop group {}.", + group.primary(), + group + ); + return Stream.empty(); + } else { + return group.secondaries().stream().map(r -> new StopReplacement(primaryStop, r)); + } + }) + .toList(); + } + + @Override + public List secondaryStops() { + return replacements().stream().map(StopReplacement::secondary).toList(); + } + + @Override + public boolean isPrimaryStop(StopLocation stop) { + return repo.groups().stream().anyMatch(r -> r.primary().equals(stop.getId())); + } + + @Override + public boolean isActive() { + return !repo.groups().isEmpty(); + } + + @Override + public I18NString agencySpecificName(StopLocation stop, Agency agency) { + if (agency.getId().getFeedId().equals(stop.getId().getFeedId())) { + return stop.getName(); + } else { + return repo + .groups() + .stream() + .filter(r -> r.primary().equals(stop.getId())) + .flatMap(g -> g.secondaries().stream()) + .filter(secondary -> secondary.getFeedId().equals(agency.getId().getFeedId())) + .findAny() + .map(id -> transitModel.getStopModel().getRegularStop(id)) + .map(RegularStop::getName) + .orElseGet(stop::getName); + } + } +} diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopGroup.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopGroup.java new file mode 100644 index 00000000000..71a98342f39 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopGroup.java @@ -0,0 +1,6 @@ +package org.opentripplanner.ext.stopconsolidation.model; + +import java.util.Collection; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +public record ConsolidatedStopGroup(FeedScopedId primary, Collection secondaries) {} diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopLeg.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopLeg.java new file mode 100644 index 00000000000..e201c7ed805 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopLeg.java @@ -0,0 +1,28 @@ +package org.opentripplanner.ext.stopconsolidation.model; + +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.model.plan.Place; +import org.opentripplanner.model.plan.ScheduledTransitLeg; +import org.opentripplanner.model.plan.ScheduledTransitLegBuilder; + +public class ConsolidatedStopLeg extends ScheduledTransitLeg { + + private final I18NString fromName; + private final I18NString toName; + + public ConsolidatedStopLeg(ScheduledTransitLeg original, I18NString fromName, I18NString toName) { + super(new ScheduledTransitLegBuilder<>(original)); + this.fromName = fromName; + this.toName = toName; + } + + @Override + public Place getFrom() { + return Place.forStop(super.getFrom().stop, fromName); + } + + @Override + public Place getTo() { + return Place.forStop(super.getTo().stop, toName); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/model/StopReplacement.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/model/StopReplacement.java new file mode 100644 index 00000000000..2a781f3e866 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/model/StopReplacement.java @@ -0,0 +1,6 @@ +package org.opentripplanner.ext.stopconsolidation.model; + +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.site.StopLocation; + +public record StopReplacement(StopLocation primary, FeedScopedId secondary) {} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PatternImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PatternImpl.java index 35777dc9836..18ab5c5d5e9 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PatternImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PatternImpl.java @@ -241,11 +241,7 @@ private Route getRoute(DataFetchingEnvironment environment) { } private List getStops(DataFetchingEnvironment environment) { - return getSource(environment) - .getStops() - .stream() - .map(Object.class::cast) - .collect(Collectors.toList()); + return getSource(environment).getStops().stream().map(Object.class::cast).toList(); } private List getTrips(DataFetchingEnvironment environment) { diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java index 750cb0a4929..1c2a7b7c72a 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java @@ -126,7 +126,7 @@ public DataFetcher> bikeParks() { .getContext() .vehicleParkingService(); - return vehicleParkingService.getBikeParks().collect(Collectors.toList()); + return vehicleParkingService.getBikeParks().toList(); }; } @@ -176,7 +176,7 @@ public DataFetcher> bikeRentalStations() { .getGraphQLIds() .stream() .flatMap(id -> vehicleRentalStations.get(id).stream()) - .collect(Collectors.toList()); + .toList(); } return vehicleRentalStationService.getVehicleRentalPlaces(); @@ -223,11 +223,11 @@ public DataFetcher> carParks() { .getCarParks() .collect(Collectors.toMap(station -> station.getId().getId(), station -> station)); - return idList.stream().map(carParkMap::get).collect(Collectors.toList()); + return idList.stream().map(carParkMap::get).toList(); } } - return vehicleParkingService.getCarParks().collect(Collectors.toList()); + return vehicleParkingService.getCarParks().toList(); }; } @@ -291,19 +291,11 @@ public DataFetcher> nearest() { if (filterByIds != null) { filterByStops = filterByIds.getGraphQLStops() != null - ? filterByIds - .getGraphQLStops() - .stream() - .map(FeedScopedId::parse) - .collect(Collectors.toList()) + ? filterByIds.getGraphQLStops().stream().map(FeedScopedId::parse).toList() : null; filterByRoutes = filterByIds.getGraphQLRoutes() != null - ? filterByIds - .getGraphQLRoutes() - .stream() - .map(FeedScopedId::parse) - .collect(Collectors.toList()) + ? filterByIds.getGraphQLRoutes().stream().map(FeedScopedId::parse).toList() : null; filterByBikeRentalStations = filterByIds.getGraphQLBikeRentalStations(); filterByBikeParks = filterByIds.getGraphQLBikeParks(); @@ -322,7 +314,7 @@ public DataFetcher> nearest() { } }) .filter(Objects::nonNull) - .collect(Collectors.toList()) + .toList() : null; List filterByPlaceTypes = args.getGraphQLFilterByPlaceTypes() != null ? args.getGraphQLFilterByPlaceTypes().stream().map(GraphQLUtils::toModel).toList() @@ -360,7 +352,7 @@ public DataFetcher node() { var args = new GraphQLTypes.GraphQLQueryTypeNodeArgs(environment.getArguments()); String type = args.getGraphQLId().getType(); String id = args.getGraphQLId().getId(); - final GraphQLRequestContext context = environment.getContext(); + final GraphQLRequestContext context = environment.getContext(); TransitService transitService = context.transitService(); VehicleParkingService vehicleParkingService = context.vehicleParkingService(); VehicleRentalService vehicleRentalStationService = context.vehicleRentalService(); @@ -531,7 +523,7 @@ public DataFetcher> rentalVehicles() { .getGraphQLIds() .stream() .flatMap(id -> vehicleRentalVehicles.get(id).stream()) - .collect(Collectors.toList()); + .toList(); } var formFactorArgs = args.getGraphQLFormFactors(); @@ -574,7 +566,7 @@ public DataFetcher> routes() { .stream() .map(FeedScopedId::parse) .map(transitService::getRouteForId) - .collect(Collectors.toList()); + .toList(); } Stream routeStream = transitService.getAllRoutes().stream(); @@ -589,7 +581,7 @@ public DataFetcher> routes() { .getGraphQLTransportModes() .stream() .map(mode -> TransitMode.valueOf(mode.name())) - .collect(Collectors.toList()); + .toList(); routeStream = routeStream.filter(route -> modes.contains(route.getMode())); } @@ -601,7 +593,7 @@ public DataFetcher> routes() { GraphQLUtils.startsWith(route.getLongName(), name, environment.getLocale()) ); } - return routeStream.collect(Collectors.toList()); + return routeStream.toList(); }; } @@ -784,7 +776,7 @@ public DataFetcher> trips() { tripStream = tripStream.filter(trip -> feeds.contains(trip.getId().getFeedId())); } - return tripStream.collect(Collectors.toList()); + return tripStream.toList(); }; } @@ -823,11 +815,11 @@ public DataFetcher> vehicleParkings() { .getVehicleParkings() .collect(Collectors.toMap(station -> station.getId().toString(), station -> station)); - return idList.stream().map(vehicleParkingMap::get).collect(Collectors.toList()); + return idList.stream().map(vehicleParkingMap::get).toList(); } } - return vehicleParkingService.getVehicleParkings().collect(Collectors.toList()); + return vehicleParkingService.getVehicleParkings().toList(); }; } @@ -884,7 +876,7 @@ public DataFetcher> vehicleRentalStations() { .getGraphQLIds() .stream() .flatMap(id -> vehicleRentalStations.get(id).stream()) - .collect(Collectors.toList()); + .toList(); } return vehicleRentalStationService.getVehicleRentalStations(); @@ -965,6 +957,6 @@ protected static List filterAlerts( .map(EntitySelector.Stop.class::cast) .anyMatch(stop -> args.getGraphQLStop().contains(stop.stopId().toString())) ) - .collect(Collectors.toList()); + .toList(); } } diff --git a/src/main/java/org/opentripplanner/framework/geometry/GeometryUtils.java b/src/main/java/org/opentripplanner/framework/geometry/GeometryUtils.java index 7e9335b1ad8..cf634ad0e23 100644 --- a/src/main/java/org/opentripplanner/framework/geometry/GeometryUtils.java +++ b/src/main/java/org/opentripplanner/framework/geometry/GeometryUtils.java @@ -81,11 +81,15 @@ public static LineString makeLineString(List coordinates) { return factory.createLineString(coordinates.toArray(new Coordinate[] {})); } - public static LineString makeLineString(Coordinate[] coordinates) { + public static LineString makeLineString(Coordinate... coordinates) { GeometryFactory factory = getGeometryFactory(); return factory.createLineString(coordinates); } + public static LineString makeLineString(WgsCoordinate... coordinates) { + return makeLineString(Arrays.stream(coordinates).map(WgsCoordinate::asJtsCoordinate).toList()); + } + public static LineString concatenateLineStrings( List inputObjects, Function mapper diff --git a/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java b/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java index 662846b3952..e6aba29278b 100644 --- a/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java +++ b/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java @@ -11,6 +11,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.opentripplanner.ext.emissions.EmissionsDataModel; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.application.OtpAppException; import org.opentripplanner.framework.lang.OtpNumberFormat; @@ -63,10 +64,10 @@ public static GraphBuilder create( TransitModel transitModel, WorldEnvelopeRepository worldEnvelopeRepository, @Nullable EmissionsDataModel emissionsDataModel, + @Nullable StopConsolidationRepository stopConsolidationRepository, boolean loadStreetGraph, boolean saveStreetGraph ) { - //DaggerGraphBuilderFactory appFactory = GraphBuilderFactoryDa boolean hasOsm = dataSources.has(OSM); boolean hasGtfs = dataSources.has(GTFS); boolean hasNetex = dataSources.has(NETEX); @@ -80,6 +81,7 @@ public static GraphBuilder create( .graph(graph) .transitModel(transitModel) .worldEnvelopeRepository(worldEnvelopeRepository) + .stopConsolidationRepository(stopConsolidationRepository) .dataSources(dataSources) .timeZoneId(transitModel.getTimeZone()); @@ -105,6 +107,11 @@ public static GraphBuilder create( graphBuilder.addModule(factory.netexModule()); } + // Consolidate stops only if a stop consolidation repo has been provided + if (hasTransitData && factory.stopConsolidationModule() != null) { + graphBuilder.addModule(factory.stopConsolidationModule()); + } + if (hasTransitData) { graphBuilder.addModule(factory.tripPatternNamer()); } diff --git a/src/main/java/org/opentripplanner/graph_builder/GraphBuilderDataSources.java b/src/main/java/org/opentripplanner/graph_builder/GraphBuilderDataSources.java index 310b3303aaf..eaa61fea139 100644 --- a/src/main/java/org/opentripplanner/graph_builder/GraphBuilderDataSources.java +++ b/src/main/java/org/opentripplanner/graph_builder/GraphBuilderDataSources.java @@ -12,12 +12,14 @@ import java.io.File; import java.net.URI; import java.util.EnumSet; +import java.util.Optional; import java.util.Set; import org.opentripplanner.datastore.OtpDataStore; import org.opentripplanner.datastore.api.CompositeDataSource; import org.opentripplanner.datastore.api.DataSource; import org.opentripplanner.datastore.api.FileType; import org.opentripplanner.datastore.api.OtpBaseDirectory; +import org.opentripplanner.datastore.file.FileDataSource; import org.opentripplanner.framework.application.OtpAppException; import org.opentripplanner.graph_builder.module.ned.parameter.DemExtractParameters; import org.opentripplanner.graph_builder.module.ned.parameter.DemExtractParametersBuilder; @@ -181,6 +183,18 @@ public NetexFeedParameters getNetexConfig(DataSource dataSource) { .orElse(buildConfig.netexDefaults.copyOf().withSource(dataSource.uri()).build()); } + /** + * Returns the optional data source for the stop consolidation configuration. + */ + public Optional stopConsolidationDataSource() { + return Optional + .ofNullable(buildConfig.stopConsolidationFile) + .map(fileName -> { + var f = baseDirectory.toPath().resolve(fileName).toFile(); + return new FileDataSource(f, FileType.CONFIG); + }); + } + /** * Match the URI provided in the configuration with the URI of a datasource, * either by comparing directly the two URIs or by first prepending the OTP base directory diff --git a/src/main/java/org/opentripplanner/graph_builder/issue/api/DataImportIssueSummary.java b/src/main/java/org/opentripplanner/graph_builder/issue/api/DataImportIssueSummary.java index cf5c3a51822..ba1e6610a30 100644 --- a/src/main/java/org/opentripplanner/graph_builder/issue/api/DataImportIssueSummary.java +++ b/src/main/java/org/opentripplanner/graph_builder/issue/api/DataImportIssueSummary.java @@ -19,7 +19,7 @@ public class DataImportIssueSummary implements Serializable { private static final Logger ISSUE_LOG = LoggerFactory.getLogger(ISSUES_LOG_NAME); - private Map summary; + private final Map summary; public DataImportIssueSummary(List issues) { this( diff --git a/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderFactory.java b/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderFactory.java index 0b2a46e165e..1f50c7a6327 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderFactory.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderFactory.java @@ -10,6 +10,8 @@ import org.opentripplanner.ext.emissions.EmissionsDataModel; import org.opentripplanner.ext.emissions.EmissionsModule; import org.opentripplanner.ext.flex.AreaStopsToVerticesMapper; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationModule; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; import org.opentripplanner.ext.transferanalyzer.DirectTransferAnalyzer; import org.opentripplanner.graph_builder.GraphBuilder; import org.opentripplanner.graph_builder.GraphBuilderDataSources; @@ -56,6 +58,12 @@ public interface GraphBuilderFactory { DataImportIssueReporter dataImportIssueReporter(); CalculateWorldEnvelopeModule calculateWorldEnvelopeModule(); + @Nullable + StopConsolidationModule stopConsolidationModule(); + + @Nullable + StopConsolidationRepository stopConsolidationRepository(); + @Component.Builder interface Builder { @BindsInstance @@ -70,6 +78,11 @@ interface Builder { @BindsInstance Builder worldEnvelopeRepository(WorldEnvelopeRepository worldEnvelopeRepository); + @BindsInstance + Builder stopConsolidationRepository( + @Nullable StopConsolidationRepository stopConsolidationRepository + ); + @BindsInstance Builder dataSources(GraphBuilderDataSources graphBuilderDataSources); diff --git a/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java b/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java index 755b7725941..ba2bfde4631 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java @@ -14,6 +14,8 @@ import org.opentripplanner.ext.dataoverlay.configure.DataOverlayFactory; import org.opentripplanner.ext.emissions.EmissionsDataModel; import org.opentripplanner.ext.emissions.EmissionsModule; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationModule; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; import org.opentripplanner.ext.transferanalyzer.DirectTransferAnalyzer; import org.opentripplanner.graph_builder.ConfiguredDataSource; import org.opentripplanner.graph_builder.GraphBuilderDataSources; @@ -272,10 +274,25 @@ static DataImportIssueReporter provideDataImportIssuesToHTML( } @Provides + @Singleton static DataImportIssueSummary providesDataImportIssueSummary(DataImportIssueStore issueStore) { return new DataImportIssueSummary(issueStore.listIssues()); } + @Provides + @Singleton + @Nullable + static StopConsolidationModule providesStopConsolidationModule( + TransitModel transitModel, + @Nullable StopConsolidationRepository repo, + GraphBuilderDataSources dataSources + ) { + return dataSources + .stopConsolidationDataSource() + .map(ds -> StopConsolidationModule.of(transitModel, repo, ds)) + .orElse(null); + } + /* private methods */ private static ElevationGridCoverageFactory createNedElevationFactory( diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java index 6c52e780211..df40e2cb647 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java @@ -748,7 +748,7 @@ private void processMultipolygonRelations() { } else if (member.hasRoleOuter()) { outerWays.add(way); } else { - LOG.warn("Unexpected role {} in multipolygon", member.getRole()); + LOG.warn("Unexpected role '{}' in multipolygon", member.getRole()); } } processedAreas.add(relation); @@ -1044,8 +1044,8 @@ private void processRoute(OSMRelation relation) { *

* This goes through all public_transport=stop_area relations and adds the parent (either an area * or multipolygon relation) as the key and a Set of transit stop nodes that should be included in - * the parent area as the value into stopsInAreas. This improves TransitToTaggedStopsGraphBuilder - * by enabling us to have unconnected stop nodes within the areas by creating relations . + * the parent area as the value into stopsInAreas. This improves {@link org.opentripplanner.graph_builder.module.OsmBoardingLocationsModule} + * by enabling us to have unconnected stop nodes within the areas by creating relations. * * @author hannesj * @see "http://wiki.openstreetmap.org/wiki/Tag:public_transport%3Dstop_area" diff --git a/src/main/java/org/opentripplanner/gtfs/GenerateTripPatternsOperation.java b/src/main/java/org/opentripplanner/gtfs/GenerateTripPatternsOperation.java index 979876cf523..5e09915b43a 100644 --- a/src/main/java/org/opentripplanner/gtfs/GenerateTripPatternsOperation.java +++ b/src/main/java/org/opentripplanner/gtfs/GenerateTripPatternsOperation.java @@ -41,7 +41,7 @@ public class GenerateTripPatternsOperation { private final DataImportIssueStore issueStore; private final Deduplicator deduplicator; private final Set calendarServiceIds; - private GeometryProcessor geometryProcessor; + private final GeometryProcessor geometryProcessor; private final Multimap tripPatterns; private final ListMultimap frequenciesForTrip = ArrayListMultimap.create(); diff --git a/src/main/java/org/opentripplanner/gtfs/graphbuilder/GtfsModule.java b/src/main/java/org/opentripplanner/gtfs/graphbuilder/GtfsModule.java index 59cb4fc3b09..a66b3f659bb 100644 --- a/src/main/java/org/opentripplanner/gtfs/graphbuilder/GtfsModule.java +++ b/src/main/java/org/opentripplanner/gtfs/graphbuilder/GtfsModule.java @@ -39,8 +39,8 @@ import org.opentripplanner.graph_builder.module.GtfsFeedId; import org.opentripplanner.graph_builder.module.ValidateAndInterpolateStopTimesForEachTrip; import org.opentripplanner.graph_builder.module.geometry.GeometryProcessor; -import org.opentripplanner.graph_builder.module.interlining.InterlineProcessor; import org.opentripplanner.gtfs.GenerateTripPatternsOperation; +import org.opentripplanner.gtfs.interlining.InterlineProcessor; import org.opentripplanner.gtfs.mapping.GTFSToOtpTransitServiceMapper; import org.opentripplanner.model.OtpTransitService; import org.opentripplanner.model.TripStopTimes; diff --git a/src/main/java/org/opentripplanner/graph_builder/module/interlining/InterlineProcessor.java b/src/main/java/org/opentripplanner/gtfs/interlining/InterlineProcessor.java similarity index 99% rename from src/main/java/org/opentripplanner/graph_builder/module/interlining/InterlineProcessor.java rename to src/main/java/org/opentripplanner/gtfs/interlining/InterlineProcessor.java index 25aaa8413b6..4247af4117e 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/interlining/InterlineProcessor.java +++ b/src/main/java/org/opentripplanner/gtfs/interlining/InterlineProcessor.java @@ -1,4 +1,4 @@ -package org.opentripplanner.graph_builder.module.interlining; +package org.opentripplanner.gtfs.interlining; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; diff --git a/src/main/java/org/opentripplanner/model/plan/Itinerary.java b/src/main/java/org/opentripplanner/model/plan/Itinerary.java index 4fa4707f590..27dae9d4dba 100644 --- a/src/main/java/org/opentripplanner/model/plan/Itinerary.java +++ b/src/main/java/org/opentripplanner/model/plan/Itinerary.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -372,6 +373,27 @@ public List getLegs() { return legs; } + /** + * Applies the transformation in {@code mapper} to all instances of {@link TransitLeg} in the + * legs of this Itinerary. + *

+ * NOTE: The itinerary is mutable so the transformation is done in-place! + */ + public Itinerary transformTransitLegs(Function mapper) { + legs = + legs + .stream() + .map(l -> { + if (l instanceof TransitLeg tl) { + return mapper.apply(tl); + } else { + return l; + } + }) + .toList(); + return this; + } + public Stream getStreetLegs() { return legs.stream().filter(StreetLeg.class::isInstance).map(StreetLeg.class::cast); } diff --git a/src/main/java/org/opentripplanner/model/plan/Place.java b/src/main/java/org/opentripplanner/model/plan/Place.java index f20bb2a0cb9..6f39dcad3d1 100644 --- a/src/main/java/org/opentripplanner/model/plan/Place.java +++ b/src/main/java/org/opentripplanner/model/plan/Place.java @@ -90,7 +90,11 @@ public static Place normal(Vertex vertex, I18NString name) { } public static Place forStop(StopLocation stop) { - return new Place(stop.getName(), stop.getCoordinate(), VertexType.TRANSIT, stop, null, null); + return forStop(stop, stop.getName()); + } + + public static Place forStop(StopLocation stop, I18NString nameOverride) { + return new Place(nameOverride, stop.getCoordinate(), VertexType.TRANSIT, stop, null, null); } public static Place forFlexStop(StopLocation stop, Vertex vertex) { diff --git a/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java index 1c535f631b9..4a67b780835 100644 --- a/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java +++ b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java @@ -64,7 +64,7 @@ public class ScheduledTransitLeg implements TransitLeg { private final Float accessibilityScore; private List fareProducts = List.of(); - ScheduledTransitLeg(ScheduledTransitLegBuilder builder) { + protected ScheduledTransitLeg(ScheduledTransitLegBuilder builder) { this.tripTimes = builder.tripTimes(); this.tripPattern = builder.tripPattern(); diff --git a/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java b/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java index 04815d8820b..6e596e52076 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java +++ b/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java @@ -444,7 +444,7 @@ public boolean isParkAndRide() { * This intentionally excludes railway=stop and public_transport=stop because these are supposed * to be placed on the tracks not on the platform. * - * @return whether the node is a transit stop + * @return whether the node is a place used to board a public transport vehicle */ public boolean isBoardingLocation() { return ( @@ -588,6 +588,7 @@ private boolean isTagDeniedAccess(String tagName) { * Returns level tag (i.e. building floor) or layer tag values, defaults to "0" * Some entities can have a semicolon separated list of levels (e.g. elevators) */ + @Nonnull public Set getLevels() { var levels = getMultiTagValues(LEVEL_TAGS); if (levels.isEmpty()) { diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java index 42f911937c7..f3179eaa8b7 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java @@ -12,7 +12,6 @@ import java.util.function.Function; import javax.annotation.Nullable; import org.opentripplanner.ext.accessibilityscore.AccessibilityScoreFilter; -import org.opentripplanner.ext.fares.FaresFilter; import org.opentripplanner.framework.lang.Sandbox; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.model.plan.SortOrder; @@ -42,7 +41,6 @@ import org.opentripplanner.routing.algorithm.filterchain.groupids.GroupBySameRoutesAndStops; import org.opentripplanner.routing.api.request.framework.CostLinearFunction; import org.opentripplanner.routing.api.request.preference.ItineraryFilterDebugProfile; -import org.opentripplanner.routing.fares.FareService; import org.opentripplanner.routing.services.TransitAlertService; import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.transit.model.site.MultiModalStation; @@ -74,19 +72,28 @@ public class ItineraryListFilterChainBuilder { private Duration searchWindow = null; private boolean accessibilityScore; private double wheelchairMaxSlope; - private FareService faresService; private TransitAlertService transitAlertService; private Function getMultiModalStation; private boolean removeItinerariesWithSameRoutesAndStops; private double minBikeParkingDistance; private boolean removeTransitIfWalkingIsBetter = true; + /** + * Sandbox filters which decorate the itineraries with extra information. + */ + @Sandbox private ItineraryListFilter emissionsFilter; + @Sandbox + private ItineraryListFilter faresFilter; + @Sandbox private ItineraryListFilter rideHailingFilter; + @Sandbox + private ItineraryListFilter stopConsolidationFilter; + public ItineraryListFilterChainBuilder(SortOrder sortOrder) { this.sortOrder = sortOrder; } @@ -292,8 +299,8 @@ public ItineraryListFilterChainBuilder withAccessibilityScore( return this; } - public ItineraryListFilterChainBuilder withFares(FareService fareService) { - this.faresService = fareService; + public ItineraryListFilterChainBuilder withFaresFilter(ItineraryListFilter filter) { + this.faresFilter = filter; return this; } @@ -319,6 +326,13 @@ public ItineraryListFilterChainBuilder withRideHailingFilter(ItineraryListFilter return this; } + public ItineraryListFilterChainBuilder withStopConsolidationFilter( + @Nullable ItineraryListFilter filter + ) { + this.stopConsolidationFilter = filter; + return this; + } + @SuppressWarnings("CollectionAddAllCanBeReplacedWithConstructor") public ItineraryListFilterChain build() { List filters = new ArrayList<>(); @@ -344,8 +358,8 @@ public ItineraryListFilterChain build() { filters.add(new AccessibilityScoreFilter(wheelchairMaxSlope)); } - if (faresService != null) { - filters.add(new FaresFilter(faresService)); + if (faresFilter != null) { + filters.add(faresFilter); } if (this.emissionsFilter != null) { @@ -448,10 +462,20 @@ public ItineraryListFilterChain build() { // Do the final itineraries sort filters.add(new SortingFilter(SortOrderComparator.comparator(sortOrder))); + // Sandbox filters to decorate itineraries + + if (faresFilter != null) { + filters.add(faresFilter); + } + if (rideHailingFilter != null) { filters.add(rideHailingFilter); } + if (stopConsolidationFilter != null) { + filters.add(stopConsolidationFilter); + } + var debugHandler = new DeleteResultHandler(debug, maxNumberOfItineraries); return new ItineraryListFilterChain(filters, debugHandler); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java index cc24652f6f2..430155969a6 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java @@ -4,9 +4,9 @@ import java.time.Instant; import java.util.List; import java.util.function.Consumer; -import org.opentripplanner.ext.emissions.EmissionsFilter; +import org.opentripplanner.ext.fares.FaresFilter; import org.opentripplanner.ext.ridehailing.RideHailingFilter; -import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.ext.stopconsolidation.ConsolidatedStopNameFilter; import org.opentripplanner.routing.algorithm.filterchain.GroupBySimilarity; import org.opentripplanner.routing.algorithm.filterchain.ItineraryListFilterChain; import org.opentripplanner.routing.algorithm.filterchain.ItineraryListFilterChainBuilder; @@ -77,7 +77,6 @@ public static ItineraryListFilterChain createFilterChain( params.useAccessibilityScore() && request.wheelchair(), request.preferences().wheelchair().maxSlope() ) - .withFares(context.graph().getFareService()) .withMinBikeParkingDistance(minBikeParkingDistance(request)) .withRemoveTimeshiftedItinerariesWithSameRoutesAndStops( params.removeItinerariesWithSameRoutesAndStops() @@ -92,14 +91,21 @@ public static ItineraryListFilterChain createFilterChain( .withRemoveTransitIfWalkingIsBetter(true) .withDebugEnabled(params.debug()); + var fareService = context.graph().getFareService(); + if (fareService != null) { + builder.withFaresFilter(new FaresFilter(fareService)); + } + if (!context.rideHailingServices().isEmpty()) { builder.withRideHailingFilter( new RideHailingFilter(context.rideHailingServices(), request.wheelchair()) ); } - if (OTPFeature.Co2Emissions.isOn() && context.emissionsService() != null) { - builder.withEmissions(new EmissionsFilter(context.emissionsService())); + if (context.stopConsolidationService() != null) { + builder.withStopConsolidationFilter( + new ConsolidatedStopNameFilter(context.stopConsolidationService()) + ); } return builder.build(); diff --git a/src/main/java/org/opentripplanner/routing/graph/SerializedGraphObject.java b/src/main/java/org/opentripplanner/routing/graph/SerializedGraphObject.java index fbcc77f3a5b..690c4c7e821 100644 --- a/src/main/java/org/opentripplanner/routing/graph/SerializedGraphObject.java +++ b/src/main/java/org/opentripplanner/routing/graph/SerializedGraphObject.java @@ -18,6 +18,7 @@ import javax.annotation.Nullable; import org.opentripplanner.datastore.api.DataSource; import org.opentripplanner.ext.emissions.EmissionsDataModel; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; import org.opentripplanner.framework.application.OtpAppException; import org.opentripplanner.framework.geometry.CompactElevationProfile; import org.opentripplanner.framework.lang.OtpNumberFormat; @@ -73,6 +74,7 @@ public class SerializedGraphObject implements Serializable { private final List allTransitSubModes; public final DataImportIssueSummary issueSummary; + public final StopConsolidationRepository stopConsolidationRepository; private final int routingTripPatternCounter; public final EmissionsDataModel emissionsDataModel; @@ -83,7 +85,8 @@ public SerializedGraphObject( BuildConfig buildConfig, RouterConfig routerConfig, DataImportIssueSummary issueSummary, - EmissionsDataModel emissionsDataModel + EmissionsDataModel emissionsDataModel, + StopConsolidationRepository stopConsolidationRepository ) { this.graph = graph; this.edges = graph.getEdges(); @@ -95,6 +98,7 @@ public SerializedGraphObject( this.emissionsDataModel = emissionsDataModel; this.allTransitSubModes = SubMode.listAllCachedSubModes(); this.routingTripPatternCounter = RoutingTripPattern.indexCounter(); + this.stopConsolidationRepository = stopConsolidationRepository; } public static void verifyTheOutputGraphIsWritableIfDataSourceExist(DataSource graphOutput) { diff --git a/src/main/java/org/opentripplanner/standalone/OTPMain.java b/src/main/java/org/opentripplanner/standalone/OTPMain.java index 65b40dc3630..f5871dde995 100644 --- a/src/main/java/org/opentripplanner/standalone/OTPMain.java +++ b/src/main/java/org/opentripplanner/standalone/OTPMain.java @@ -152,7 +152,8 @@ private static void startOTPServer(CommandLineParameters cli) { config.buildConfig(), config.routerConfig(), DataImportIssueSummary.combine(graphBuilder.issueSummary(), app.dataImportIssueSummary()), - app.emissionsDataModel() + app.emissionsDataModel(), + app.stopConsolidationRepository() ) .save(app.graphOutputDataSource()); // Log size info for the deduplicator diff --git a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index db105338736..8fa8069ba70 100644 --- a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -8,6 +8,7 @@ import org.opentripplanner.ext.dataoverlay.routing.DataOverlayContext; import org.opentripplanner.ext.emissions.EmissionsService; import org.opentripplanner.ext.ridehailing.RideHailingService; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; import org.opentripplanner.ext.vectortiles.VectorTilesResource; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.inspector.raster.TileRendererManager; @@ -94,6 +95,9 @@ public interface OtpServerRequestContext { List rideHailingServices(); + @Nullable + StopConsolidationService stopConsolidationService(); + MeterRegistry meterRegistry(); @Nullable diff --git a/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java b/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java index 060d224b487..4a56a0722e4 100644 --- a/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java @@ -5,6 +5,7 @@ import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_0; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_1; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_2; +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_5; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.MissingNode; @@ -167,21 +168,23 @@ public class BuildConfig implements OtpDataStoreConfig { public final OsmExtractParametersList osm; public final EmissionsConfig emissions; public final TransitFeeds transitFeeds; - public boolean staticParkAndRide; - public boolean staticBikeParkAndRide; - public double distanceBetweenElevationSamples; - public double maxElevationPropagationMeters; - public boolean readCachedElevations; - public boolean writeCachedElevations; + public final boolean staticParkAndRide; + public final boolean staticBikeParkAndRide; + public final double distanceBetweenElevationSamples; + public final double maxElevationPropagationMeters; + public final boolean readCachedElevations; + public final boolean writeCachedElevations; - public boolean includeEllipsoidToGeoidDifference; + public final boolean includeEllipsoidToGeoidDifference; - public boolean multiThreadElevationCalculations; + public final boolean multiThreadElevationCalculations; - public LocalDate transitServiceStart; + public final LocalDate transitServiceStart; - public LocalDate transitServiceEnd; - public ZoneId transitModelTimeZone; + public final LocalDate transitServiceEnd; + public final ZoneId transitModelTimeZone; + + public final String stopConsolidationFile; /** * Set all parameters from the given Jackson JSON tree, applying defaults. Supplying @@ -609,6 +612,15 @@ that we support remote input files (cloud storage or arbitrary URLs) not all dat ) .asUri(null); + stopConsolidationFile = + root + .of("stopConsolidationFile") + .since(V2_5) + .summary( + "Name of the CSV-formatted file in the build directory which contains the configuration for stop consolidation." + ) + .asString(null); + osmDefaults = OsmConfig.mapOsmDefaults(root, "osmDefaults"); osm = OsmConfig.mapOsmConfig(root, "osm", osmDefaults); demDefaults = DemConfig.mapDemDefaultsConfig(root, "demDefaults"); diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java index faf1952c10d..3191a67aacd 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java @@ -5,6 +5,7 @@ import org.opentripplanner.datastore.api.DataSource; import org.opentripplanner.ext.emissions.EmissionsDataModel; import org.opentripplanner.ext.geocoder.LuceneIndex; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; import org.opentripplanner.ext.transmodelapi.TransmodelAPI; import org.opentripplanner.framework.application.LogMDCSupport; import org.opentripplanner.framework.application.OTPFeature; @@ -72,7 +73,8 @@ public class ConstructApplication { ConfigModel config, GraphBuilderDataSources graphBuilderDataSources, DataImportIssueSummary issueSummary, - EmissionsDataModel emissionsDataModel + EmissionsDataModel emissionsDataModel, + @Nullable StopConsolidationRepository stopConsolidationRepository ) { this.cli = cli; this.graphBuilderDataSources = graphBuilderDataSources; @@ -91,6 +93,7 @@ public class ConstructApplication { .worldEnvelopeRepository(worldEnvelopeRepository) .emissionsDataModel(emissionsDataModel) .dataImportIssueSummary(issueSummary) + .stopConsolidationRepository(stopConsolidationRepository) .build(); } @@ -122,6 +125,7 @@ public GraphBuilder createGraphBuilder() { transitModel(), factory.worldEnvelopeRepository(), factory.emissionsDataModel(), + factory.stopConsolidationRepository(), cli.doLoadStreetGraph(), cli.doSaveStreetGraph() ); @@ -243,6 +247,10 @@ public DataImportIssueSummary dataImportIssueSummary() { return factory.dataImportIssueSummary(); } + public StopConsolidationRepository stopConsolidationRepository() { + return factory.stopConsolidationRepository(); + } + public RealtimeVehicleRepository realtimeVehicleRepository() { return factory.realtimeVehicleRepository(); } diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java index eb286ea5843..097fcfb4f7a 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java @@ -7,6 +7,8 @@ import org.opentripplanner.ext.emissions.EmissionsDataModel; import org.opentripplanner.ext.emissions.EmissionsServiceModule; import org.opentripplanner.ext.ridehailing.configure.RideHailingServicesModule; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; +import org.opentripplanner.ext.stopconsolidation.configure.StopConsolidationServiceModule; import org.opentripplanner.graph_builder.issue.api.DataImportIssueSummary; import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; @@ -47,6 +49,7 @@ ConstructApplicationModule.class, RideHailingServicesModule.class, EmissionsServiceModule.class, + StopConsolidationServiceModule.class, } ) public interface ConstructApplicationFactory { @@ -73,6 +76,9 @@ public interface ConstructApplicationFactory { MetricsLogging metricsLogging(); + @Nullable + StopConsolidationRepository stopConsolidationRepository(); + @Component.Builder interface Builder { @BindsInstance @@ -90,6 +96,11 @@ interface Builder { @BindsInstance Builder worldEnvelopeRepository(WorldEnvelopeRepository worldEnvelopeRepository); + @BindsInstance + Builder stopConsolidationRepository( + @Nullable StopConsolidationRepository stopConsolidationRepository + ); + @BindsInstance Builder dataImportIssueSummary(DataImportIssueSummary issueSummary); diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java index 8f936d48522..2ff6de29b3d 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java @@ -8,6 +8,7 @@ import org.opentripplanner.astar.spi.TraverseVisitor; import org.opentripplanner.ext.emissions.EmissionsService; import org.opentripplanner.ext.ridehailing.RideHailingService; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; import org.opentripplanner.routing.graph.Graph; @@ -33,6 +34,7 @@ OtpServerRequestContext providesServerContext( RealtimeVehicleService realtimeVehicleService, VehicleRentalService vehicleRentalService, List rideHailingServices, + @Nullable StopConsolidationService stopConsolidationService, @Nullable TraverseVisitor traverseVisitor, EmissionsService emissionsService ) { @@ -50,6 +52,7 @@ OtpServerRequestContext providesServerContext( emissionsService, routerConfig.flexConfig(), rideHailingServices, + stopConsolidationService, traverseVisitor ); } diff --git a/src/main/java/org/opentripplanner/standalone/configure/LoadApplication.java b/src/main/java/org/opentripplanner/standalone/configure/LoadApplication.java index c8936ab3054..f22c5ab80ec 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/LoadApplication.java +++ b/src/main/java/org/opentripplanner/standalone/configure/LoadApplication.java @@ -1,7 +1,9 @@ package org.opentripplanner.standalone.configure; +import javax.annotation.Nullable; import org.opentripplanner.datastore.api.DataSource; import org.opentripplanner.ext.emissions.EmissionsDataModel; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; import org.opentripplanner.graph_builder.GraphBuilderDataSources; import org.opentripplanner.graph_builder.issue.api.DataImportIssueSummary; import org.opentripplanner.routing.graph.Graph; @@ -54,7 +56,8 @@ public ConstructApplication appConstruction(SerializedGraphObject obj) { obj.transitModel, obj.worldEnvelopeRepository, obj.issueSummary, - obj.emissionsDataModel + obj.emissionsDataModel, + obj.stopConsolidationRepository ); } @@ -65,7 +68,8 @@ public ConstructApplication appConstruction() { factory.emptyTransitModel(), factory.emptyWorldEnvelopeRepository(), DataImportIssueSummary.empty(), - factory.emptyEmissionsDataModel() + factory.emptyEmissionsDataModel(), + factory.emptyStopConsolidationRepository() ); } @@ -85,7 +89,8 @@ private ConstructApplication createAppConstruction( TransitModel transitModel, WorldEnvelopeRepository worldEnvelopeRepository, DataImportIssueSummary issueSummary, - EmissionsDataModel emissionsDataModel + @Nullable EmissionsDataModel emissionsDataModel, + @Nullable StopConsolidationRepository stopConsolidationRepository ) { return new ConstructApplication( cli, @@ -95,7 +100,8 @@ private ConstructApplication createAppConstruction( config(), graphBuilderDataSources(), issueSummary, - emissionsDataModel + emissionsDataModel, + stopConsolidationRepository ); } } diff --git a/src/main/java/org/opentripplanner/standalone/configure/LoadApplicationFactory.java b/src/main/java/org/opentripplanner/standalone/configure/LoadApplicationFactory.java index fbd5ad2de51..ca90c613db5 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/LoadApplicationFactory.java +++ b/src/main/java/org/opentripplanner/standalone/configure/LoadApplicationFactory.java @@ -7,6 +7,8 @@ import org.opentripplanner.datastore.configure.DataStoreModule; import org.opentripplanner.ext.datastore.gs.GsDataSourceModule; import org.opentripplanner.ext.emissions.EmissionsDataModel; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; +import org.opentripplanner.ext.stopconsolidation.configure.StopConsolidationRepositoryModule; import org.opentripplanner.graph_builder.GraphBuilderDataSources; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.service.worldenvelope.WorldEnvelopeRepository; @@ -26,6 +28,7 @@ DataStoreModule.class, GsDataSourceModule.class, WorldEnvelopeRepositoryModule.class, + StopConsolidationRepositoryModule.class, } ) public interface LoadApplicationFactory { @@ -48,6 +51,9 @@ public interface LoadApplicationFactory { @Singleton EmissionsDataModel emptyEmissionsDataModel(); + @Singleton + StopConsolidationRepository emptyStopConsolidationRepository(); + @Component.Builder interface Builder { @BindsInstance diff --git a/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java index 66bb4f5cbc3..f14fea66693 100644 --- a/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java @@ -7,6 +7,7 @@ import org.opentripplanner.astar.spi.TraverseVisitor; import org.opentripplanner.ext.emissions.EmissionsService; import org.opentripplanner.ext.ridehailing.RideHailingService; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; import org.opentripplanner.ext.vectortiles.VectorTilesResource; import org.opentripplanner.inspector.raster.TileRendererManager; import org.opentripplanner.raptor.api.request.RaptorTuningParameters; @@ -45,6 +46,7 @@ public class DefaultServerRequestContext implements OtpServerRequestContext { private final RealtimeVehicleService realtimeVehicleService; private final VehicleRentalService vehicleRentalService; private final EmissionsService emissionsService; + private final StopConsolidationService stopConsolidationService; /** * Make sure all mutable components are copied/cloned before calling this constructor. @@ -63,8 +65,9 @@ private DefaultServerRequestContext( VehicleRentalService vehicleRentalService, EmissionsService emissionsService, List rideHailingServices, - TraverseVisitor traverseVisitor, - FlexConfig flexConfig + StopConsolidationService stopConsolidationService, + FlexConfig flexConfig, + TraverseVisitor traverseVisitor ) { this.graph = graph; this.transitService = transitService; @@ -81,6 +84,7 @@ private DefaultServerRequestContext( this.realtimeVehicleService = realtimeVehicleService; this.rideHailingServices = rideHailingServices; this.emissionsService = emissionsService; + this.stopConsolidationService = stopConsolidationService; } /** @@ -100,6 +104,7 @@ public static DefaultServerRequestContext create( @Nullable EmissionsService emissionsService, FlexConfig flexConfig, List rideHailingServices, + @Nullable StopConsolidationService stopConsolidationService, @Nullable TraverseVisitor traverseVisitor ) { return new DefaultServerRequestContext( @@ -116,8 +121,9 @@ public static DefaultServerRequestContext create( vehicleRentalService, emissionsService, rideHailingServices, - traverseVisitor, - flexConfig + stopConsolidationService, + flexConfig, + traverseVisitor ); } @@ -188,6 +194,11 @@ public List rideHailingServices() { return rideHailingServices; } + @Override + public StopConsolidationService stopConsolidationService() { + return stopConsolidationService; + } + @Override public MeterRegistry meterRegistry() { return meterRegistry; diff --git a/src/main/java/org/opentripplanner/transit/model/network/RoutingTripPattern.java b/src/main/java/org/opentripplanner/transit/model/network/RoutingTripPattern.java index 95c76ad18e4..0d319af0529 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/RoutingTripPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/RoutingTripPattern.java @@ -12,7 +12,7 @@ * - The RTP is accessed frequently during the Raptor search, and we want it to be as small as * possible to load/access it in the cache and CPU for performance reasons. * - Also, we deduplicate these so a RTP can be reused by more than one TP. - * - This also provide explicit documentation on witch fields are used during a search and witch + * - This also provide explicit documentation on witch fields are used during a search and which * are not. */ public class RoutingTripPattern implements DefaultTripPattern, Serializable { diff --git a/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java b/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java index 934910bc414..fe1bb84cf3f 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java @@ -5,11 +5,13 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; import javax.annotation.Nonnull; import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.StopTime; +import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.site.StopLocation; @@ -323,6 +325,20 @@ public StopPatternBuilder cancelStops(List cancelledStopIndices) { return this; } + /** + * Replace the stop {@code old} in the stop pattern with ${code newStop}. + */ + public StopPatternBuilder replaceStop(FeedScopedId old, StopLocation newStop) { + Objects.requireNonNull(old); + Objects.requireNonNull(newStop); + for (int i = 0; i < stops.length; i++) { + if (stops[i].getId().equals(old)) { + stops[i] = newStop; + } + } + return this; + } + public StopPattern build() { boolean sameStops = Arrays.equals(stops, original.stops); boolean sameDropoffs = Arrays.equals(dropoffs, original.dropoffs); diff --git a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java index 05d4375c7ba..e89ea1940f6 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java @@ -5,9 +5,11 @@ import static org.opentripplanner.framework.lang.ObjectUtils.requireNotInitialized; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nonnull; import org.locationtech.jts.geom.Coordinate; @@ -415,14 +417,6 @@ public I18NString getTripHeadsign() { : getTripHeadSignFromTripTimes(tripTimes); } - public I18NString getStopHeadsign(int stopIndex) { - var tripTimes = scheduledTimetable.getRepresentativeTripTimes(); - if (tripTimes == null) { - return null; - } - return tripTimes.getHeadsign(stopIndex); - } - public TripPattern clone() { try { return (TripPattern) super.clone(); @@ -451,8 +445,25 @@ public String logName() { return route.logName(); } + /** + * Does the pattern contain any stops passed in as argument? + * This method is not optimized for performance so don't use it where that is critical. + */ + public boolean containsAnyStopId(Collection ids) { + return ids + .stream() + .anyMatch(id -> + stopPattern + .getStops() + .stream() + .map(StopLocation::getId) + .collect(Collectors.toUnmodifiableSet()) + .contains(id) + ); + } + private static Coordinate coordinate(StopLocation s) { - return new Coordinate(s.getLon(), s.getLat()); + return s.getCoordinate().asJtsCoordinate(); } @Override diff --git a/src/main/java/org/opentripplanner/transit/model/network/TripPatternBuilder.java b/src/main/java/org/opentripplanner/transit/model/network/TripPatternBuilder.java index e359952f5f9..f34d206922b 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/TripPatternBuilder.java +++ b/src/main/java/org/opentripplanner/transit/model/network/TripPatternBuilder.java @@ -49,7 +49,10 @@ public final class TripPatternBuilder this.hopGeometries = original.getGeometry() == null ? null - : IntStream.range(0, original.numberOfStops()).mapToObj(original::getHopGeometry).toList(); + : IntStream + .range(0, original.numberOfStops() - 1) + .mapToObj(original::getHopGeometry) + .toList(); } public TripPatternBuilder withName(String name) { diff --git a/src/test/java/org/opentripplanner/TestServerContext.java b/src/test/java/org/opentripplanner/TestServerContext.java index a62aba32759..1f3e6491232 100644 --- a/src/test/java/org/opentripplanner/TestServerContext.java +++ b/src/test/java/org/opentripplanner/TestServerContext.java @@ -49,6 +49,7 @@ public static OtpServerRequestContext createServerContext( createEmissionsService(), routerConfig.flexConfig(), List.of(), + null, null ); creatTransitLayerForRaptor(transitModel, routerConfig.transitTuningConfig()); diff --git a/src/test/java/org/opentripplanner/generate/doc/StopConsolidationDocTest.java b/src/test/java/org/opentripplanner/generate/doc/StopConsolidationDocTest.java new file mode 100644 index 00000000000..62f76032a09 --- /dev/null +++ b/src/test/java/org/opentripplanner/generate/doc/StopConsolidationDocTest.java @@ -0,0 +1,76 @@ +package org.opentripplanner.generate.doc; + +import static org.opentripplanner.framework.io.FileUtils.assertFileEquals; +import static org.opentripplanner.framework.io.FileUtils.readFile; +import static org.opentripplanner.framework.io.FileUtils.writeFile; +import static org.opentripplanner.generate.doc.framework.DocsTestConstants.DOCS_ROOT; +import static org.opentripplanner.generate.doc.framework.DocsTestConstants.TEMPLATE_ROOT; +import static org.opentripplanner.generate.doc.framework.TemplateUtil.replaceSection; +import static org.opentripplanner.standalone.config.framework.json.JsonSupport.jsonNodeFromResource; + +import java.io.File; +import org.junit.jupiter.api.Test; +import org.opentripplanner.generate.doc.framework.DocBuilder; +import org.opentripplanner.generate.doc.framework.GeneratesDocumentation; +import org.opentripplanner.generate.doc.framework.TemplateUtil; +import org.opentripplanner.standalone.config.BuildConfig; +import org.opentripplanner.standalone.config.framework.json.NodeAdapter; +import org.opentripplanner.test.support.ResourceLoader; + +@GeneratesDocumentation +public class StopConsolidationDocTest { + + private static final String FILE_NAME = "StopConsolidation.md"; + private static final File TEMPLATE = new File(TEMPLATE_ROOT, FILE_NAME); + private static final File OUT_FILE = new File(DOCS_ROOT + "/sandbox", FILE_NAME); + + private static final String CONFIG_FILENAME = "standalone/config/build-config.json"; + + @Test + public void updateDoc() { + NodeAdapter node = readConfig(); + + var lines = ResourceLoader + .of(this) + .lines("/org/opentripplanner/ext/stopconsolidation/consolidated-stops.csv", 6); + + // Read and close input file (same as output file) + String template = readFile(TEMPLATE); + String original = readFile(OUT_FILE); + + var joined = String.join("\n", lines); + + var csvExample = """ + ``` + %s + ``` + """.formatted(joined); + + template = replaceSection(template, "config", updaterDoc(node)); + template = replaceSection(template, "file", csvExample); + + writeFile(OUT_FILE, template); + assertFileEquals(original, OUT_FILE); + } + + private NodeAdapter readConfig() { + var json = jsonNodeFromResource(CONFIG_FILENAME); + final String propName = "stopConsolidationFile"; + var node = json.path(propName); + var jsonNode = TemplateUtil.jsonExampleBuilder(node).wrapInObject(propName).build(); + var adapter = new NodeAdapter(jsonNode, "source"); + var conf = new BuildConfig(adapter, false); + return conf.asNodeAdapter(); + } + + private String updaterDoc(NodeAdapter node) { + DocBuilder buf = new DocBuilder(); + addExample(buf, node); + return buf.toString(); + } + + private void addExample(DocBuilder buf, NodeAdapter node) { + var root = TemplateUtil.jsonExampleBuilder(node.rawNode()).build(); + buf.addExample("build-config.json", root); + } +} diff --git a/src/test/java/org/opentripplanner/graph_builder/module/interlining/InterlineProcessorTest.java b/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java similarity index 99% rename from src/test/java/org/opentripplanner/graph_builder/module/interlining/InterlineProcessorTest.java rename to src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java index 1519cce3372..8e9a803c94b 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/interlining/InterlineProcessorTest.java +++ b/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java @@ -1,4 +1,4 @@ -package org.opentripplanner.graph_builder.module.interlining; +package org.opentripplanner.gtfs.interlining; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/src/test/java/org/opentripplanner/routing/graph/GraphSerializationTest.java b/src/test/java/org/opentripplanner/routing/graph/GraphSerializationTest.java index 0b4a7456e52..5ca89c0984d 100644 --- a/src/test/java/org/opentripplanner/routing/graph/GraphSerializationTest.java +++ b/src/test/java/org/opentripplanner/routing/graph/GraphSerializationTest.java @@ -184,7 +184,8 @@ private void testRoundTrip( BuildConfig.DEFAULT, RouterConfig.DEFAULT, DataImportIssueSummary.empty(), - emissionsDataModel + emissionsDataModel, + null ); serializedObj.save(new FileDataSource(tempFile, FileType.GRAPH)); SerializedGraphObject deserializedGraph = SerializedGraphObject.load(tempFile); diff --git a/src/test/java/org/opentripplanner/test/support/ResourceLoader.java b/src/test/java/org/opentripplanner/test/support/ResourceLoader.java index 74d34c573ef..38fe02a8c74 100644 --- a/src/test/java/org/opentripplanner/test/support/ResourceLoader.java +++ b/src/test/java/org/opentripplanner/test/support/ResourceLoader.java @@ -4,9 +4,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; /** * Loads files from the resources folder relative to the package name of the class/instances @@ -70,4 +75,27 @@ public URI uri(String s) { throw new IllegalArgumentException(e); } } + + /** + * Returns the specified number of lines from a file. + */ + public List lines(String s, int lines) { + var path = file(s).toPath(); + try { + return Files.readAllLines(path, StandardCharsets.UTF_8).stream().limit(lines).toList(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns the specified resources as an input stream. + */ + public InputStream inputStream(String path) { + try { + return url(path).openStream(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java b/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java index cdee510a86a..d92aad0e159 100644 --- a/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java +++ b/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java @@ -1,5 +1,6 @@ package org.opentripplanner.transit.model.network; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -60,4 +61,19 @@ void boardingAlightingConditions() { assertFalse(stopPattern.canBoard(2), "Forbidden at GroupStop"); assertFalse(stopPattern.canBoard(3), "Forbidden at AreaStop"); } + + @Test + void replaceStop() { + var s1 = testModel.stop("1").build(); + var s2 = testModel.stop("2").build(); + var s3 = testModel.stop("3").build(); + var s4 = testModel.stop("4").build(); + + var pattern = TransitModelForTest.stopPattern(s1, s2, s3); + + assertEquals(List.of(s1, s2, s3), pattern.getStops()); + + var updated = pattern.mutate().replaceStop(s2.getId(), s4).build(); + assertEquals(List.of(s1, s4, s3), updated.getStops()); + } } diff --git a/src/test/java/org/opentripplanner/transit/model/network/TripPatternTest.java b/src/test/java/org/opentripplanner/transit/model/network/TripPatternTest.java index 3f7c6d0ee65..b2adc7544a3 100644 --- a/src/test/java/org/opentripplanner/transit/model/network/TripPatternTest.java +++ b/src/test/java/org/opentripplanner/transit/model/network/TripPatternTest.java @@ -5,25 +5,44 @@ import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.framework.geometry.GeometryUtils.makeLineString; +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; +import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.LineString; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.site.RegularStop; class TripPatternTest { private static final String ID = "1"; private static final String NAME = "short name"; - private static TransitModelForTest TEST_MODEL = TransitModelForTest.of(); + private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); private static final Route ROUTE = TransitModelForTest.route("routeId").build(); - private static final StopPattern STOP_PATTERN = TEST_MODEL.stopPattern(10); + public static final RegularStop STOP_A = TEST_MODEL.stop("A").build(); + public static final RegularStop STOP_B = TEST_MODEL.stop("B").build(); + public static final RegularStop STOP_C = TEST_MODEL.stop("C").build(); + private static final StopPattern STOP_PATTERN = TransitModelForTest.stopPattern( + STOP_A, + STOP_B, + STOP_C + ); + + private static final List HOP_GEOMETRIES = List.of( + makeLineString(STOP_A.getCoordinate(), STOP_B.getCoordinate()), + makeLineString(STOP_B.getCoordinate(), STOP_C.getCoordinate()) + ); + private static final TripPattern subject = TripPattern - .of(TransitModelForTest.id(ID)) + .of(id(ID)) .withName(NAME) .withRoute(ROUTE) .withStopPattern(STOP_PATTERN) + .withHopGeometries(HOP_GEOMETRIES) .build(); @Test @@ -46,12 +65,15 @@ void copy() { assertEquals("v2", copy.getName()); assertEquals(ROUTE, copy.getRoute()); assertEquals(STOP_PATTERN, copy.getStopPattern()); + assertEquals(subject.getHopGeometry(0), copy.getHopGeometry(0)); + assertEquals(subject.getHopGeometry(1), copy.getHopGeometry(1)); + assertEquals(HOP_GEOMETRIES.get(1), copy.getHopGeometry(1)); } @Test void sameAs() { assertTrue(subject.sameAs(subject.copy().build())); - assertFalse(subject.sameAs(subject.copy().withId(TransitModelForTest.id("X")).build())); + assertFalse(subject.sameAs(subject.copy().withId(id("X")).build())); assertFalse(subject.sameAs(subject.copy().withName("X").build())); assertFalse( subject.sameAs( @@ -70,11 +92,7 @@ void initNameShouldThrow() { @Test void shouldAddName() { var name = "xyz"; - var noNameYet = TripPattern - .of(TransitModelForTest.id(ID)) - .withRoute(ROUTE) - .withStopPattern(STOP_PATTERN) - .build(); + var noNameYet = TripPattern.of(id(ID)).withRoute(ROUTE).withStopPattern(STOP_PATTERN).build(); noNameYet.initName(name); @@ -84,11 +102,19 @@ void shouldAddName() { @Test void shouldResolveMode() { var patternWithoutExplicitMode = TripPattern - .of(TransitModelForTest.id(ID)) + .of(id(ID)) .withRoute(ROUTE) .withStopPattern(STOP_PATTERN) .build(); assertEquals(patternWithoutExplicitMode.getMode(), ROUTE.getMode()); } + + @Test + void containsAnyStopId() { + assertFalse(subject.containsAnyStopId(List.of())); + assertFalse(subject.containsAnyStopId(List.of(id("not-in-pattern")))); + assertTrue(subject.containsAnyStopId(List.of(STOP_A.getId()))); + assertTrue(subject.containsAnyStopId(List.of(STOP_A.getId(), id("not-in-pattern")))); + } } diff --git a/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java b/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java index f7f290e4608..463e9342e6d 100644 --- a/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java +++ b/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java @@ -15,6 +15,8 @@ import java.util.function.Predicate; import org.opentripplanner.TestServerContext; import org.opentripplanner.datastore.OtpDataStore; +import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationRepository; +import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationService; import org.opentripplanner.framework.application.OtpAppException; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.raptor.configure.RaptorConfig; @@ -118,6 +120,7 @@ public SpeedTest( TestServerContext.createEmissionsService(), config.flexConfig, List.of(), + null, null ); // Creating transitLayerForRaptor should be integrated into the TransitModel, but for now diff --git a/src/test/resources/standalone/config/build-config.json b/src/test/resources/standalone/config/build-config.json index fb8c682bbda..2e268cb427d 100644 --- a/src/test/resources/standalone/config/build-config.json +++ b/src/test/resources/standalone/config/build-config.json @@ -74,6 +74,7 @@ } } ], + "stopConsolidationFile": "consolidated-stops.csv", "emissions": { "carAvgCo2PerKm": 170, "carAvgOccupancy": 1.3