From c85c55d37ae7535b4815ecfd5723a49d38507386 Mon Sep 17 00:00:00 2001 From: Olaf Flebbe at Bosch eBike <123375381+OlafFlebbeBosch@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:27:50 +0200 Subject: [PATCH 01/50] mapbox profile specific voice instructions (#3165) * navigation profile specific voiceinstruction placement * fix: hiking should have been walking * fix: try to find a mapbox profile for the graphhopper profile * Revert "fix: try to find a mapbox profile for the graphhopper profile" This reverts commit 3db1c5bc79176fe0c525409e4d77fde431726c11. * feat: configure DistanceConfig from Profile hints too * chore: add myself (Karol Olszacki) to the contributors list * fix: create NavigationTransportMode enum and use that * fix: make sure we accept all kinds of values for wider compatibility with OSRM/Mapbox/etc * fix: DistanceConfig enhancement (#6) * fix: fold more cases into DistanceConfig, remove the new enum and allow usage of existing one * chore: update the example with gh-centric terms --------- Co-authored-by: Karol Olszacki --- CONTRIBUTORS.md | 1 + config-example.yml | 5 + .../navigation/DistanceConfig.java | 76 +++++++-- .../navigation/NavigateResource.java | 4 +- .../navigation/DistanceConfigTest.java | 53 +++++++ .../NavigateResponseConverterTest.java | 148 +++++++++++++++++- 6 files changed, 266 insertions(+), 21 deletions(-) create mode 100644 navigation/src/test/java/com/graphhopper/navigation/DistanceConfigTest.java diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index a45fe969111..b2e60a0f144 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -49,6 +49,7 @@ Here is an overview: * jessLryan, max elevation can now be negative * joe-akeem, improvements like #2158 * JohannesPelzer, improved GPX information and various other things + * karololszacki, introduce `navigation_transport_mode` option for Profiles to easily set which Voice Guidance distances to use * karussell, one of the core developers * khuebner, initial turn costs support * kodonnell, adding support for CH and other algorithms (#60) and penalizing inner-link U-turns (#88) diff --git a/config-example.yml b/config-example.yml index 7b84e74e4b7..91e38c91f9e 100644 --- a/config-example.yml +++ b/config-example.yml @@ -30,6 +30,7 @@ graphhopper: profiles: - name: car +# navigation_transport_mode: "car" # turn_costs: # vehicle_types: [motorcar, motor_vehicle] # u_turn_costs: 60 @@ -39,15 +40,19 @@ graphhopper: # You can use the following in-built profiles. After you start GraphHopper it will print which encoded values you'll have to add to graph.encoded_values in this config file. # # - name: foot +# navigation_transport_mode: "foot" # custom_model_files: [foot.json, foot_elevation.json] # # - name: bike +# navigation_transport_mode: "bike" # custom_model_files: [bike.json, bike_elevation.json] # # - name: racingbike +# navigation_transport_mode: "bike" # custom_model_files: [racingbike.json, bike_elevation.json] # # - name: mtb +# navigation_transport_mode: "bike" # custom_model_files: [mtb.json, bike_elevation.json] # # # See the bus.json for more details. diff --git a/navigation/src/main/java/com/graphhopper/navigation/DistanceConfig.java b/navigation/src/main/java/com/graphhopper/navigation/DistanceConfig.java index eafebd53132..59177b5cb79 100644 --- a/navigation/src/main/java/com/graphhopper/navigation/DistanceConfig.java +++ b/navigation/src/main/java/com/graphhopper/navigation/DistanceConfig.java @@ -17,6 +17,9 @@ */ package com.graphhopper.navigation; +import com.graphhopper.config.Profile; +import com.graphhopper.routing.util.TransportationMode; +import com.graphhopper.util.Helper; import com.graphhopper.util.TranslationMap; import java.util.ArrayList; @@ -30,22 +33,65 @@ public class DistanceConfig { final List voiceInstructions; final DistanceUtils.Unit unit; - public DistanceConfig(DistanceUtils.Unit unit, TranslationMap translationMap, Locale locale) { + public DistanceConfig(DistanceUtils.Unit unit, TranslationMap translationMap, Locale locale, Profile profile) { + this(unit, translationMap, locale, profile.getHints().getString("navigation_transport_mode", "")); + } + + public DistanceConfig(DistanceUtils.Unit unit, TranslationMap translationMap, Locale locale, TransportationMode mode) { + this(unit, translationMap, locale, mode.name()); + } + + public DistanceConfig(DistanceUtils.Unit unit, TranslationMap translationMap, Locale locale, String mode) { this.unit = unit; - if (unit == DistanceUtils.Unit.METRIC) { - voiceInstructions = Arrays.asList( - new InitialVoiceInstructionConfig(FOR_HIGHER_DISTANCE_PLURAL.metric, translationMap, locale, 4250, 250, unit), - new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_PLURAL.metric, translationMap, locale, 2000, 2), - new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_SINGULAR.metric, translationMap, locale, 1000, 1), - new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.metric, translationMap, locale, new int[]{400, 200}, new int[]{400, 200}) - ); - } else { - voiceInstructions = Arrays.asList( - new InitialVoiceInstructionConfig(FOR_HIGHER_DISTANCE_PLURAL.metric, translationMap, locale, 4250, 250, unit), - new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_PLURAL.imperial, translationMap, locale, 3220, 2), - new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_SINGULAR.imperial, translationMap, locale, 1610, 1), - new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.imperial, translationMap, locale, new int[]{400, 200}, new int[]{1300, 600}) - ); + switch (Helper.toLowerCase(mode)) { + case "biking": + case "cycling": + case "cyclist": + case "mtb": + case "racingbike": + case "bike": + if (unit == DistanceUtils.Unit.METRIC) { + voiceInstructions = Arrays.asList( + new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.metric, translationMap, locale, new int[]{150}, + new int[]{150})); + } else { + voiceInstructions = Arrays.asList( + new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.imperial, translationMap, locale, new int[]{150}, + new int[]{500})); + } + break; + case "walking": + case "walk": + case "hiking": + case "hike": + case "foot": + case "pedestrian": + if (unit == DistanceUtils.Unit.METRIC) { + voiceInstructions = Arrays.asList( + new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.metric, translationMap, locale, new int[]{50}, + new int[]{50})); + } else { + voiceInstructions = Arrays.asList( + new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.imperial, translationMap, locale, new int[]{50}, + new int[]{150})); + } + break; + default: + if (unit == DistanceUtils.Unit.METRIC) { + voiceInstructions = Arrays.asList( + new InitialVoiceInstructionConfig(FOR_HIGHER_DISTANCE_PLURAL.metric, translationMap, locale, 4250, 250, unit), + new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_PLURAL.metric, translationMap, locale, 2000, 2), + new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_SINGULAR.metric, translationMap, locale, 1000, 1), + new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.metric, translationMap, locale, new int[]{400, 200}, new int[]{400, 200}) + ); + } else { + voiceInstructions = Arrays.asList( + new InitialVoiceInstructionConfig(FOR_HIGHER_DISTANCE_PLURAL.metric, translationMap, locale, 4250, 250, unit), + new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_PLURAL.imperial, translationMap, locale, 3220, 2), + new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_SINGULAR.imperial, translationMap, locale, 1610, 1), + new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.imperial, translationMap, locale, new int[]{400, 200}, new int[]{1300, 600}) + ); + } } } diff --git a/navigation/src/main/java/com/graphhopper/navigation/NavigateResource.java b/navigation/src/main/java/com/graphhopper/navigation/NavigateResource.java index c2d213d2ad8..8daa3f9c557 100644 --- a/navigation/src/main/java/com/graphhopper/navigation/NavigateResource.java +++ b/navigation/src/main/java/com/graphhopper/navigation/NavigateResource.java @@ -144,7 +144,7 @@ public Response doGet( } else { DistanceUtils.Unit unit = voiceUnits.equals("metric") ? DistanceUtils.Unit.METRIC : DistanceUtils.Unit.IMPERIAL; Locale locale = Helper.getLocale(localeStr); - DistanceConfig config = new DistanceConfig(unit, translationMap, locale); + DistanceConfig config = new DistanceConfig(unit, translationMap, locale, graphHopper.getProfile(ghProfile)); logger.info(logStr); return Response.ok(NavigateResponseConverter.convertFromGHResponse(ghResponse, translationMap, locale, config)). header("X-GH-Took", "" + Math.round(took * 1000)). @@ -214,7 +214,7 @@ public Response doPost(@NotNull GHRequest request, @Context HttpServletRequest h unit = DistanceUtils.Unit.IMPERIAL; } - DistanceConfig config = new DistanceConfig(unit, translationMap, request.getLocale()); + DistanceConfig config = new DistanceConfig(unit, translationMap, request.getLocale(), graphHopper.getProfile(request.getProfile())); logger.info(logStr); return Response.ok(NavigateResponseConverter.convertFromGHResponse(ghResponse, translationMap, request.getLocale(), config)). header("X-GH-Took", "" + Math.round(took * 1000)). diff --git a/navigation/src/test/java/com/graphhopper/navigation/DistanceConfigTest.java b/navigation/src/test/java/com/graphhopper/navigation/DistanceConfigTest.java new file mode 100644 index 00000000000..472491a4833 --- /dev/null +++ b/navigation/src/test/java/com/graphhopper/navigation/DistanceConfigTest.java @@ -0,0 +1,53 @@ +package com.graphhopper.navigation; + +import com.graphhopper.config.Profile; +import com.graphhopper.routing.util.TransportationMode; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DistanceConfigTest { + + @Test + public void distanceConfigTest() { + // from TransportationMode + DistanceConfig car = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, TransportationMode.CAR); + assertEquals(4, car.voiceInstructions.size()); + DistanceConfig foot = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, TransportationMode.FOOT); + assertEquals(1, foot.voiceInstructions.size()); + DistanceConfig bike = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, TransportationMode.BIKE); + assertEquals(1, bike.voiceInstructions.size()); + DistanceConfig bus = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, TransportationMode.BUS); + assertEquals(4, bus.voiceInstructions.size()); + + + // from Profile + Profile awesomeProfile = new Profile("my_awesome_profile").putHint("navigation_transport_mode", "car"); + DistanceConfig carFromProfile = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, awesomeProfile); + assertEquals(4, carFromProfile.voiceInstructions.size()); + + Profile fastWalkProfile = new Profile("my_fast_walk_profile").putHint("navigation_transport_mode", "foot"); + DistanceConfig footFromProfile = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, fastWalkProfile); + assertEquals(1, footFromProfile.voiceInstructions.size()); + + Profile crazyMtbProfile = new Profile("my_crazy_mtb").putHint("navigation_transport_mode", "bike"); + DistanceConfig bikeFromProfile = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, crazyMtbProfile); + assertEquals(1, bikeFromProfile.voiceInstructions.size()); + + Profile truckProfile = new Profile("my_truck"); // no hint set, so defaults to car + DistanceConfig truckCfg = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, truckProfile); + assertEquals(4, truckCfg.voiceInstructions.size()); + + + // from String + DistanceConfig driving = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, "driving"); + assertEquals(4, driving.voiceInstructions.size()); + DistanceConfig anything = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, "anything"); + assertEquals(4, anything.voiceInstructions.size()); + DistanceConfig none = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, ""); + assertEquals(4, none.voiceInstructions.size()); + DistanceConfig biking = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, "biking"); + assertEquals(1, biking.voiceInstructions.size()); + } + +} diff --git a/navigation/src/test/java/com/graphhopper/navigation/NavigateResponseConverterTest.java b/navigation/src/test/java/com/graphhopper/navigation/NavigateResponseConverterTest.java index 2a64e09ef76..f71b0e5a85d 100644 --- a/navigation/src/test/java/com/graphhopper/navigation/NavigateResponseConverterTest.java +++ b/navigation/src/test/java/com/graphhopper/navigation/NavigateResponseConverterTest.java @@ -7,6 +7,7 @@ import com.graphhopper.GraphHopper; import com.graphhopper.jackson.ResponsePathSerializer; import com.graphhopper.routing.TestProfiles; +import com.graphhopper.routing.util.TransportationMode; import com.graphhopper.util.Helper; import com.graphhopper.util.Parameters; import com.graphhopper.util.PointList; @@ -29,9 +30,8 @@ public class NavigateResponseConverterTest { private static final String osmFile = "../core/files/andorra.osm.gz"; private static GraphHopper hopper; private static final String profile = "my_car"; - private final TranslationMap trMap = hopper.getTranslationMap(); - private final DistanceConfig distanceConfig = new DistanceConfig(DistanceUtils.Unit.METRIC, trMap, Locale.ENGLISH); + private final DistanceConfig distanceConfig = new DistanceConfig(DistanceUtils.Unit.METRIC, trMap, Locale.ENGLISH, TransportationMode.CAR); @BeforeAll public static void beforeClass() { @@ -183,7 +183,7 @@ public void voiceInstructionsImperialTest() { GHResponse rsp = hopper.route(new GHRequest(42.554851, 1.536198, 42.510071, 1.548128).setProfile(profile)); ObjectNode json = NavigateResponseConverter.convertFromGHResponse(rsp, trMap, Locale.ENGLISH, - new DistanceConfig(DistanceUtils.Unit.IMPERIAL, trMap, Locale.ENGLISH)); + new DistanceConfig(DistanceUtils.Unit.IMPERIAL, trMap, Locale.ENGLISH, TransportationMode.CAR)); JsonNode steps = json.get("routes").get(0).get("legs").get(0).get("steps"); @@ -212,6 +212,146 @@ public void voiceInstructionsImperialTest() { assertEquals("keep right", voiceInstruction.get("announcement").asText()); } + @Test + public void voiceInstructionsWalkingMetricTest() { + + GHResponse rsp = hopper.route(new GHRequest(42.554851, 1.536198, 42.510071, 1.548128).setProfile(profile)); + + ObjectNode json = NavigateResponseConverter.convertFromGHResponse(rsp, trMap, Locale.ENGLISH, + new DistanceConfig(DistanceUtils.Unit.METRIC, trMap, Locale.ENGLISH, TransportationMode.FOOT)); + + JsonNode steps = json.get("routes").get(0).get("legs").get(0).get("steps"); + + // Step 4 is about 240m long + JsonNode step = steps.get(4); + JsonNode maneuver = step.get("maneuver"); + + JsonNode voiceInstructions = step.get("voiceInstructions"); + assertEquals(2, voiceInstructions.size()); + JsonNode voiceInstruction = voiceInstructions.get(0); + assertEquals(50, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1); + assertEquals("In 50 meters At roundabout, take exit 2 onto CS-340, then At roundabout, take exit 2 onto CG-3", + voiceInstruction.get("announcement").asText()); + + // Step 14 is over 3km long + step = steps.get(14); + maneuver = step.get("maneuver"); + + voiceInstructions = step.get("voiceInstructions"); + assertEquals(2, voiceInstructions.size()); + voiceInstruction = voiceInstructions.get(0); + assertEquals(50, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1); + assertEquals("In 50 meters keep right", voiceInstruction.get("announcement").asText()); + + voiceInstruction = voiceInstructions.get(1); + assertEquals("keep right", voiceInstruction.get("announcement").asText()); + } + + @Test + public void voiceInstructionsWalkingImperialTest() { + + GHResponse rsp = hopper.route(new GHRequest(42.554851, 1.536198, 42.510071, 1.548128).setProfile(profile)); + + ObjectNode json = NavigateResponseConverter.convertFromGHResponse(rsp, trMap, Locale.ENGLISH, + new DistanceConfig(DistanceUtils.Unit.IMPERIAL, trMap, Locale.ENGLISH, TransportationMode.FOOT)); + + JsonNode steps = json.get("routes").get(0).get("legs").get(0).get("steps"); + + // Step 4 is about 240m long + JsonNode step = steps.get(4); + JsonNode maneuver = step.get("maneuver"); + + JsonNode voiceInstructions = step.get("voiceInstructions"); + assertEquals(2, voiceInstructions.size()); + JsonNode voiceInstruction = voiceInstructions.get(0); + assertEquals(50, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1); + assertEquals("In 150 feet At roundabout, take exit 2 onto CS-340, then At roundabout, take exit 2 onto CG-3", + voiceInstruction.get("announcement").asText()); + + // Step 14 is over 3km long + step = steps.get(14); + maneuver = step.get("maneuver"); + + voiceInstructions = step.get("voiceInstructions"); + assertEquals(2, voiceInstructions.size()); + voiceInstruction = voiceInstructions.get(0); + assertEquals(50, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1); + assertEquals("In 150 feet keep right", voiceInstruction.get("announcement").asText()); + + voiceInstruction = voiceInstructions.get(1); + assertEquals("keep right", voiceInstruction.get("announcement").asText()); + } + + @Test + public void voiceInstructionsCyclingMetricTest() { + + GHResponse rsp = hopper.route(new GHRequest(42.554851, 1.536198, 42.510071, 1.548128).setProfile(profile)); + + ObjectNode json = NavigateResponseConverter.convertFromGHResponse(rsp, trMap, Locale.ENGLISH, + new DistanceConfig(DistanceUtils.Unit.METRIC, trMap, Locale.ENGLISH, TransportationMode.BIKE)); + + JsonNode steps = json.get("routes").get(0).get("legs").get(0).get("steps"); + + // Step 4 is about 240m long + JsonNode step = steps.get(4); + JsonNode maneuver = step.get("maneuver"); + + JsonNode voiceInstructions = step.get("voiceInstructions"); + assertEquals(2, voiceInstructions.size()); + JsonNode voiceInstruction = voiceInstructions.get(0); + assertEquals(150, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1); + assertEquals("In 150 meters At roundabout, take exit 2 onto CS-340, then At roundabout, take exit 2 onto CG-3", + voiceInstruction.get("announcement").asText()); + + // Step 14 is over 3km long + step = steps.get(14); + maneuver = step.get("maneuver"); + + voiceInstructions = step.get("voiceInstructions"); + assertEquals(2, voiceInstructions.size()); + voiceInstruction = voiceInstructions.get(0); + assertEquals(150, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1); + assertEquals("In 150 meters keep right", voiceInstruction.get("announcement").asText()); + + voiceInstruction = voiceInstructions.get(1); + assertEquals("keep right", voiceInstruction.get("announcement").asText()); + } + + @Test + public void voiceInstructionsCyclingImperialTest() { + + GHResponse rsp = hopper.route(new GHRequest(42.554851, 1.536198, 42.510071, 1.548128).setProfile(profile)); + + ObjectNode json = NavigateResponseConverter.convertFromGHResponse(rsp, trMap, Locale.ENGLISH, + new DistanceConfig(DistanceUtils.Unit.IMPERIAL, trMap, Locale.ENGLISH, TransportationMode.BIKE)); + + JsonNode steps = json.get("routes").get(0).get("legs").get(0).get("steps"); + + // Step 4 is about 240m long + JsonNode step = steps.get(4); + JsonNode maneuver = step.get("maneuver"); + + JsonNode voiceInstructions = step.get("voiceInstructions"); + assertEquals(2, voiceInstructions.size()); + JsonNode voiceInstruction = voiceInstructions.get(0); + assertEquals(150, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1); + assertEquals("In 500 feet At roundabout, take exit 2 onto CS-340, then At roundabout, take exit 2 onto CG-3", + voiceInstruction.get("announcement").asText()); + + // Step 14 is over 3km long + step = steps.get(14); + maneuver = step.get("maneuver"); + + voiceInstructions = step.get("voiceInstructions"); + assertEquals(2, voiceInstructions.size()); + voiceInstruction = voiceInstructions.get(0); + assertEquals(150, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1); + assertEquals("In 500 feet keep right", voiceInstruction.get("announcement").asText()); + + voiceInstruction = voiceInstructions.get(1); + assertEquals("keep right", voiceInstruction.get("announcement").asText()); + } + @Test @Disabled public void alternativeRoutesTest() { @@ -244,7 +384,7 @@ public void voiceInstructionTranslationTest() { rsp = hopper.route( new GHRequest(42.554851, 1.536198, 42.510071, 1.548128).setProfile(profile).setLocale(Locale.GERMAN)); - DistanceConfig distanceConfigGerman = new DistanceConfig(DistanceUtils.Unit.METRIC, trMap, Locale.GERMAN); + DistanceConfig distanceConfigGerman = new DistanceConfig(DistanceUtils.Unit.METRIC, trMap, Locale.GERMAN, TransportationMode.CAR); json = NavigateResponseConverter.convertFromGHResponse(rsp, trMap, Locale.GERMAN, distanceConfigGerman); From 087bacdd8b96ab2d1d46f64ec90f0edc8b0131e1 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 18 Jul 2025 18:26:42 +0200 Subject: [PATCH 02/50] Update maps to c688588a --- web-bundle/pom.xml | 2 +- web-bundle/src/main/resources/com/graphhopper/maps/config.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/web-bundle/pom.xml b/web-bundle/pom.xml index 9cdc3e17cff..15816a99118 100644 --- a/web-bundle/pom.xml +++ b/web-bundle/pom.xml @@ -7,7 +7,7 @@ jar 11.0-SNAPSHOT - 0.0.0-34acd30e898fe0d0ee388bf73d6619fec6cdc824 + 0.0.0-c688588ae7fcba2b28d3ce25a6ebbaaa27a42f8e GraphHopper Dropwizard Bundle diff --git a/web-bundle/src/main/resources/com/graphhopper/maps/config.js b/web-bundle/src/main/resources/com/graphhopper/maps/config.js index 3caed0352ce..5de049ac33b 100644 --- a/web-bundle/src/main/resources/com/graphhopper/maps/config.js +++ b/web-bundle/src/main/resources/com/graphhopper/maps/config.js @@ -19,4 +19,5 @@ const config = { ], snapPreventions: ['ferry'], }, -} \ No newline at end of file + profile_group_mapping: {}, +} From a148d512442e293c354553b37f0c9fb866ef296b Mon Sep 17 00:00:00 2001 From: Michael Zilske Date: Tue, 5 Aug 2025 12:30:33 +0200 Subject: [PATCH 03/50] Trip-Based Public Transit Routing (#3184) --- .../main/java/com/conveyal/gtfs/GTFSFeed.java | 40 +- .../com/conveyal/gtfs/model/StopTime.java | 10 + .../com/graphhopper/gtfs/GraphExplorer.java | 7 +- .../com/graphhopper/gtfs/GraphHopperGtfs.java | 61 ++- .../java/com/graphhopper/gtfs/GtfsReader.java | 18 +- .../com/graphhopper/gtfs/GtfsStorage.java | 170 +++++-- .../graphhopper/gtfs/PtEdgeAttributes.java | 1 + .../gtfs/PtRouterTripBasedImpl.java | 382 ++++++++++++++++ .../com/graphhopper/gtfs/RealtimeFeed.java | 30 +- .../java/com/graphhopper/gtfs/Request.java | 15 +- .../java/com/graphhopper/gtfs/Transfers.java | 3 +- .../com/graphhopper/gtfs/TripBasedRouter.java | 424 ++++++++++++++++++ .../com/graphhopper/gtfs/TripFromLabel.java | 39 +- .../main/java/com/graphhopper/gtfs/Trips.java | 342 ++++++++++++++ .../java/com/graphhopper/AnotherAgencyIT.java | 194 ++++++-- .../com/graphhopper/GraphHopperGtfsIT.java | 307 ++++++++----- .../graphhopper/GraphHopperMultimodalIT.java | 241 ++++++---- .../src/main/java/com/graphhopper/Trip.java | 12 +- .../jackson/ResponsePathSerializer.java | 2 +- .../graphhopper/http/GraphHopperBundle.java | 5 + .../resources/PtRouteResource.java | 9 +- .../resources/PtRouteResourceTest.java | 21 + .../PtRouteResourceTripBasedTest.java | 207 +++++++++ 23 files changed, 2235 insertions(+), 305 deletions(-) create mode 100644 reader-gtfs/src/main/java/com/graphhopper/gtfs/PtRouterTripBasedImpl.java create mode 100644 reader-gtfs/src/main/java/com/graphhopper/gtfs/TripBasedRouter.java create mode 100644 reader-gtfs/src/main/java/com/graphhopper/gtfs/Trips.java create mode 100644 web/src/test/java/com/graphhopper/application/resources/PtRouteResourceTripBasedTest.java diff --git a/reader-gtfs/src/main/java/com/conveyal/gtfs/GTFSFeed.java b/reader-gtfs/src/main/java/com/conveyal/gtfs/GTFSFeed.java index 68ad264b3c7..b7e7239f851 100644 --- a/reader-gtfs/src/main/java/com/conveyal/gtfs/GTFSFeed.java +++ b/reader-gtfs/src/main/java/com/conveyal/gtfs/GTFSFeed.java @@ -31,6 +31,7 @@ import com.conveyal.gtfs.model.Calendar; import com.conveyal.gtfs.model.*; import com.google.common.collect.Iterables; +import com.graphhopper.gtfs.Trips; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateList; import org.locationtech.jts.geom.GeometryFactory; @@ -51,6 +52,7 @@ import java.util.*; import java.util.concurrent.ConcurrentNavigableMap; import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.stream.StreamSupport; /** @@ -174,6 +176,35 @@ public FeedInfo getFeedInfo () { return this.hasFeedInfo() ? this.feedInfo.values().iterator().next() : null; } + + public static class StopTimesForTripWithTripPatternKey { + public StopTimesForTripWithTripPatternKey(String feedId, Trip trip, Service service, int routeType, List stopTimes, Trips.Pattern pattern) { + this.feedId = feedId; + this.trip = trip; + this.service = service; + this.routeType = routeType; + this.stopTimes = stopTimes; + this.pattern = pattern; + } + + public final String feedId; + public final Trip trip; + public final Service service; + public final int routeType; + public final List stopTimes; + public final Trips.Pattern pattern; + public int idx; + public int endIdxOfPattern; // exclusive + public int getDepartureTime() { + for (StopTime stopTime : stopTimes) { + if (stopTime != null) { + return stopTime.departure_time; + } + } + throw new RuntimeException(); + } + } + /** * For the given trip ID, fetch all the stop times in order of increasing stop_sequence. * This is an efficient iteration over a tree map. @@ -196,9 +227,9 @@ public Shape getShape (String shape_id) { /** * For the given trip ID, fetch all the stop times in order, and interpolate stop-to-stop travel times. */ - public Iterable getInterpolatedStopTimesForTrip (String trip_id) throws FirstAndLastStopsDoNotHaveTimes { + public List getInterpolatedStopTimesForTrip (String trip_id) throws FirstAndLastStopsDoNotHaveTimes { // clone stop times so as not to modify base GTFS structures - StopTime[] stopTimes = StreamSupport.stream(getOrderedStopTimesForTrip(trip_id).spliterator(), false) + StopTime[] stopTimes = StreamSupport.stream(Spliterators.spliteratorUnknownSize(getOrderedStopTimesForTrip(trip_id).iterator(), 0), false) .map(st -> st.clone()) .toArray(i -> new StopTime[i]); @@ -489,4 +520,9 @@ public LocalDate getCalendarDateEnd() { return endDate; } + // Utility to more efficiently stream MapDB collections -- by default, stream() expensively determines collection size + public static Stream stream(Collection collection) { + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(collection.iterator(), 0), false); + } + } diff --git a/reader-gtfs/src/main/java/com/conveyal/gtfs/model/StopTime.java b/reader-gtfs/src/main/java/com/conveyal/gtfs/model/StopTime.java index f9cee5fa03d..480c6bef77d 100644 --- a/reader-gtfs/src/main/java/com/conveyal/gtfs/model/StopTime.java +++ b/reader-gtfs/src/main/java/com/conveyal/gtfs/model/StopTime.java @@ -51,6 +51,16 @@ public class StopTime extends Entity implements Cloneable, Serializable { public double shape_dist_traveled; public int timepoint = INT_MISSING; + @Override + public String toString() { + return "StopTime{" + + "stop_sequence=" + stop_sequence + + ", arrival_time=" + arrival_time + + ", departure_time=" + departure_time + + ", stop_id='" + stop_id + '\'' + + '}'; + } + public static class Loader extends Entity.Loader { public Loader(GTFSFeed feed) { diff --git a/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphExplorer.java b/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphExplorer.java index 5eebdcec128..4d7b6782e75 100644 --- a/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphExplorer.java +++ b/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphExplorer.java @@ -160,8 +160,11 @@ private Iterable streetEdgeStream(int streetNode) { public boolean tryAdvance(Consumer action) { while (e.next()) { if (Double.isFinite(accessEgressWeighting.calcEdgeWeight(e, reverse))) { - action.accept(new MultiModalEdge(e.getEdge(), e.getBaseNode(), e.getAdjNode(), (long) (accessEgressWeighting.calcEdgeMillis(e.detach(false), reverse) * (5.0 / walkSpeedKmH)), e.getDistance())); - return true; + long travelTimeOrInfty = accessEgressWeighting.calcEdgeMillis(e.detach(false), reverse); + if (travelTimeOrInfty != Long.MAX_VALUE) { + action.accept(new MultiModalEdge(e.getEdge(), e.getBaseNode(), e.getAdjNode(), (long) (travelTimeOrInfty * (5.0 / walkSpeedKmH)), e.getDistance())); + return true; + } } } return false; diff --git a/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphHopperGtfs.java b/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphHopperGtfs.java index 8812922f913..6546252f355 100644 --- a/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphHopperGtfs.java +++ b/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphHopperGtfs.java @@ -18,7 +18,11 @@ package com.graphhopper.gtfs; +import com.conveyal.gtfs.GTFSFeed; +import com.conveyal.gtfs.model.Stop; import com.conveyal.gtfs.model.Transfer; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimaps; import com.graphhopper.GraphHopper; import com.graphhopper.GraphHopperConfig; import com.graphhopper.routing.ev.Subnetwork; @@ -37,6 +41,7 @@ import java.io.File; import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; import java.util.*; import java.util.stream.Collectors; @@ -65,10 +70,23 @@ protected void importOSM() { protected void importPublicTransit() { ptGraph = new PtGraph(getBaseGraph().getDirectory(), 100); gtfsStorage = new GtfsStorage(getBaseGraph().getDirectory()); + gtfsStorage.setPtGraph(ptGraph); LineIntIndex stopIndex = new LineIntIndex(new BBox(-180.0, 180.0, -90.0, 90.0), getBaseGraph().getDirectory(), "stop_index"); if (getGtfsStorage().loadExisting()) { ptGraph.loadExisting(); stopIndex.loadExisting(); + if (ghConfig.getBool("gtfs.trip_based", false)) { + for (String trafficDayString : ghConfig.getString("gtfs.schedule_day", null).split(",")) { + LocalDate trafficDay = LocalDate.parse(trafficDayString); + LOGGER.info("Loading trip-based transfers for pt router. Schedule day: {}", trafficDay); + gtfsStorage.tripTransfers.getTripTransfers().put(trafficDay, gtfsStorage.deserializeTripTransfersMap("trip_transfers_" + trafficDayString)); + } + for (Map.Entry entry : this.gtfsStorage.getGtfsFeeds().entrySet()) { + for (Stop stop : entry.getValue().stops.values()) { + gtfsStorage.tripTransfers.getPatternBoardings(new GtfsStorage.FeedIdWithStopId(entry.getKey(), stop.stop_id)); + } + } + } } else { ensureWriteAccess(); getGtfsStorage().create(); @@ -103,6 +121,17 @@ protected void importPublicTransit() { allReaders.put(id, gtfsReader); }); interpolateTransfers(allReaders, allTransfers); + if (ghConfig.getBool("gtfs.trip_based", false)) { + ArrayListMultimap stopsForStationNode = Multimaps.invertFrom(Multimaps.forMap(gtfsStorage.getStationNodes()), ArrayListMultimap.create()); + for (String trafficDayString : ghConfig.getString("gtfs.schedule_day", null).split(",")) { + LocalDate trafficDay = LocalDate.parse(trafficDayString); + LOGGER.info("Computing trip-based transfers for pt router. Schedule day: {}", trafficDay); + Map> tripTransfersMap = gtfsStorage.tripTransfers.getTripTransfers(trafficDay); + gtfsStorage.tripTransfers.findAllTripTransfersInto(tripTransfersMap, trafficDay, allTransfers, stopsForStationNode); + LOGGER.info("Writing. Schedule day: {}", trafficDay); + gtfsStorage.serializeTripTransfersMap("trip_transfers_" + trafficDayString, tripTransfersMap); + } + } } catch (Exception e) { throw new RuntimeException("Error while constructing transit network. Is your GTFS file valid? Please check log for possible causes.", e); } @@ -112,7 +141,6 @@ protected void importPublicTransit() { stopIndex.flush(); } gtfsStorage.setStopIndex(stopIndex); - gtfsStorage.setPtGraph(ptGraph); } private void interpolateTransfers(HashMap readers, Map allTransfers) { @@ -135,12 +163,14 @@ private void interpolateTransfers(HashMap readers, Map " + toPlatformDescriptor); if (!toPlatformDescriptor.feed_id.equals(fromPlatformDescriptor.feed_id)) { LOGGER.debug(" Different feed. Inserting transfer with " + (int) (label.streetTime / 1000L) + " s."); - insertInterpolatedTransfer(label, toPlatformDescriptor, readers); + insertInterpolatedTripTransfer(fromPlatformDescriptor, toPlatformDescriptor, (int) (label.streetTime / 1000L), getSkippedEdgesForTransfer(label)); + insertInterpolatedTransfer(label, toPlatformDescriptor, readers, getSkippedEdgesForTransfer(label)); } else { List transfersToStop = transfers.getTransfersToStop(toPlatformDescriptor.stop_id, routeIdOrNull(toPlatformDescriptor)); if (transfersToStop.stream().noneMatch(t -> t.from_stop_id.equals(fromPlatformDescriptor.stop_id))) { LOGGER.debug(" Inserting transfer with " + (int) (label.streetTime / 1000L) + " s."); - insertInterpolatedTransfer(label, toPlatformDescriptor, readers); + insertInterpolatedTripTransfer(fromPlatformDescriptor, toPlatformDescriptor, (int) (label.streetTime / 1000L), getSkippedEdgesForTransfer(label)); + insertInterpolatedTransfer(label, toPlatformDescriptor, readers, getSkippedEdgesForTransfer(label)); } } } @@ -151,15 +181,16 @@ private void interpolateTransfers(HashMap readers, Map readers) { + + private void insertInterpolatedTripTransfer(GtfsStorage.PlatformDescriptor fromPlatformDescriptor, GtfsStorage.PlatformDescriptor toPlatformDescriptor, int streetTime, int[] skippedEdgesForTransfer) { + if (skippedEdgesForTransfer.length > 0) { // TODO: Elsewhere, we distinguish empty path ("at" a node) from no path + gtfsStorage.interpolatedTransfers.put(new GtfsStorage.FeedIdWithStopId(fromPlatformDescriptor.feed_id, fromPlatformDescriptor.stop_id), new GtfsStorage.InterpolatedTransfer(new GtfsStorage.FeedIdWithStopId(toPlatformDescriptor.feed_id, toPlatformDescriptor.stop_id), streetTime, skippedEdgesForTransfer)); + } + } + + private void insertInterpolatedTransfer(Label label, GtfsStorage.PlatformDescriptor toPlatformDescriptor, HashMap readers, int[] skippedEdgesForTransfer) { GtfsReader toFeedReader = readers.get(toPlatformDescriptor.feed_id); List transferEdgeIds = toFeedReader.insertTransferEdges(label.node.ptNode, (int) (label.streetTime / 1000L), toPlatformDescriptor); - List transitions = Label.getTransitions(label.parent, true); - int[] skippedEdgesForTransfer = transitions.stream().filter(t -> t.edge != null).mapToInt(t -> { - Label.NodeId adjNode = t.label.node; - EdgeIteratorState edgeIteratorState = getBaseGraph().getEdgeIteratorState(t.edge.getId(), adjNode.streetNode); - return edgeIteratorState.getEdgeKey(); - }).toArray(); if (skippedEdgesForTransfer.length > 0) { // TODO: Elsewhere, we distinguish empty path ("at" a node) from no path assert isValidPath(skippedEdgesForTransfer); for (Integer transferEdgeId : transferEdgeIds) { @@ -168,6 +199,16 @@ private void insertInterpolatedTransfer(Label label, GtfsStorage.PlatformDescrip } } + private int[] getSkippedEdgesForTransfer(Label label) { + List transitions = Label.getTransitions(label.parent, true); + int[] skippedEdgesForTransfer = transitions.stream().filter(t -> t.edge != null).mapToInt(t -> { + Label.NodeId adjNode = t.label.node; + EdgeIteratorState edgeIteratorState = getBaseGraph().getEdgeIteratorState(t.edge.getId(), adjNode.streetNode); + return edgeIteratorState.getEdgeKey(); + }).toArray(); + return skippedEdgesForTransfer; + } + private boolean isValidPath(int[] edgeKeys) { List edges = Arrays.stream(edgeKeys).mapToObj(i -> getBaseGraph().getEdgeIteratorStateForKey(i)).collect(Collectors.toList()); for (int i = 1; i < edges.size(); i++) { diff --git a/reader-gtfs/src/main/java/com/graphhopper/gtfs/GtfsReader.java b/reader-gtfs/src/main/java/com/graphhopper/gtfs/GtfsReader.java index 06864413759..317f9fca466 100644 --- a/reader-gtfs/src/main/java/com/graphhopper/gtfs/GtfsReader.java +++ b/reader-gtfs/src/main/java/com/graphhopper/gtfs/GtfsReader.java @@ -240,14 +240,18 @@ void wireUpAdditionalDeparturesAndArrivals(ZoneId zoneId) { private void addTrips(ZoneId zoneId, List trips, int time, boolean frequencyBased) { List arrivalNodes = new ArrayList<>(); for (TripWithStopTimes trip : trips) { - GtfsRealtime.TripDescriptor.Builder tripDescriptor = GtfsRealtime.TripDescriptor.newBuilder() - .setTripId(trip.trip.trip_id) - .setRouteId(trip.trip.route_id); - if (frequencyBased) { - tripDescriptor = tripDescriptor.setStartTime(convertToGtfsTime(time)); - } - addTrip(zoneId, time, arrivalNodes, trip, tripDescriptor.build()); + addTrip(zoneId, time, arrivalNodes, trip, getTripDescriptor(time, frequencyBased, trip)); + } + } + + private static GtfsRealtime.TripDescriptor getTripDescriptor(int time, boolean frequencyBased, TripWithStopTimes trip) { + GtfsRealtime.TripDescriptor.Builder tripDescriptor = GtfsRealtime.TripDescriptor.newBuilder() + .setTripId(trip.trip.trip_id) + .setRouteId(trip.trip.route_id); + if (frequencyBased) { + tripDescriptor = tripDescriptor.setStartTime(convertToGtfsTime(time)); } + return tripDescriptor.build(); } private static class TripWithStopTimeAndArrivalNode { diff --git a/reader-gtfs/src/main/java/com/graphhopper/gtfs/GtfsStorage.java b/reader-gtfs/src/main/java/com/graphhopper/gtfs/GtfsStorage.java index 01f87aba735..286d2dfa66d 100644 --- a/reader-gtfs/src/main/java/com/graphhopper/gtfs/GtfsStorage.java +++ b/reader-gtfs/src/main/java/com/graphhopper/gtfs/GtfsStorage.java @@ -24,6 +24,13 @@ import com.carrotsearch.hppc.cursors.IntObjectCursor; import com.conveyal.gtfs.GTFSFeed; import com.conveyal.gtfs.model.Fare; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.MappingIterator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SequenceWriter; +import com.google.common.collect.HashMultimap; import com.graphhopper.storage.Directory; import com.graphhopper.storage.index.LineIntIndex; import org.mapdb.DB; @@ -41,8 +48,12 @@ public class GtfsStorage { private static final Logger LOGGER = LoggerFactory.getLogger(GtfsStorage.class); + + static ObjectMapper ionMapper = new ObjectMapper(); + private LineIntIndex stopIndex; private PtGraph ptGraph; + public Trips tripTransfers; public void setStopIndex(LineIntIndex stopIndex) { this.stopIndex = stopIndex; @@ -88,8 +99,8 @@ public int hashCode() { } } - static class FeedIdWithTimezone implements Serializable { - final String feedId; + public static class FeedIdWithTimezone implements Serializable { + public final String feedId; final ZoneId zoneId; FeedIdWithTimezone(String feedId, ZoneId zoneId) { @@ -112,10 +123,15 @@ public int hashCode() { } public static class FeedIdWithStopId implements Serializable { + + @JsonProperty("feed_id") public final String feedId; + + @JsonProperty("stop_id") public final String stopId; - public FeedIdWithStopId(String feedId, String stopId) { + public FeedIdWithStopId(@JsonProperty("feed_id") String feedId, + @JsonProperty("stop_id") String stopId) { this.feedId = feedId; this.stopId = stopId; } @@ -158,9 +174,9 @@ public enum EdgeType { HIGHWAY, ENTER_TIME_EXPANDED_NETWORK, LEAVE_TIME_EXPANDED_NETWORK, ENTER_PT, EXIT_PT, HOP, DWELL, BOARD, ALIGHT, OVERNIGHT, TRANSFER, WAIT, WAIT_ARRIVAL } - private DB data; + public DB data; - GtfsStorage(Directory dir) { + public GtfsStorage(Directory dir) { this.dir = dir; } @@ -171,27 +187,43 @@ boolean loadExisting() { } this.data = DBMaker.newFileDB(file).transactionDisable().mmapFileEnable().readOnly().make(); init(); - for (String gtfsFeedId : this.gtfsFeedIds) { - File dbFile = new File(dir.getLocation() + "/" + gtfsFeedId); - - if (!dbFile.exists()) { - throw new RuntimeException(String.format("The mapping of the gtfsFeeds in the transit_schedule DB does not reflect the files in %s. " - + "dbFile %s is missing.", - dir.getLocation(), dbFile.getName())); - } - - GTFSFeed feed = new GTFSFeed(dbFile); - this.gtfsFeeds.put(gtfsFeedId, feed); - } - ptToStreet = deserialize("pt_to_street"); - streetToPt = deserialize("street_to_pt"); + for (int i = 0; i < gtfsFeedIds.size(); i++) { + String gtfsFeedId = "gtfs_" + i; + File dbFile = new File(dir.getLocation() + "/" + gtfsFeedId); + + if (!dbFile.exists()) { + throw new RuntimeException(String.format("The mapping of the gtfsFeeds in the transit_schedule DB does not reflect the files in %s. " + + "dbFile %s is missing.", + dir.getLocation(), dbFile.getName())); + } + + GTFSFeed feed = new GTFSFeed(dbFile); + this.gtfsFeeds.put(gtfsFeedId, feed); + } + ptToStreet = deserializeIntoIntIntHashMap("pt_to_street"); + streetToPt = deserializeIntoIntIntHashMap("street_to_pt"); skippedEdgesForTransfer = deserializeIntoIntObjectHashMap("skipped_edges_for_transfer"); - postInit(); + try (InputStream is = Files.newInputStream(Paths.get(dir.getLocation() + "interpolated_transfers"))) { + MappingIterator objectMappingIterator = ionMapper.reader(JsonNode.class).readValues(is); + objectMappingIterator.forEachRemaining(e -> { + try { + FeedIdWithStopId key = ionMapper.treeToValue(e.get(0), FeedIdWithStopId.class); + for (JsonNode jsonNode : e.get(1)) { + InterpolatedTransfer interpolatedTransfer = ionMapper.treeToValue(jsonNode, InterpolatedTransfer.class); + interpolatedTransfers.put(key, interpolatedTransfer); + } + } catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + postInit(); return true; } - - private IntIntHashMap deserialize(String filename) { + private IntIntHashMap deserializeIntoIntIntHashMap(String filename) { try (FileInputStream in = new FileInputStream(dir.getLocation() + filename)) { ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(in)); int size = ois.readInt(); @@ -205,16 +237,22 @@ private IntIntHashMap deserialize(String filename) { } } - private IntObjectHashMap deserializeIntoIntObjectHashMap(String filename) { + public IntObjectHashMap deserializeIntoIntObjectHashMap(String filename) { try (FileInputStream in = new FileInputStream(dir.getLocation() + filename)) { ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(in)); int size = ois.readInt(); - IntObjectHashMap result = new IntObjectHashMap<>(); + IntObjectHashMap result = new IntObjectHashMap<>(size); for (int i = 0; i < size; i++) { - result.put(ois.readInt(), ((int[]) ois.readObject())); + int key = ois.readInt(); + int n = ois.readInt(); + int[] ints = new int[n]; + for (int j = 0; j < n; j++) { + ints[j] = ois.readInt(); + } + result.put(key, ints); } return result; - } catch (IOException | ClassNotFoundException e) { + } catch (IOException e) { throw new RuntimeException(e); } } @@ -259,6 +297,7 @@ public void postInit() { LOGGER.info("Calendar range covered by all feeds: {} till {}", latestStartDate, earliestEndDate); faresByFeed = new HashMap<>(); this.gtfsFeeds.forEach((feed_id, feed) -> faresByFeed.put(feed_id, feed.fares)); + tripTransfers = new Trips(this); } public void close() { @@ -295,14 +334,65 @@ public void flush() { serialize("pt_to_street", ptToStreet); serialize("street_to_pt", streetToPt); serialize("skipped_edges_for_transfer", skippedEdgesForTransfer); + try (OutputStream os = Files.newOutputStream(Paths.get(dir.getLocation() + "interpolated_transfers"))) { + SequenceWriter sequenceWriter = ionMapper.writer().writeValuesAsArray(os); + for (Map.Entry> e : interpolatedTransfers.asMap().entrySet()) { + sequenceWriter.write(ionMapper.createArrayNode().addPOJO(e.getKey()).addPOJO(e.getValue())); + } + sequenceWriter.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void serializeTripTransfersMap(String filename, Map> data) { + try (ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(Files.newOutputStream(Paths.get(dir.getLocation() + filename))))) { + oos.writeInt(data.size()); + for (Map.Entry> entry : data.entrySet()) { + oos.writeInt(entry.getKey().tripIdx); + oos.writeInt(entry.getKey().stop_sequence); + oos.writeInt(entry.getValue().size()); + for (Trips.TripAtStopTime tripAtStopTime : entry.getValue()) { + oos.writeInt(tripAtStopTime.tripIdx); + oos.writeInt(tripAtStopTime.stop_sequence); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } } - private void serialize(String filename, IntObjectHashMap data) { + public Map> deserializeTripTransfersMap(String filename) { + try (FileInputStream in = new FileInputStream(dir.getLocation() + filename)) { + ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(in)); + int size = ois.readInt(); + Map> result = new TreeMap<>(); + for (int i = 0; i < size; i++) { + Trips.TripAtStopTime origin = new Trips.TripAtStopTime(ois.readInt(), ois.readInt()); + int nDestinations = ois.readInt(); + List destinations = new ArrayList<>(nDestinations); + for (int j = 0; j < nDestinations; j++) { + int tripIdxTo = ois.readInt(); + int stop_sequenceTo = ois.readInt(); + destinations.add(new Trips.TripAtStopTime(tripIdxTo, stop_sequenceTo)); + } + result.put(origin, destinations); + } + return result; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void serialize(String filename, IntObjectHashMap data) { try (ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(Files.newOutputStream(Paths.get(dir.getLocation() + filename))))) { oos.writeInt(data.size()); for (IntObjectCursor e : data) { oos.writeInt(e.key); - oos.writeObject(e.value); + oos.writeInt(e.value.length); + for (int v : e.value) { + oos.writeInt(v); + } } } catch (IOException e) { throw new RuntimeException(e); @@ -410,4 +500,28 @@ public String toString() { '}'; } } + + public HashMultimap interpolatedTransfers = HashMultimap.create(); + + + public static class InterpolatedTransfer { + + @JsonProperty("to_stop") + public final FeedIdWithStopId toPlatformDescriptor; + + @JsonProperty("street_time") + public final int streetTime; + + @JsonProperty("skipped_edges") + public final int[] skippedEdgesForTransfer; + + public InterpolatedTransfer(@JsonProperty("to_stop") FeedIdWithStopId toPlatformDescriptor, + @JsonProperty("street_time") int streetTime, + @JsonProperty("skipped_edges") int[] skippedEdgesForTransfer) { + this.toPlatformDescriptor = toPlatformDescriptor; + this.streetTime = streetTime; + this.skippedEdgesForTransfer = skippedEdgesForTransfer; + } + } + } diff --git a/reader-gtfs/src/main/java/com/graphhopper/gtfs/PtEdgeAttributes.java b/reader-gtfs/src/main/java/com/graphhopper/gtfs/PtEdgeAttributes.java index 65137bedcbc..3f89fdc643b 100644 --- a/reader-gtfs/src/main/java/com/graphhopper/gtfs/PtEdgeAttributes.java +++ b/reader-gtfs/src/main/java/com/graphhopper/gtfs/PtEdgeAttributes.java @@ -20,6 +20,7 @@ public String toString() { "type=" + type + ", time=" + time + ", transfers=" + transfers + + ", tripDescriptor=" + tripDescriptor + '}'; } diff --git a/reader-gtfs/src/main/java/com/graphhopper/gtfs/PtRouterTripBasedImpl.java b/reader-gtfs/src/main/java/com/graphhopper/gtfs/PtRouterTripBasedImpl.java new file mode 100644 index 00000000000..dc407ec7ec1 --- /dev/null +++ b/reader-gtfs/src/main/java/com/graphhopper/gtfs/PtRouterTripBasedImpl.java @@ -0,0 +1,382 @@ +/* + * Licensed to GraphHopper GmbH under one or more contributor + * license agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * GraphHopper GmbH licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.graphhopper.gtfs; + +import com.conveyal.gtfs.GTFSFeed; +import com.conveyal.gtfs.model.Stop; +import com.graphhopper.*; +import com.graphhopper.config.Profile; +import com.graphhopper.routing.DefaultWeightingFactory; +import com.graphhopper.routing.WeightingFactory; +import com.graphhopper.routing.ev.Subnetwork; +import com.graphhopper.routing.querygraph.QueryGraph; +import com.graphhopper.routing.util.DefaultSnapFilter; +import com.graphhopper.routing.util.EdgeFilter; +import com.graphhopper.routing.util.EncodingManager; +import com.graphhopper.routing.weighting.Weighting; +import com.graphhopper.storage.BaseGraph; +import com.graphhopper.storage.index.LocationIndex; +import com.graphhopper.util.PMap; +import com.graphhopper.util.StopWatch; +import com.graphhopper.util.Translation; +import com.graphhopper.util.TranslationMap; +import com.graphhopper.util.details.PathDetailsBuilderFactory; +import com.graphhopper.util.exceptions.ConnectionNotFoundException; +import com.graphhopper.util.exceptions.MaximumNodesExceededException; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public final class PtRouterTripBasedImpl implements PtRouter { + + private static final Logger logger = LoggerFactory.getLogger(PtRouterTripBasedImpl.class); + + private final GraphHopperConfig config; + private final TranslationMap translationMap; + private final BaseGraph baseGraph; + private final EncodingManager encodingManager; + private final LocationIndex locationIndex; + private final GtfsStorage gtfsStorage; + private final PtGraph ptGraph; + private final PathDetailsBuilderFactory pathDetailsBuilderFactory; + private final WeightingFactory weightingFactory; + private final Map feedZoneIds = new ConcurrentHashMap<>(); // ad-hoc cache for timezone field of gtfs feed + private final GraphHopper graphHopper; + + @Inject + public PtRouterTripBasedImpl(GraphHopper graphHopper, GraphHopperConfig config, TranslationMap translationMap, BaseGraph baseGraph, EncodingManager encodingManager, LocationIndex locationIndex, GtfsStorage gtfsStorage, PathDetailsBuilderFactory pathDetailsBuilderFactory) { + this.graphHopper = graphHopper; + this.config = config; + this.weightingFactory = new DefaultWeightingFactory(baseGraph, encodingManager); + this.translationMap = translationMap; + this.baseGraph = baseGraph; + this.encodingManager = encodingManager; + this.locationIndex = locationIndex; + this.gtfsStorage = gtfsStorage; + this.ptGraph = gtfsStorage.getPtGraph(); + this.pathDetailsBuilderFactory = pathDetailsBuilderFactory; + } + + @Override + public GHResponse route(Request request) { + return new RequestHandler(request).route(); + } + + private class RequestHandler { + private final int maxVisitedNodesForRequest; + private final int limitSolutions; + private final Duration maxProfileDuration; + private final Instant initialTime; + private final boolean profileQuery; + private final boolean arriveBy; + private final boolean ignoreTransfers; + private final double betaTransfers; + private final double betaStreetTime; + private final double walkSpeedKmH; + private final int blockedRouteTypes; + private final Map transferPenaltiesByRouteType; + private final GHLocation enter; + private final GHLocation exit; + private final Translation translation; + private final List requestedPathDetails; + + private final GHResponse response = new GHResponse(); + private final long limitTripTime; + private final long limitStreetTime; + private final double betaAccessTime; + private final double betaEgressTime; + private QueryGraph queryGraph; + private int visitedNodes; + private final Profile accessProfile; + private final EdgeFilter accessSnapFilter; + private final Weighting accessWeighting; + private final Profile egressProfile; + private final EdgeFilter egressSnapFilter; + private final Weighting egressWeighting; + private TripFromLabel tripFromLabel; + private List