diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java b/application/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java index f84fa51f67b..a289e0c8ded 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java @@ -25,6 +25,7 @@ import org.opentripplanner.routing.algorithm.mapping.GraphPathToItineraryMapper; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graphfinder.NearbyStop; +import org.opentripplanner.service.streetdetails.StreetDetailsService; import org.opentripplanner.street.model.vertex.TransitStopVertex; import org.opentripplanner.transit.api.request.TripRequest; import org.opentripplanner.transit.model.filter.expr.Matcher; @@ -61,6 +62,7 @@ public class FlexRouter { public FlexRouter( Graph graph, TransitService transitService, + StreetDetailsService streetDetailsService, FlexParameters flexParameters, TripRequest filterRequest, Instant requestedTime, @@ -85,6 +87,7 @@ public FlexRouter( transitService::getRegularStop, transitService.getTimeZone(), graph.streetNotesService, + streetDetailsService, graph.ellipsoidToGeoidDifference ); diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/SchemaFactory.java b/application/src/main/java/org/opentripplanner/apis/gtfs/SchemaFactory.java index 47cde5de6e0..07d05ae419d 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/SchemaFactory.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/SchemaFactory.java @@ -26,6 +26,7 @@ import org.opentripplanner.apis.gtfs.datafetchers.DepartureRowImpl; import org.opentripplanner.apis.gtfs.datafetchers.DependentFareProductImpl; import org.opentripplanner.apis.gtfs.datafetchers.EntranceImpl; +import org.opentripplanner.apis.gtfs.datafetchers.EscalatorUseImpl; import org.opentripplanner.apis.gtfs.datafetchers.EstimatedTimeImpl; import org.opentripplanner.apis.gtfs.datafetchers.FareProductTypeResolver; import org.opentripplanner.apis.gtfs.datafetchers.FareProductUseImpl; @@ -54,6 +55,7 @@ import org.opentripplanner.apis.gtfs.datafetchers.RouteImpl; import org.opentripplanner.apis.gtfs.datafetchers.RouteTypeImpl; import org.opentripplanner.apis.gtfs.datafetchers.RoutingErrorImpl; +import org.opentripplanner.apis.gtfs.datafetchers.StairsUseImpl; import org.opentripplanner.apis.gtfs.datafetchers.StepFeatureTypeResolver; import org.opentripplanner.apis.gtfs.datafetchers.StopCallImpl; import org.opentripplanner.apis.gtfs.datafetchers.StopGeometriesImpl; @@ -208,6 +210,8 @@ public static GraphQLSchema createSchema() { .type(typeWiring.build(EstimatedTimeImpl.class)) .type(typeWiring.build(EntranceImpl.class)) .type(typeWiring.build(RentalVehicleFuelImpl.class)) + .type(typeWiring.build(EscalatorUseImpl.class)) + .type(typeWiring.build(StairsUseImpl.class)) .build(); SchemaGenerator schemaGenerator = new SchemaGenerator(); diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EscalatorUseImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EscalatorUseImpl.java new file mode 100644 index 00000000000..77c041a8b30 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EscalatorUseImpl.java @@ -0,0 +1,35 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.schema.DataFetcher; +import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLVerticalDirection; +import org.opentripplanner.apis.gtfs.mapping.VerticalDirectionMapper; +import org.opentripplanner.model.plan.walkstep.verticaltransportation.EscalatorUse; +import org.opentripplanner.service.streetdetails.model.Level; + +public class EscalatorUseImpl implements GraphQLDataFetchers.GraphQLEscalatorUse { + + @Override + public DataFetcher from() { + return environment -> { + EscalatorUse escalatorUse = environment.getSource(); + return escalatorUse.from(); + }; + } + + @Override + public DataFetcher to() { + return environment -> { + EscalatorUse escalatorUse = environment.getSource(); + return escalatorUse.to(); + }; + } + + @Override + public DataFetcher verticalDirection() { + return environment -> { + EscalatorUse escalatorUse = environment.getSource(); + return VerticalDirectionMapper.map(escalatorUse.verticalDirection()); + }; + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StairsUseImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StairsUseImpl.java new file mode 100644 index 00000000000..0621e71e40a --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StairsUseImpl.java @@ -0,0 +1,35 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.schema.DataFetcher; +import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLVerticalDirection; +import org.opentripplanner.apis.gtfs.mapping.VerticalDirectionMapper; +import org.opentripplanner.model.plan.walkstep.verticaltransportation.StairsUse; +import org.opentripplanner.service.streetdetails.model.Level; + +public class StairsUseImpl implements GraphQLDataFetchers.GraphQLStairsUse { + + @Override + public DataFetcher from() { + return environment -> { + StairsUse stairsUse = environment.getSource(); + return stairsUse.from(); + }; + } + + @Override + public DataFetcher to() { + return environment -> { + StairsUse stairsUse = environment.getSource(); + return stairsUse.to(); + }; + } + + @Override + public DataFetcher verticalDirection() { + return environment -> { + StairsUse stairsUse = environment.getSource(); + return VerticalDirectionMapper.map(stairsUse.verticalDirection()); + }; + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepFeatureTypeResolver.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepFeatureTypeResolver.java index 714518cb9ea..2426a6d0151 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepFeatureTypeResolver.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepFeatureTypeResolver.java @@ -4,6 +4,8 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.TypeResolver; +import org.opentripplanner.model.plan.walkstep.verticaltransportation.EscalatorUse; +import org.opentripplanner.model.plan.walkstep.verticaltransportation.StairsUse; import org.opentripplanner.transit.model.site.Entrance; public class StepFeatureTypeResolver implements TypeResolver { @@ -15,6 +17,10 @@ public GraphQLObjectType getType(TypeResolutionEnvironment environment) { if (o instanceof Entrance) { return schema.getObjectType("Entrance"); + } else if (o instanceof EscalatorUse) { + return schema.getObjectType("EscalatorUse"); + } else if (o instanceof StairsUse) { + return schema.getObjectType("StairsUse"); } return null; } diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/stepImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/stepImpl.java index 9ec738e0e3e..bafdc08c810 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/stepImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/stepImpl.java @@ -10,6 +10,9 @@ import org.opentripplanner.apis.gtfs.mapping.StreetNoteMapper; import org.opentripplanner.model.plan.leg.ElevationProfile.Step; import org.opentripplanner.model.plan.walkstep.WalkStep; +import org.opentripplanner.model.plan.walkstep.verticaltransportation.EscalatorUse; +import org.opentripplanner.model.plan.walkstep.verticaltransportation.StairsUse; +import org.opentripplanner.model.plan.walkstep.verticaltransportation.VerticalTransportationUse; import org.opentripplanner.routing.alertpatch.TransitAlert; public class stepImpl implements GraphQLDataFetchers.GraphQLStep { @@ -57,7 +60,22 @@ public DataFetcher exit() { @Override public DataFetcher feature() { - return environment -> getSource(environment).entrance().orElse(null); + return environment -> { + WalkStep walkStep = getSource(environment); + if (walkStep.entrance().isPresent()) { + return walkStep.entrance().get(); + } else if (walkStep.verticalTransportationUse().isPresent()) { + VerticalTransportationUse verticalTransportationUse = walkStep + .verticalTransportationUse() + .get(); + if (verticalTransportationUse instanceof EscalatorUse escalatorUse) { + return escalatorUse; + } else if (verticalTransportationUse instanceof StairsUse stairsUse) { + return stairsUse; + } + } + return null; + }; } @Override diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java index 410db677ebb..82c2019ab91 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java @@ -23,6 +23,7 @@ import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLRelativeDirection; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLRoutingErrorCode; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLTransitMode; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLVerticalDirection; import org.opentripplanner.apis.gtfs.model.CallRealTime; import org.opentripplanner.apis.gtfs.model.CallSchedule; import org.opentripplanner.apis.gtfs.model.CallScheduledTime; @@ -54,6 +55,7 @@ import org.opentripplanner.routing.graphfinder.PlaceAtDistance; import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle.StopRelationship; +import org.opentripplanner.service.streetdetails.model.Level; import org.opentripplanner.service.vehicleparking.model.VehicleParking; import org.opentripplanner.service.vehicleparking.model.VehicleParkingSpaces; import org.opentripplanner.service.vehicleparking.model.VehicleParkingState; @@ -389,6 +391,13 @@ public interface GraphQLDepartureRow { public DataFetcher> stoptimes(); } + /** + * A (possibly discounted) fare product that requires another fare product to be purchased previously + * in order to be valid. + * + * For example, when taking the train into a city, you might get a discounted "transfer fare" when + * switching to the bus for the second leg. + */ public interface GraphQLDependentFareProduct { public DataFetcher> dependencies(); @@ -420,6 +429,15 @@ public interface GraphQLEntrance { > wheelchairAccessible(); } + /** A single use of an escalator. */ + public interface GraphQLEscalatorUse { + public DataFetcher from(); + + public DataFetcher to(); + + public DataFetcher verticalDirection(); + } + /** Real-time estimates for an arrival or departure at a certain place. */ public interface GraphQLEstimatedTime { public DataFetcher delay(); @@ -614,6 +632,13 @@ public interface GraphQLLegTime { public DataFetcher scheduledTime(); } + /** A level with a name and comparable number. Levels can sometimes contain half levels, e.g. '1.5'. */ + public interface GraphQLLevel { + public DataFetcher level(); + + public DataFetcher name(); + } + /** A span of time. */ public interface GraphQLLocalTimeSpan { public DataFetcher from(); @@ -642,7 +667,7 @@ public interface GraphQLLocation { } /** - * A group of fixed stops that are visited in an arbitrary order. + * A group of fixed stops that is visited in an arbitrary order. * * This is mostly used by demand-responsive services. */ @@ -1099,6 +1124,15 @@ public interface GraphQLRoutingError { public DataFetcher inputField(); } + /** A single use of a set of stairs. */ + public interface GraphQLStairsUse { + public DataFetcher from(); + + public DataFetcher to(); + + public DataFetcher verticalDirection(); + } + /** A feature for a step */ public interface GraphQLStepFeature extends TypeResolver {} @@ -1281,7 +1315,7 @@ public interface GraphQLTicketType { public DataFetcher> zones(); } - /** A time window when a vehicle visit a stop, area or group of stops. */ + /** A time window when a vehicle visits a stop, area or group of stops. */ public interface GraphQLTimeWindow { public DataFetcher end(); diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java index 55b67cf06b1..6469c220ea6 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java @@ -777,6 +777,15 @@ public void setGraphQLFilter(GraphQLDependentFareProductFilter filter) { } } + /** + * Dependent fare products can lead to many combinations of fares, however it is often not useful + * information to the passenger. + * + * This enum allows filtering of the dependencies. + * + * Since it is recognised that this is not covered well in the specification, it is discussed here: + * https://github.com/google/transit/pull/423 + */ public enum GraphQLDependentFareProductFilter { ALL, MATCH_CATEGORY_AND_MEDIUM, @@ -4452,6 +4461,7 @@ public enum GraphQLRelativeDirection { DEPART, ELEVATOR, ENTER_STATION, + ESCALATOR, EXIT_STATION, FOLLOW_SIGNS, HARD_LEFT, @@ -4460,6 +4470,7 @@ public enum GraphQLRelativeDirection { RIGHT, SLIGHTLY_LEFT, SLIGHTLY_RIGHT, + STAIRS, UTURN_LEFT, UTURN_RIGHT, } @@ -5683,6 +5694,12 @@ public enum GraphQLVertexType { TRANSIT, } + /** The vertical direction e.g. for a set of stairs. */ + public enum GraphQLVerticalDirection { + DOWN, + UP, + } + public static class GraphQLWalkPreferencesInput { private org.opentripplanner.framework.model.Cost boardCost; diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml index 129a527b750..fcd38f80964 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml @@ -74,6 +74,7 @@ config: Itinerary: org.opentripplanner.model.plan.Itinerary#Itinerary Leg: org.opentripplanner.model.plan.Leg#Leg LegTime: org.opentripplanner.model.plan.leg.LegCallTime#LegCallTime + Level: org.opentripplanner.service.streetdetails.model.Level#Level Mode: String OccupancyStatus: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLOccupancyStatus#GraphQLOccupancyStatus TransitMode: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLTransitMode#GraphQLTransitMode @@ -112,6 +113,7 @@ config: VehicleParkingSpaces: org.opentripplanner.service.vehicleparking.model.VehicleParkingSpaces#VehicleParkingSpaces VehicleParkingState: org.opentripplanner.service.vehicleparking.model.VehicleParkingState#VehicleParkingState VertexType: String + VerticalDirection: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLVerticalDirection#GraphQLVerticalDirection SystemNotice: org.opentripplanner.model.SystemNotice#SystemNotice VehiclePosition: org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle#RealtimeVehicle StopRelationship: org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle.StopRelationship#StopRelationship diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/DirectionMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/DirectionMapper.java index 562a0d25add..33d613e4035 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/DirectionMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/DirectionMapper.java @@ -33,6 +33,8 @@ public static GraphQLRelativeDirection map(RelativeDirection relativeDirection) case CIRCLE_CLOCKWISE -> GraphQLRelativeDirection.CIRCLE_CLOCKWISE; case CIRCLE_COUNTERCLOCKWISE -> GraphQLRelativeDirection.CIRCLE_COUNTERCLOCKWISE; case ELEVATOR -> GraphQLRelativeDirection.ELEVATOR; + case ESCALATOR -> GraphQLRelativeDirection.ESCALATOR; + case STAIRS -> GraphQLRelativeDirection.STAIRS; case UTURN_LEFT -> GraphQLRelativeDirection.UTURN_LEFT; case UTURN_RIGHT -> GraphQLRelativeDirection.UTURN_RIGHT; case ENTER_STATION -> GraphQLRelativeDirection.ENTER_STATION; diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/VerticalDirectionMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/VerticalDirectionMapper.java new file mode 100644 index 00000000000..da912abdfc4 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/VerticalDirectionMapper.java @@ -0,0 +1,14 @@ +package org.opentripplanner.apis.gtfs.mapping; + +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLVerticalDirection; +import org.opentripplanner.model.plan.walkstep.verticaltransportation.VerticalDirection; + +public class VerticalDirectionMapper { + + public static GraphQLVerticalDirection map(VerticalDirection verticalDirection) { + return switch (verticalDirection) { + case DOWN -> GraphQLVerticalDirection.DOWN; + case UP -> GraphQLVerticalDirection.UP; + }; + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/RelativeDirectionMapper.java b/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/RelativeDirectionMapper.java index 64c4f79b18b..c50be55d010 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/RelativeDirectionMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/RelativeDirectionMapper.java @@ -21,6 +21,8 @@ public static RelativeDirection map(RelativeDirection relativeDirection) { CIRCLE_CLOCKWISE, CIRCLE_COUNTERCLOCKWISE, ELEVATOR, + ESCALATOR, + STAIRS, UTURN_LEFT, UTURN_RIGHT, ENTER_STATION, diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java b/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java index b4dfb38e290..dc7fe208d51 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java @@ -269,6 +269,8 @@ public class EnumTypes { .value("circleClockwise", RelativeDirection.CIRCLE_CLOCKWISE) .value("circleCounterclockwise", RelativeDirection.CIRCLE_COUNTERCLOCKWISE) .value("elevator", RelativeDirection.ELEVATOR) + .value("escalator", RelativeDirection.ESCALATOR) + .value("stairs", RelativeDirection.STAIRS) .value("uturnLeft", RelativeDirection.UTURN_LEFT) .value("uturnRight", RelativeDirection.UTURN_RIGHT) .value("enterStation", RelativeDirection.ENTER_STATION) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java b/application/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java index 8b50642b49f..77ea4e61e4c 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java @@ -24,6 +24,7 @@ import org.opentripplanner.routing.fares.FareServiceFactory; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.worldenvelope.WorldEnvelopeRepository; import org.opentripplanner.standalone.config.BuildConfig; @@ -75,6 +76,7 @@ public static GraphBuilder create( GraphBuilderDataSources dataSources, Graph graph, OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, + StreetDetailsRepository streetDetailsRepository, FareServiceFactory fareServiceFactory, TimetableRepository timetableRepository, WorldEnvelopeRepository worldEnvelopeRepository, @@ -98,6 +100,7 @@ public static GraphBuilder create( .config(config) .graph(graph) .osmInfoGraphBuildRepository(osmInfoGraphBuildRepository) + .streetDetailsRepository(streetDetailsRepository) .timetableRepository(timetableRepository) .worldEnvelopeRepository(worldEnvelopeRepository) .vehicleParkingRepository(vehicleParkingService) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/issues/ContradictoryLevelAndInclineInfoForWay.java b/application/src/main/java/org/opentripplanner/graph_builder/issues/ContradictoryLevelAndInclineInfoForWay.java new file mode 100644 index 00000000000..ba94350f630 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/graph_builder/issues/ContradictoryLevelAndInclineInfoForWay.java @@ -0,0 +1,24 @@ +package org.opentripplanner.graph_builder.issues; + +import org.opentripplanner.graph_builder.issue.api.DataImportIssue; +import org.opentripplanner.osm.model.OsmWay; + +public record ContradictoryLevelAndInclineInfoForWay(OsmWay way) implements DataImportIssue { + private static final String FMT = + "Way %s has contradictory level information in the 'incline' and 'level'/'layer' tags. " + + "Please verify that the tags indicate the same vertical direction."; + + private static final String HTMLFMT = + "Way %s has contradictory level information in the 'incline' and 'level'/'layer' tags. " + + "Please verify that the tags indicate the same vertical direction."; + + @Override + public String getMessage() { + return String.format(FMT, way.getId()); + } + + @Override + public String getHTMLMessage() { + return String.format(HTMLFMT, way.url(), way.getId()); + } +} diff --git a/application/src/main/java/org/opentripplanner/graph_builder/issues/CouldNotApplyMultiLevelInfoToWay.java b/application/src/main/java/org/opentripplanner/graph_builder/issues/CouldNotApplyMultiLevelInfoToWay.java new file mode 100644 index 00000000000..617de4d6c8f --- /dev/null +++ b/application/src/main/java/org/opentripplanner/graph_builder/issues/CouldNotApplyMultiLevelInfoToWay.java @@ -0,0 +1,26 @@ +package org.opentripplanner.graph_builder.issues; + +import org.opentripplanner.graph_builder.issue.api.DataImportIssue; +import org.opentripplanner.osm.model.OsmWay; + +public record CouldNotApplyMultiLevelInfoToWay(OsmWay way, int nodes) implements DataImportIssue { + private static final String FMT = + "Multi-level info for way %s can not be used because node references did not match. " + + "This is probably caused by more than 2 intersection nodes in the way. " + + "The way has %s nodes in total."; + + private static final String HTMLFMT = + "Multi-level info for way %s can not be used because node references did not match. " + + "This is probably caused by more than 2 intersection nodes in the way. " + + "The way has %s nodes in total."; + + @Override + public String getMessage() { + return String.format(FMT, way.getId(), nodes); + } + + @Override + public String getHTMLMessage() { + return String.format(HTMLFMT, way.url(), way.getId(), nodes); + } +} diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderFactory.java b/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderFactory.java index 9596c3f6185..eae72f10699 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderFactory.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderFactory.java @@ -41,6 +41,7 @@ import org.opentripplanner.routing.linking.configure.VertexLinkerGraphBuildingModule; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; import org.opentripplanner.service.osminfo.configure.OsmInfoGraphBuildServiceModule; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.worldenvelope.WorldEnvelopeRepository; import org.opentripplanner.standalone.config.BuildConfig; @@ -111,6 +112,9 @@ interface Builder { @BindsInstance Builder osmInfoGraphBuildRepository(OsmInfoGraphBuildRepository osmInfoGraphBuildRepository); + @BindsInstance + Builder streetDetailsRepository(StreetDetailsRepository streetDetailsRepository); + @BindsInstance Builder worldEnvelopeRepository(WorldEnvelopeRepository worldEnvelopeRepository); diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java b/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java index 9a3d66b0623..c583c73e68e 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java @@ -45,6 +45,7 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.linking.VertexLinker; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.standalone.config.BuildConfig; import org.opentripplanner.street.model.StreetLimitationParameters; @@ -65,6 +66,7 @@ static OsmModule provideOsmModule( BuildConfig config, Graph graph, OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, + StreetDetailsRepository streetDetailsRepository, VehicleParkingRepository vehicleParkingRepository, DataImportIssueStore issueStore, StreetLimitationParameters streetLimitationParameters @@ -90,6 +92,8 @@ static OsmModule provideOsmModule( .withPlatformEntriesLinking(config.platformEntriesLinking) .withStaticParkAndRide(config.staticParkAndRide) .withStaticBikeParkAndRide(config.staticBikeParkAndRide) + .withIncludeEdgeLevelInfo(config.includeEdgeLevelInfo) + .withStreetDetailsRepository(streetDetailsRepository) .withMaxAreaNodes(config.maxAreaNodes) .withBoardingAreaRefTags(config.boardingLocationTags) .withIncludeOsmSubwayEntrances(config.osmDefaults.includeOsmSubwayEntrances()) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/BarrierEdgeBuilder.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/BarrierEdgeBuilder.java index 43e7598d5f4..937af1c768f 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/BarrierEdgeBuilder.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/BarrierEdgeBuilder.java @@ -68,7 +68,7 @@ public void build(OsmNode node, Collection vertices, Collection intersectionNodes; private final DataImportIssueStore issueStore; // If an escalator is tagged as moving less than 5 cm/s, or more than 5 m/s, @@ -24,22 +20,16 @@ class EscalatorProcessor { private static final double SLOW_ESCALATOR_ERROR_CUTOFF = 0.05; private static final double FAST_ESCALATOR_ERROR_CUTOFF = 5.0; - public EscalatorProcessor( - Map intersectionNodes, - DataImportIssueStore issueStore - ) { - this.intersectionNodes = intersectionNodes; + public EscalatorProcessor(DataImportIssueStore issueStore) { this.issueStore = issueStore; } - public void buildEscalatorEdge(OsmWay escalatorWay, double length) { - List nodes = Arrays.stream(escalatorWay.getNodeRefs().toArray()) - .filter( - nodeRef -> intersectionNodes.containsKey(nodeRef) && intersectionNodes.get(nodeRef) != null - ) - .boxed() - .toList(); - + public EscalatorEdgePair buildEscalatorEdge( + OsmWay escalatorWay, + double length, + IntersectionVertex fromVertex, + IntersectionVertex toVertex + ) { Optional duration = escalatorWay.getDuration(v -> issueStore.add( Issue.issue( @@ -64,36 +54,21 @@ public void buildEscalatorEdge(OsmWay escalatorWay, double length) { ); } } - for (int i = 0; i < nodes.size() - 1; i++) { - if (escalatorWay.isForwardEscalator()) { - EscalatorEdge.createEscalatorEdge( - intersectionNodes.get(nodes.get(i)), - intersectionNodes.get(nodes.get(i + 1)), - length, - duration.orElse(null) - ); - } else if (escalatorWay.isBackwardEscalator()) { - EscalatorEdge.createEscalatorEdge( - intersectionNodes.get(nodes.get(i + 1)), - intersectionNodes.get(nodes.get(i)), - length, - duration.orElse(null) - ); - } else { - EscalatorEdge.createEscalatorEdge( - intersectionNodes.get(nodes.get(i)), - intersectionNodes.get(nodes.get(i + 1)), - length, - duration.orElse(null) - ); - - EscalatorEdge.createEscalatorEdge( - intersectionNodes.get(nodes.get(i + 1)), - intersectionNodes.get(nodes.get(i)), - length, - duration.orElse(null) - ); - } + if (escalatorWay.isForwardEscalator()) { + return new EscalatorEdgePair( + EscalatorEdge.createEscalatorEdge(fromVertex, toVertex, length, duration.orElse(null)), + null + ); + } + if (escalatorWay.isBackwardEscalator()) { + return new EscalatorEdgePair( + null, + EscalatorEdge.createEscalatorEdge(toVertex, fromVertex, length, duration.orElse(null)) + ); } + return new EscalatorEdgePair( + EscalatorEdge.createEscalatorEdge(fromVertex, toVertex, length, duration.orElse(null)), + EscalatorEdge.createEscalatorEdge(toVertex, fromVertex, length, duration.orElse(null)) + ); } } diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java index 83e6f2367a2..5161a0c107a 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java @@ -100,8 +100,8 @@ public class OsmDatabase { * Track which vertical levels OSM entities belong to. * Level information can be set for ways, nodes and relations. * An entity only has an entry if at least one level is defined in OSM. - * The ordering is important because in the future it will be used for building stairs - * and escalators. At the moment, the level is used e.g. for building elevators. + * The ordering is important because it is used for building stairs and escalators. + * The level is also used e.g. for building elevators and connecting areas. */ private final ArrayListMultimap entityLevels = ArrayListMultimap.create(); diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java index e5fec185718..a8566cdf936 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java @@ -27,6 +27,8 @@ import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.graph_builder.model.GraphBuilderModule; +import org.opentripplanner.graph_builder.module.osm.edgelevelinfo.DefaultEdgeLevelInfoProcessor; +import org.opentripplanner.graph_builder.module.osm.edgelevelinfo.NoopEdgeLevelInfoProcessor; import org.opentripplanner.graph_builder.module.osm.parameters.OsmProcessingParameters; import org.opentripplanner.osm.OsmProvider; import org.opentripplanner.osm.model.OsmEntity; @@ -39,6 +41,7 @@ import org.opentripplanner.routing.util.ElevationUtils; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; import org.opentripplanner.service.osminfo.model.Platform; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.vehicleparking.model.VehicleParking; import org.opentripplanner.street.model.StreetLimitationParameters; @@ -67,6 +70,9 @@ public class OsmModule implements GraphBuilderModule { private final List providers; private final Graph graph; private final OsmInfoGraphBuildRepository osmInfoGraphBuildRepository; + + private final StreetDetailsRepository streetDetailsRepository; + private final VehicleParkingRepository parkingRepository; private final DataImportIssueStore issueStore; @@ -78,6 +84,7 @@ public class OsmModule implements GraphBuilderModule { Collection providers, Graph graph, OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, + StreetDetailsRepository streetDetailsRepository, VehicleParkingRepository parkingRepository, DataImportIssueStore issueStore, StreetLimitationParameters streetLimitationParameters, @@ -86,6 +93,7 @@ public class OsmModule implements GraphBuilderModule { this.providers = List.copyOf(providers); this.graph = graph; this.osmInfoGraphBuildRepository = osmInfoGraphBuildRepository; + this.streetDetailsRepository = streetDetailsRepository; this.parkingRepository = parkingRepository; this.issueStore = issueStore; this.params = params; @@ -314,10 +322,10 @@ private void buildBasicGraph(OsmDatabase osmdb, VertexGenerator vertexGenerator) long wayCount = osmdb.getWays().size(); ProgressTracker progress = ProgressTracker.track("Build street graph", 5_000, wayCount); LOG.info(progress.startMessage()); - var escalatorProcessor = new EscalatorProcessor( - vertexGenerator.intersectionNodes(), - issueStore - ); + var escalatorProcessor = new EscalatorProcessor(issueStore); + var edgeLevelInfoProcessor = params.includeEdgeLevelInfo() + ? new DefaultEdgeLevelInfoProcessor(issueStore, streetDetailsRepository) + : new NoopEdgeLevelInfoProcessor(); WAY: for (OsmWay way : osmdb.getWays()) { WayPropertiesPair wayData = way.getOsmProvider().getWayPropertySet().getDataForWay(way); @@ -363,8 +371,8 @@ private void buildBasicGraph(OsmDatabase osmdb, VertexGenerator vertexGenerator) lastLevel = level; } - IntersectionVertex startEndpoint = null; - IntersectionVertex endEndpoint = null; + IntersectionVertex fromVertex = null; + IntersectionVertex toVertex = null; ArrayList segmentCoordinates = new ArrayList<>(); @@ -378,7 +386,8 @@ private void buildBasicGraph(OsmDatabase osmdb, VertexGenerator vertexGenerator) // where the current edge should start OsmNode osmStartNode = null; - var platform = getPlatform(osmdb, way); + var platformOptional = getPlatform(osmdb, way); + var edgeLevelInfoOptional = edgeLevelInfoProcessor.findEdgeLevelInfo(osmdb, way); for (int i = 0; i < nodes.size() - 1; i++) { OsmNode segmentStartOsmNode = osmdb.getNode(nodes.get(i)); @@ -426,36 +435,47 @@ private void buildBasicGraph(OsmDatabase osmdb, VertexGenerator vertexGenerator) } /* generate endpoints */ - if (startEndpoint == null) { // first iteration on this way + if (fromVertex == null) { // first iteration on this way // make or get a shared vertex for flat intersections, // one vertex per level for multilevel nodes like elevators - startEndpoint = vertexGenerator.getVertexForOsmNode(osmStartNode, way, NORMAL); + fromVertex = vertexGenerator.getVertexForOsmNode(osmStartNode, way, NORMAL); String ele = segmentStartOsmNode.getTag("ele"); if (ele != null) { Double elevation = ElevationUtils.parseEleTag(ele); if (elevation != null) { - elevationData.put(startEndpoint, elevation); + elevationData.put(fromVertex, elevation); } } } else { // subsequent iterations - startEndpoint = endEndpoint; + fromVertex = toVertex; } - endEndpoint = vertexGenerator.getVertexForOsmNode(osmEndNode, way, NORMAL); + toVertex = vertexGenerator.getVertexForOsmNode(osmEndNode, way, NORMAL); String ele = osmEndNode.getTag("ele"); if (ele != null) { Double elevation = ElevationUtils.parseEleTag(ele); if (elevation != null) { - elevationData.put(endEndpoint, elevation); + elevationData.put(toVertex, elevation); } } if (way.isEscalator()) { var length = getGeometryLengthMeters(geometry); - escalatorProcessor.buildEscalatorEdge(way, length); + EscalatorEdgePair escalatorEdgePair = escalatorProcessor.buildEscalatorEdge( + way, + length, + fromVertex, + toVertex + ); + edgeLevelInfoProcessor.storeLevelInfoForEdge( + escalatorEdgePair.main(), + escalatorEdgePair.back(), + edgeLevelInfoOptional, + way + ); } else { StreetEdgePair streets = getEdgesForStreet( - startEndpoint, - endEndpoint, + fromVertex, + toVertex, way, i, forwardPermission, @@ -475,12 +495,21 @@ private void buildBasicGraph(OsmDatabase osmdb, VertexGenerator vertexGenerator) way ); - platform.ifPresent(plat -> { + platformOptional.ifPresent(plat -> { for (var s : streets.asIterable()) { osmInfoGraphBuildRepository.addPlatform(s, plat); } }); + if (way.isStairs()) { + edgeLevelInfoProcessor.storeLevelInfoForEdge( + street, + backStreet, + edgeLevelInfoOptional, + way + ); + } + applyEdgesToTurnRestrictions(osmdb, way, startNode, endNode, street, backStreet); startNode = endNode; osmStartNode = osmdb.getNode(startNode); @@ -598,8 +627,8 @@ private void applyEdgesToTurnRestrictions( * http://wiki.openstreetmap.org/wiki/OSM_tags_for_routing#Oneway. */ private StreetEdgePair getEdgesForStreet( - IntersectionVertex startEndpoint, - IntersectionVertex endEndpoint, + IntersectionVertex fromVertex, + IntersectionVertex toVertex, OsmWay way, int index, StreetTraversalPermission forwardPermission, @@ -618,8 +647,8 @@ private StreetEdgePair getEdgesForStreet( if (forwardPermission.allowsAnything()) { street = getEdgeForStreet( - startEndpoint, - endEndpoint, + fromVertex, + toVertex, way, index, length, @@ -630,8 +659,8 @@ private StreetEdgePair getEdgesForStreet( } if (backwardPermission.allowsAnything()) { backStreet = getEdgeForStreet( - endEndpoint, - startEndpoint, + toVertex, + fromVertex, way, index, length, @@ -647,8 +676,8 @@ private StreetEdgePair getEdgesForStreet( } private StreetEdge getEdgeForStreet( - IntersectionVertex startEndpoint, - IntersectionVertex endEndpoint, + IntersectionVertex fromVertex, + IntersectionVertex toVertex, OsmWay way, int index, double length, @@ -668,8 +697,8 @@ private StreetEdge getEdgeForStreet( float carSpeed = way.getOsmProvider().getOsmTagMapper().getCarSpeedForWay(way, direction); StreetEdgeBuilder seb = new StreetEdgeBuilder<>() - .withFromVertex(startEndpoint) - .withToVertex(endEndpoint) + .withFromVertex(fromVertex) + .withToVertex(toVertex) .withGeometry(geometry) .withName(name) .withMeterLength(length) @@ -680,7 +709,7 @@ private StreetEdge getEdgeForStreet( .withRoundabout(way.isRoundabout()) .withCrossing(way.isCrossing()) .withSlopeOverride(way.getOsmProvider().getWayPropertySet().getSlopeOverride(way)) - .withStairs(way.isSteps()) + .withStairs(way.isStairs()) .withWheelchairAccessible(way.isWheelchairAccessible()) .withBogusName(way.hasNoName()); diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModuleBuilder.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModuleBuilder.java index bf75c5d38b2..63cfbd56239 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModuleBuilder.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModuleBuilder.java @@ -9,6 +9,8 @@ import org.opentripplanner.osm.OsmProvider; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.street.model.StreetConstants; import org.opentripplanner.street.model.StreetLimitationParameters; @@ -22,6 +24,8 @@ public class OsmModuleBuilder { private final Graph graph; private final VehicleParkingRepository parkingRepository; private final OsmInfoGraphBuildRepository osmInfoGraphBuildRepository; + private StreetDetailsRepository streetDetailsRepository = new DefaultStreetDetailsRepository(); + private Set boardingAreaRefTags = Set.of(); private DataImportIssueStore issueStore = DataImportIssueStore.NOOP; private EdgeNamer edgeNamer = new DefaultNamer(); @@ -29,6 +33,7 @@ public class OsmModuleBuilder { private boolean platformEntriesLinking = false; private boolean staticParkAndRide = false; private boolean staticBikeParkAndRide = false; + private boolean includeEdgeLevelInfo = false; private boolean includeOsmSubwayEntrances = false; private int maxAreaNodes = StreetConstants.DEFAULT_MAX_AREA_NODES; private StreetLimitationParameters streetLimitationParameters = new StreetLimitationParameters(); @@ -80,6 +85,11 @@ public OsmModuleBuilder withStaticBikeParkAndRide(boolean staticBikeParkAndRide) return this; } + public OsmModuleBuilder withIncludeEdgeLevelInfo(boolean includeEdgeLevelInfo) { + this.includeEdgeLevelInfo = includeEdgeLevelInfo; + return this; + } + public OsmModuleBuilder withMaxAreaNodes(int maxAreaNodes) { this.maxAreaNodes = maxAreaNodes; return this; @@ -95,11 +105,19 @@ public OsmModuleBuilder withStreetLimitationParameters(StreetLimitationParameter return this; } + public OsmModuleBuilder withStreetDetailsRepository( + StreetDetailsRepository streetDetailsRepository + ) { + this.streetDetailsRepository = streetDetailsRepository; + return this; + } + public OsmModule build() { return new OsmModule( providers, graph, osmInfoGraphBuildRepository, + streetDetailsRepository, parkingRepository, issueStore, streetLimitationParameters, @@ -111,6 +129,7 @@ public OsmModule build() { platformEntriesLinking, staticParkAndRide, staticBikeParkAndRide, + includeEdgeLevelInfo, includeOsmSubwayEntrances ) ); diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/edgelevelinfo/DefaultEdgeLevelInfoProcessor.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/edgelevelinfo/DefaultEdgeLevelInfoProcessor.java new file mode 100644 index 00000000000..e0028f542c0 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/edgelevelinfo/DefaultEdgeLevelInfoProcessor.java @@ -0,0 +1,138 @@ +package org.opentripplanner.graph_builder.module.osm.edgelevelinfo; + +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.graph_builder.issues.ContradictoryLevelAndInclineInfoForWay; +import org.opentripplanner.graph_builder.issues.CouldNotApplyMultiLevelInfoToWay; +import org.opentripplanner.graph_builder.module.osm.OsmDatabase; +import org.opentripplanner.osm.model.OsmLevel; +import org.opentripplanner.osm.model.OsmWay; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; +import org.opentripplanner.service.streetdetails.model.EdgeLevelInfo; +import org.opentripplanner.service.streetdetails.model.Level; +import org.opentripplanner.service.streetdetails.model.VertexLevelInfo; +import org.opentripplanner.street.model.edge.Edge; + +public class DefaultEdgeLevelInfoProcessor implements EdgeLevelInfoProcessor { + + private final DataImportIssueStore issueStore; + private final StreetDetailsRepository streetDetailsRepository; + + public DefaultEdgeLevelInfoProcessor( + DataImportIssueStore issueStore, + StreetDetailsRepository streetDetailsRepository + ) { + this.issueStore = issueStore; + this.streetDetailsRepository = streetDetailsRepository; + } + + @Override + public Optional findEdgeLevelInfo(OsmDatabase osmdb, OsmWay way) { + var nodeRefs = way.getNodeRefs(); + long firstNodeRef = nodeRefs.get(0); + long lastNodeRef = nodeRefs.get(nodeRefs.size() - 1); + + EdgeLevelInfo levelInfo = findLevelInfo(osmdb, way, firstNodeRef, lastNodeRef); + EdgeLevelInfo inclineInfo = findInclineInfo(osmdb, way, firstNodeRef, lastNodeRef); + + if ( + levelInfo != null && + inclineInfo != null && + levelInfo.lowerVertexInfo() != inclineInfo.lowerVertexInfo() + ) { + issueStore.add(new ContradictoryLevelAndInclineInfoForWay(way)); + // Default to level info in case of contradictory information. Ideally this should be from + // the tag that is more reliable. + return Optional.of(levelInfo); + } else if (levelInfo != null) { + return Optional.of(levelInfo); + } else if (inclineInfo != null) { + return Optional.of(inclineInfo); + } + return Optional.empty(); + } + + private EdgeLevelInfo findLevelInfo( + OsmDatabase osmdb, + OsmWay way, + long firstNodeRef, + long lastNodeRef + ) { + List levels = osmdb.getLevelsForEntity(way); + if (levels.size() == 2) { + OsmLevel firstVertexOsmLevel = levels.get(0); + OsmLevel lastVertexOsmLevel = levels.get(1); + if (firstVertexOsmLevel.level() < lastVertexOsmLevel.level()) { + return new EdgeLevelInfo( + new VertexLevelInfo( + new Level(firstVertexOsmLevel.level(), firstVertexOsmLevel.name()), + firstNodeRef + ), + new VertexLevelInfo( + new Level(lastVertexOsmLevel.level(), lastVertexOsmLevel.name()), + lastNodeRef + ) + ); + } else if (firstVertexOsmLevel.level() > lastVertexOsmLevel.level()) { + return new EdgeLevelInfo( + new VertexLevelInfo( + new Level(lastVertexOsmLevel.level(), lastVertexOsmLevel.name()), + lastNodeRef + ), + new VertexLevelInfo( + new Level(firstVertexOsmLevel.level(), firstVertexOsmLevel.name()), + firstNodeRef + ) + ); + } + } + return null; + } + + private EdgeLevelInfo findInclineInfo( + OsmDatabase osmdb, + OsmWay way, + long firstNodeRef, + long lastNodeRef + ) { + if (way.isInclineUp()) { + return new EdgeLevelInfo( + new VertexLevelInfo(null, firstNodeRef), + new VertexLevelInfo(null, lastNodeRef) + ); + } else if (way.isInclineDown()) { + return new EdgeLevelInfo( + new VertexLevelInfo(null, lastNodeRef), + new VertexLevelInfo(null, firstNodeRef) + ); + } + return null; + } + + @Override + public void storeLevelInfoForEdge( + @Nullable Edge forwardEdge, + @Nullable Edge backwardEdge, + Optional edgeLevelInfoOptional, + OsmWay way + ) { + if (edgeLevelInfoOptional.isEmpty()) { + return; + } + + EdgeLevelInfo edgeLevelInfo = edgeLevelInfoOptional.get(); + Edge edge = forwardEdge != null ? forwardEdge : backwardEdge; + if (edge != null && edgeLevelInfo.canBeAppliedToEdge(edge)) { + if (forwardEdge != null) { + streetDetailsRepository.addEdgeLevelInformation(forwardEdge, edgeLevelInfo); + } + if (backwardEdge != null) { + streetDetailsRepository.addEdgeLevelInformation(backwardEdge, edgeLevelInfo); + } + } else { + issueStore.add(new CouldNotApplyMultiLevelInfoToWay(way, way.getNodeRefs().size())); + } + } +} diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/edgelevelinfo/EdgeLevelInfoProcessor.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/edgelevelinfo/EdgeLevelInfoProcessor.java new file mode 100644 index 00000000000..267e4f7239c --- /dev/null +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/edgelevelinfo/EdgeLevelInfoProcessor.java @@ -0,0 +1,26 @@ +package org.opentripplanner.graph_builder.module.osm.edgelevelinfo; + +import java.util.Optional; +import javax.annotation.Nullable; +import org.opentripplanner.graph_builder.module.osm.OsmDatabase; +import org.opentripplanner.osm.model.OsmWay; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; +import org.opentripplanner.service.streetdetails.model.EdgeLevelInfo; +import org.opentripplanner.street.model.edge.Edge; + +/** + * Contains logic for storing edge level info in the + * {@link StreetDetailsRepository}. + */ +public interface EdgeLevelInfoProcessor { + EdgeLevelInfoProcessor NOOP = new NoopEdgeLevelInfoProcessor(); + + public Optional findEdgeLevelInfo(OsmDatabase osmdb, OsmWay way); + + public void storeLevelInfoForEdge( + @Nullable Edge forwardEdge, + @Nullable Edge backwardEdge, + Optional edgeLevelInfoOptional, + OsmWay way + ); +} diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/edgelevelinfo/NoopEdgeLevelInfoProcessor.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/edgelevelinfo/NoopEdgeLevelInfoProcessor.java new file mode 100644 index 00000000000..59036b76993 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/edgelevelinfo/NoopEdgeLevelInfoProcessor.java @@ -0,0 +1,24 @@ +package org.opentripplanner.graph_builder.module.osm.edgelevelinfo; + +import java.util.Optional; +import javax.annotation.Nullable; +import org.opentripplanner.graph_builder.module.osm.OsmDatabase; +import org.opentripplanner.osm.model.OsmWay; +import org.opentripplanner.service.streetdetails.model.EdgeLevelInfo; +import org.opentripplanner.street.model.edge.Edge; + +public class NoopEdgeLevelInfoProcessor implements EdgeLevelInfoProcessor { + + @Override + public Optional findEdgeLevelInfo(OsmDatabase osmdb, OsmWay way) { + return Optional.empty(); + } + + @Override + public void storeLevelInfoForEdge( + @Nullable Edge forwardEdge, + @Nullable Edge backwardEdge, + Optional edgeLevelInfoOptional, + OsmWay way + ) {} +} diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmProcessingParameters.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmProcessingParameters.java index a3fd14020e8..dab8fefd874 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmProcessingParameters.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmProcessingParameters.java @@ -13,6 +13,7 @@ * @param platformEntriesLinking Whether platform entries should be linked * @param staticParkAndRide Whether we should create car P+R stations from OSM data. * @param staticBikeParkAndRide Whether we should create bike P+R stations from OSM data. + * @param includeEdgeLevelInfo Whether level info for edges should be stored. * @param includeOsmSubwayEntrances Whether we should create subway entrances from OSM data. */ public record OsmProcessingParameters( @@ -23,6 +24,7 @@ public record OsmProcessingParameters( boolean platformEntriesLinking, boolean staticParkAndRide, boolean staticBikeParkAndRide, + boolean includeEdgeLevelInfo, boolean includeOsmSubwayEntrances ) { public OsmProcessingParameters { diff --git a/application/src/main/java/org/opentripplanner/model/plan/walkstep/RelativeDirection.java b/application/src/main/java/org/opentripplanner/model/plan/walkstep/RelativeDirection.java index 79df7539e77..db0015f2152 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/walkstep/RelativeDirection.java +++ b/application/src/main/java/org/opentripplanner/model/plan/walkstep/RelativeDirection.java @@ -17,6 +17,8 @@ public enum RelativeDirection { CIRCLE_CLOCKWISE, CIRCLE_COUNTERCLOCKWISE, ELEVATOR, + ESCALATOR, + STAIRS, UTURN_LEFT, UTURN_RIGHT, ENTER_STATION, diff --git a/application/src/main/java/org/opentripplanner/model/plan/walkstep/WalkStep.java b/application/src/main/java/org/opentripplanner/model/plan/walkstep/WalkStep.java index d562982e47b..adb0d2ffe04 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/walkstep/WalkStep.java +++ b/application/src/main/java/org/opentripplanner/model/plan/walkstep/WalkStep.java @@ -4,9 +4,11 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import javax.annotation.Nullable; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.model.plan.leg.ElevationProfile; +import org.opentripplanner.model.plan.walkstep.verticaltransportation.VerticalTransportationUse; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.note.StreetNote; import org.opentripplanner.transit.model.site.Entrance; @@ -46,7 +48,13 @@ public final class WalkStep { private final boolean walkingBike; private final String highwayExit; + + @Nullable private final Entrance entrance; + + @Nullable + private final VerticalTransportationUse verticalTransportationUse; + private final ElevationProfile elevationProfile; private final boolean stayOn; @@ -59,7 +67,8 @@ public final class WalkStep { I18NString directionText, Set streetNotes, String highwayExit, - Entrance entrance, + @Nullable Entrance entrance, + @Nullable VerticalTransportationUse verticalTransportationUse, ElevationProfile elevationProfile, boolean nameIsDerived, boolean walkingBike, @@ -81,6 +90,7 @@ public final class WalkStep { this.area = area; this.highwayExit = highwayExit; this.entrance = entrance; + this.verticalTransportationUse = verticalTransportationUse; this.elevationProfile = elevationProfile; this.stayOn = stayOn; this.edges = List.copyOf(Objects.requireNonNull(edges)); @@ -142,6 +152,13 @@ public Optional entrance() { return Optional.ofNullable(entrance); } + /** + * Get information about vertical transportation equipment used. + */ + public Optional verticalTransportationUse() { + return Optional.ofNullable(verticalTransportationUse); + } + /** * Indicates whether a street changes direction at an intersection. */ diff --git a/application/src/main/java/org/opentripplanner/model/plan/walkstep/WalkStepBuilder.java b/application/src/main/java/org/opentripplanner/model/plan/walkstep/WalkStepBuilder.java index 7db23ef2ea8..af40b21ef6c 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/walkstep/WalkStepBuilder.java +++ b/application/src/main/java/org/opentripplanner/model/plan/walkstep/WalkStepBuilder.java @@ -8,6 +8,7 @@ import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.model.plan.leg.ElevationProfile; +import org.opentripplanner.model.plan.walkstep.verticaltransportation.VerticalTransportationUse; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.note.StreetNote; import org.opentripplanner.transit.model.site.Entrance; @@ -27,7 +28,13 @@ public class WalkStepBuilder { private RelativeDirection relativeDirection; private ElevationProfile elevationProfile; private String exit; + + @Nullable private Entrance entrance; + + @Nullable + private VerticalTransportationUse verticalTransportationUse; + private boolean stayOn = false; private boolean crossing; /** @@ -83,6 +90,13 @@ public WalkStepBuilder withEntrance(@Nullable Entrance entrance) { return this; } + public WalkStepBuilder withVerticalTransportationUse( + @Nullable VerticalTransportationUse verticalTransportationUse + ) { + this.verticalTransportationUse = verticalTransportationUse; + return this; + } + public WalkStepBuilder withStayOn(boolean stayOn) { this.stayOn = stayOn; return this; @@ -149,6 +163,10 @@ public boolean hasEntrance() { return entrance != null; } + public boolean hasVerticalTransportationUse() { + return verticalTransportationUse != null; + } + public WalkStepBuilder addStreetNotes(Set notes) { this.streetNotes.addAll(notes); return this; @@ -182,6 +200,7 @@ public WalkStep build() { streetNotes, exit, entrance, + verticalTransportationUse, elevationProfile, nameIsDerived, walkingBike, diff --git a/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/EscalatorUse.java b/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/EscalatorUse.java new file mode 100644 index 00000000000..be61b88fbe6 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/EscalatorUse.java @@ -0,0 +1,19 @@ +package org.opentripplanner.model.plan.walkstep.verticaltransportation; + +import javax.annotation.Nullable; +import org.opentripplanner.service.streetdetails.model.Level; + +/** + * Represents information about a single use of an escalator related to + * {@link org.opentripplanner.model.plan.walkstep.WalkStep}. + */ +public final class EscalatorUse extends VerticalTransportationUse { + + public EscalatorUse( + @Nullable Level from, + @Nullable Level to, + VerticalDirection verticalDirection + ) { + super(from, to, verticalDirection); + } +} diff --git a/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/StairsUse.java b/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/StairsUse.java new file mode 100644 index 00000000000..69d1c75e0e5 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/StairsUse.java @@ -0,0 +1,15 @@ +package org.opentripplanner.model.plan.walkstep.verticaltransportation; + +import javax.annotation.Nullable; +import org.opentripplanner.service.streetdetails.model.Level; + +/** + * Represents information about a single use of a set of stairs related to + * {@link org.opentripplanner.model.plan.walkstep.WalkStep}. + */ +public final class StairsUse extends VerticalTransportationUse { + + public StairsUse(@Nullable Level from, @Nullable Level to, VerticalDirection verticalDirection) { + super(from, to, verticalDirection); + } +} diff --git a/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/VerticalDirection.java b/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/VerticalDirection.java new file mode 100644 index 00000000000..5b2306cad0b --- /dev/null +++ b/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/VerticalDirection.java @@ -0,0 +1,6 @@ +package org.opentripplanner.model.plan.walkstep.verticaltransportation; + +public enum VerticalDirection { + DOWN, + UP, +} diff --git a/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/VerticalTransportationUse.java b/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/VerticalTransportationUse.java new file mode 100644 index 00000000000..6eb719dced7 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/VerticalTransportationUse.java @@ -0,0 +1,41 @@ +package org.opentripplanner.model.plan.walkstep.verticaltransportation; + +import javax.annotation.Nullable; +import org.opentripplanner.service.streetdetails.model.Level; + +/** + * Represents information about a single use of vertical transportation equipment stored in + * {@link org.opentripplanner.model.plan.walkstep.WalkStep}. + */ +public abstract sealed class VerticalTransportationUse permits EscalatorUse, StairsUse { + + @Nullable + private final Level from; + + @Nullable + private final Level to; + + private final VerticalDirection verticalDirection; + + public VerticalTransportationUse( + @Nullable Level from, + @Nullable Level to, + VerticalDirection verticalDirection + ) { + this.from = from; + this.to = to; + this.verticalDirection = verticalDirection; + } + + public Level from() { + return this.from; + } + + public Level to() { + return this.to; + } + + public VerticalDirection verticalDirection() { + return this.verticalDirection; + } +} diff --git a/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/VerticalTransportationUseFactory.java b/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/VerticalTransportationUseFactory.java new file mode 100644 index 00000000000..2531d7a4579 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/model/plan/walkstep/verticaltransportation/VerticalTransportationUseFactory.java @@ -0,0 +1,50 @@ +package org.opentripplanner.model.plan.walkstep.verticaltransportation; + +import java.util.Optional; +import org.opentripplanner.service.streetdetails.StreetDetailsService; +import org.opentripplanner.service.streetdetails.model.EdgeLevelInfo; +import org.opentripplanner.service.streetdetails.model.VertexLevelInfo; +import org.opentripplanner.street.model.edge.Edge; +import org.opentripplanner.street.model.edge.EscalatorEdge; +import org.opentripplanner.street.model.edge.StreetEdge; +import org.opentripplanner.street.model.vertex.OsmVertex; + +/** + * Represents information about a single use of a set of stairs related to + * {@link org.opentripplanner.model.plan.walkstep.WalkStep}. + */ +public class VerticalTransportationUseFactory { + + private final StreetDetailsService streetDetailsService; + + public VerticalTransportationUseFactory(StreetDetailsService streetDetailsService) { + this.streetDetailsService = streetDetailsService; + } + + public VerticalTransportationUse createInclinedVerticalTransportationUse(Edge edge) { + Optional edgeLevelInfoOptional = streetDetailsService.findEdgeInformation(edge); + if (edgeLevelInfoOptional.isEmpty()) { + return null; + } + EdgeLevelInfo edgeLevelInfo = edgeLevelInfoOptional.get(); + + VerticalDirection verticalDirection = edge.getFromVertex() instanceof OsmVertex fromVertex && + fromVertex.nodeId() == edgeLevelInfo.lowerVertexInfo().osmNodeId() + ? VerticalDirection.UP + : VerticalDirection.DOWN; + VertexLevelInfo fromVertexInfo = verticalDirection == VerticalDirection.UP + ? edgeLevelInfo.lowerVertexInfo() + : edgeLevelInfo.upperVertexInfo(); + VertexLevelInfo toVertexInfo = verticalDirection == VerticalDirection.UP + ? edgeLevelInfo.upperVertexInfo() + : edgeLevelInfo.lowerVertexInfo(); + + if (edge instanceof EscalatorEdge) { + return new EscalatorUse(fromVertexInfo.level(), toVertexInfo.level(), verticalDirection); + } + if (edge instanceof StreetEdge streetEdge && streetEdge.isStairs()) { + return new StairsUse(fromVertexInfo.level(), toVertexInfo.level(), verticalDirection); + } + return null; + } +} diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java index e0f4fb1e091..444fe791493 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java @@ -332,7 +332,10 @@ protected boolean isExplicitlyAllowed(String key) { ); } - /** @return a tag's value, converted to lower case. */ + /** + * @return Converts a tag to lower case and returns the associated value. + * Returns null if tag is not present. + */ @Nullable public String getTag(String tag) { tag = tag.toLowerCase(); @@ -343,8 +346,8 @@ public String getTag(String tag) { } /** - * - * @return A tags value converted to lower case. An empty Optional if tags is not present. + * @return Converts a tag to lower case and returns the associated value. + * An empty Optional if tag is not present. */ public Optional getTagOpt(String network) { return Optional.ofNullable(getTag(network)); @@ -493,7 +496,7 @@ public OptionalInt parseIntOrBoolean(String tag, Consumer errorHandler) } /** - * Checks is a tag contains the specified value. + * Checks if a tag contains the specified value. */ public boolean isTag(String tag, String value) { tag = tag.toLowerCase(); diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmLevel.java b/application/src/main/java/org/opentripplanner/osm/model/OsmLevel.java index 15634d44b3d..dbd1ca7854d 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmLevel.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmLevel.java @@ -24,15 +24,14 @@ public int hashCode() { } @Override - public boolean equals(Object object) { - if (object instanceof OsmLevel other) { - // Levels should be equal if their 0-based number representations are equal. - // There should be no need to compare names. - // For example, comparing the default level and a ground level with a name should be equal. - return this.level == other.level; - } else { - return false; - } + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OsmLevel that = (OsmLevel) o; + // Levels should be equal if their 0-based number representations are equal. + // There should be no need to compare names. + // For example, comparing the default level and a ground level with a name should be equal. + return this.level == that.level; } /** diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmWay.java b/application/src/main/java/org/opentripplanner/osm/model/OsmWay.java index 2e8235f1053..983fba5a546 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmWay.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmWay.java @@ -54,15 +54,22 @@ public boolean isBoardingArea() { } /** - * Returns true if these are steps. + * @return true if these are steps. */ public boolean isSteps() { return isTag("highway", "steps"); } /** - * Checks the wheelchair-accessibility of this way. Stairs are by default inaccessible but - * can be made accessible if they explicitly set wheelchair=true. + * @return true if these are stairs. + */ + public boolean isStairs() { + return isTag("highway", "steps") && !isOneOfTags("conveying", ESCALATOR_CONVEYING_TAGS); + } + + /** + * Checks the wheelchair-accessibility of this way. Stairs and escalators are by default + * inaccessible but can be made accessible if they explicitly set wheelchair=true. */ public boolean isWheelchairAccessible() { if (isSteps()) { @@ -73,15 +80,23 @@ public boolean isWheelchairAccessible() { } public boolean isEscalator() { - return (isTag("highway", "steps") && isOneOfTags("conveying", ESCALATOR_CONVEYING_TAGS)); + return isTag("highway", "steps") && isOneOfTags("conveying", ESCALATOR_CONVEYING_TAGS); } public boolean isForwardEscalator() { - return isEscalator() && "forward".equals(this.getTag("conveying")); + return isEscalator() && isTag("conveying", "forward"); } public boolean isBackwardEscalator() { - return isEscalator() && "backward".equals(this.getTag("conveying")); + return isEscalator() && isTag("conveying", "backward"); + } + + public boolean isInclineUp() { + return isTag("incline", "up"); + } + + public boolean isInclineDown() { + return isTag("incline", "down"); } /** diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapper.java b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapper.java index 5aa9e782629..393a0f34032 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapper.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapper.java @@ -30,6 +30,7 @@ import org.opentripplanner.model.plan.walkstep.WalkStep; import org.opentripplanner.routing.graphfinder.StopResolver; import org.opentripplanner.routing.services.notes.StreetNotesService; +import org.opentripplanner.service.streetdetails.StreetDetailsService; import org.opentripplanner.service.vehiclerental.street.VehicleRentalEdge; import org.opentripplanner.service.vehiclerental.street.VehicleRentalPlaceVertex; import org.opentripplanner.street.model.edge.BoardingLocationToStopLink; @@ -56,17 +57,20 @@ public class GraphPathToItineraryMapper { private final StopResolver stopResolver; private final ZoneId timeZone; private final StreetNotesService streetNotesService; + private final StreetDetailsService streetDetailsService; private final double ellipsoidToGeoidDifference; public GraphPathToItineraryMapper( StopResolver stopResolver, ZoneId timeZone, StreetNotesService streetNotesService, + StreetDetailsService streetDetailsService, double ellipsoidToGeoidDifference ) { this.stopResolver = stopResolver; this.timeZone = ZoneIdFallback.zoneId(timeZone); this.streetNotesService = streetNotesService; + this.streetDetailsService = streetDetailsService; this.ellipsoidToGeoidDifference = ellipsoidToGeoidDifference; } @@ -374,6 +378,7 @@ private StreetLeg generateLeg(List states, WalkStep previousStep) { states, previousStep, streetNotesService, + streetDetailsService, ellipsoidToGeoidDifference ); List walkSteps = statesToWalkStepsMapper.generateWalkSteps(); diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java index c58847d5c51..dab5d0199ca 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java @@ -40,6 +40,7 @@ import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.via.model.ViaCoordinateTransfer; +import org.opentripplanner.service.streetdetails.StreetDetailsService; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.street.search.request.StreetSearchRequest; @@ -80,6 +81,7 @@ public class RaptorPathToItineraryMapper { public RaptorPathToItineraryMapper( Graph graph, TransitService transitService, + StreetDetailsService streetDetailsService, RaptorTransitData raptorTransitData, ZonedDateTime transitSearchTimeZero, RouteRequest request @@ -93,6 +95,7 @@ public RaptorPathToItineraryMapper( transitService::getRegularStop, transitService.getTimeZone(), graph.streetNotesService, + streetDetailsService, graph.ellipsoidToGeoidDifference ); this.transitService = transitService; diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java index 5f81521ae0d..76eef5c9251 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java @@ -17,10 +17,13 @@ import org.opentripplanner.model.plan.walkstep.RelativeDirection; import org.opentripplanner.model.plan.walkstep.WalkStep; import org.opentripplanner.model.plan.walkstep.WalkStepBuilder; +import org.opentripplanner.model.plan.walkstep.verticaltransportation.VerticalTransportationUseFactory; import org.opentripplanner.routing.services.notes.StreetNotesService; +import org.opentripplanner.service.streetdetails.StreetDetailsService; import org.opentripplanner.street.model.edge.AreaEdge; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.edge.ElevatorAlightEdge; +import org.opentripplanner.street.model.edge.EscalatorEdge; import org.opentripplanner.street.model.edge.FreeEdge; import org.opentripplanner.street.model.edge.PathwayEdge; import org.opentripplanner.street.model.edge.StreetEdge; @@ -45,6 +48,7 @@ public class StatesToWalkStepsMapper { private final double ellipsoidToGeoidDifference; private final StreetNotesService streetNotesService; + private final VerticalTransportationUseFactory verticalTransportationUseFactory; private final List states; private final WalkStep previous; @@ -74,12 +78,16 @@ public StatesToWalkStepsMapper( List states, WalkStep previousStep, StreetNotesService streetNotesService, + StreetDetailsService streetDetailsService, double ellipsoidToGeoidDifference ) { this.states = states; this.previous = previousStep; this.streetNotesService = streetNotesService; this.ellipsoidToGeoidDifference = ellipsoidToGeoidDifference; + this.verticalTransportationUseFactory = new VerticalTransportationUseFactory( + streetDetailsService + ); } public static String getNormalizedName(String streetName) { @@ -172,10 +180,16 @@ private void processState(State backState, State forwardState) { // generate a step for getting off an elevator (all elevator narrative generation occurs // when alighting). We don't need to know what came before or will come after if (edge instanceof ElevatorAlightEdge) { - addStep(createElevatorWalkStep(backState, forwardState, edge)); + createAndSaveElevatorWalkStep(backState, forwardState, edge); + return; + } else if (edge instanceof EscalatorEdge) { + createAndSaveEscalatorWalkStep(backState, forwardState, edge, geom); + return; + } else if (edge instanceof StreetEdge streetEdge && streetEdge.isStairs()) { + createAndSaveStairsWalkStep(backState, forwardState, edge, geom); return; } else if (backState.getVertex() instanceof StationEntranceVertex stationEntranceVertex) { - addStep(createStationEntranceWalkStep(backState, forwardState, stationEntranceVertex)); + createAndSaveStationEntranceWalkStep(backState, forwardState, stationEntranceVertex); return; } else if (edge instanceof PathwayEdge pwe && pwe.signpostedAs().isPresent()) { createAndSaveStep( @@ -273,11 +287,11 @@ private void processState(State backState, State forwardState) { WalkStepBuilder twoBack = steps.get(lastIndex - 1); WalkStepBuilder lastStep = steps.get(lastIndex); boolean isOnSameStreet = isOnSameStreet(lastStep, twoBack, threeBack); - if (twoBack.distance() < MAX_ZAG_DISTANCE && isOnSameStreet && !twoBack.hasEntrance()) { + if (twoBack.distance() < MAX_ZAG_DISTANCE && isOnSameStreet && canZagBeRemoved(twoBack)) { if (isUTurn(twoBack, lastStep)) { steps.remove(lastIndex - 1); processUTurn(lastStep, twoBack); - } else if (!lastStep.hasEntrance()) { + } else if (canZagBeRemoved(lastStep)) { // total hack to remove zags. steps.remove(lastIndex); steps.remove(lastIndex - 1); @@ -372,6 +386,15 @@ private void removeZag(WalkStepBuilder threeBack, WalkStepBuilder twoBack) { } } + private boolean canZagBeRemoved(WalkStepBuilder walkStepBuilder) { + return ( + !walkStepBuilder.hasEntrance() && + walkStepBuilder.relativeDirection() != RelativeDirection.ESCALATOR && + walkStepBuilder.relativeDirection() != RelativeDirection.STAIRS && + walkStepBuilder.relativeDirection() != RelativeDirection.ELEVATOR + ); + } + private void processUTurn(WalkStepBuilder lastStep, WalkStepBuilder twoBack) { // in this case, we have two left turns or two right turns in quick // succession; this is probably a U-turn. @@ -528,40 +551,76 @@ private void createFirstStep(State backState, State forwardState) { steps.add(current); } - private WalkStepBuilder createElevatorWalkStep(State backState, State forwardState, Edge edge) { + private void createAndSaveElevatorWalkStep(State backState, State forwardState, Edge edge) { // don't care what came before or comes after - var step = createWalkStep(forwardState, backState); + addStep( + createWalkStep(forwardState, backState).withRelativeDirection(RelativeDirection.ELEVATOR) + ); + } - // tell the user where to get off the elevator using the exit notation, so the - // i18n interface will say 'Elevator to ' - // what happens is that the webapp sees name == null and ignores that, and it sees - // exit != null and uses to - // the floor name is the AlightEdge name - // reset to avoid confusion with 'Elevator on floor 1 to floor 1' - step.withDirectionText(edge.getName()); + private void createAndSaveStairsWalkStep( + State backState, + State forwardState, + Edge edge, + Geometry geom + ) { + addStep( + createWalkStep(forwardState, backState) + .withRelativeDirection(RelativeDirection.STAIRS) + .withAbsoluteDirection(DirectionUtils.getFirstAngle(geom)) + .addDistance(edge.getDistanceMeters()) + .withVerticalTransportationUse( + verticalTransportationUseFactory.createInclinedVerticalTransportationUse(edge) + ) + ); - step.withRelativeDirection(RelativeDirection.ELEVATOR); + lastAngle = DirectionUtils.getLastAngle(geom); + distance = edge.getDistanceMeters(); + current.addEdge(edge); + } - return step; + private void createAndSaveEscalatorWalkStep( + State backState, + State forwardState, + Edge edge, + Geometry geom + ) { + addStep( + createWalkStep(forwardState, backState) + .withRelativeDirection(RelativeDirection.ESCALATOR) + .withAbsoluteDirection(DirectionUtils.getFirstAngle(geom)) + .addDistance(edge.getDistanceMeters()) + .withVerticalTransportationUse( + verticalTransportationUseFactory.createInclinedVerticalTransportationUse(edge) + ) + ); + + lastAngle = DirectionUtils.getLastAngle(geom); + distance = edge.getDistanceMeters(); + current.addEdge(edge); } - private WalkStepBuilder createStationEntranceWalkStep( + private void createAndSaveStationEntranceWalkStep( State backState, State forwardState, StationEntranceVertex vertex ) { - Entrance entrance = Entrance.of(vertex.id()) + // don't care what came before or comes after + addStep( + createWalkStep(forwardState, backState) + // There is not a way to definitively determine if a user is entering or exiting the + // station, since the doors might be between or inside stations. + .withRelativeDirection(RelativeDirection.ENTER_OR_EXIT_STATION) + .withEntrance(getEntrance(vertex)) + ); + } + + private Entrance getEntrance(StationEntranceVertex vertex) { + return Entrance.of(vertex.id()) .withCode(vertex.code()) .withCoordinate(new WgsCoordinate(vertex.getCoordinate())) .withWheelchairAccessibility(vertex.wheelchairAccessibility()) .build(); - - // don't care what came before or comes after - return createWalkStep(forwardState, backState) - // There is not a way to definitively determine if a user is entering or exiting the station, - // since the doors might be between or inside stations. - .withRelativeDirection(RelativeDirection.ENTER_OR_EXIT_STATION) - .withEntrance(entrance); } private void createAndSaveStep( diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java index 0423c7ea7f4..6718612f44a 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java @@ -191,6 +191,7 @@ private TransitRouterResult route() { RaptorPathToItineraryMapper itineraryMapper = new RaptorPathToItineraryMapper<>( serverContext.graph(), serverContext.transitService(), + serverContext.streetDetailsService(), raptorTransitData, transitSearchTimeZero, request diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java index 419ed944b1b..a4be52c7737 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java @@ -64,6 +64,7 @@ public static List route( var flexRouter = new FlexRouter( serverContext.graph(), serverContext.transitService(), + serverContext.streetDetailsService(), serverContext.flexParameters(), FilterMapper.map(request.journey().transit().filters()), request.dateTime(), diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java index 9c990bc48cf..12d3d876105 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java @@ -64,6 +64,7 @@ public static List route(OtpServerRequestContext serverContext, Route serverContext.transitService()::getRegularStop, serverContext.transitService().getTimeZone(), serverContext.graph().streetNotesService, + serverContext.streetDetailsService(), serverContext.graph().ellipsoidToGeoidDifference ); List response = graphPathToItineraryMapper.mapItineraries(paths); diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java index 87912cf6f36..ad02623b7a2 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java @@ -62,6 +62,7 @@ public static Collection routeAccessEgress( FlexRouter flexRouter = new FlexRouter( serverContext.graph(), transitService, + serverContext.streetDetailsService(), config, FilterMapper.map(request.journey().transit().filters()), request.dateTime(), diff --git a/application/src/main/java/org/opentripplanner/routing/graph/SerializedGraphObject.java b/application/src/main/java/org/opentripplanner/routing/graph/SerializedGraphObject.java index f650eb77bd4..ed61b576b73 100644 --- a/application/src/main/java/org/opentripplanner/routing/graph/SerializedGraphObject.java +++ b/application/src/main/java/org/opentripplanner/routing/graph/SerializedGraphObject.java @@ -28,6 +28,7 @@ import org.opentripplanner.routing.fares.FareServiceFactory; import org.opentripplanner.routing.graph.kryosupport.KryoBuilder; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.worldenvelope.WorldEnvelopeRepository; import org.opentripplanner.standalone.config.BuildConfig; @@ -63,6 +64,8 @@ public class SerializedGraphObject implements Serializable { @Nullable public final OsmInfoGraphBuildRepository osmInfoGraphBuildRepository; + public final StreetDetailsRepository streetDetailsRepository; + public final TimetableRepository timetableRepository; public final WorldEnvelopeRepository worldEnvelopeRepository; private final Collection edges; @@ -94,6 +97,7 @@ public class SerializedGraphObject implements Serializable { public SerializedGraphObject( Graph graph, @Nullable OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, + StreetDetailsRepository streetDetailsRepository, TimetableRepository timetableRepository, WorldEnvelopeRepository worldEnvelopeRepository, VehicleParkingRepository parkingRepository, @@ -109,6 +113,7 @@ public SerializedGraphObject( this.graph = graph; this.edges = graph.getEdges(); this.osmInfoGraphBuildRepository = osmInfoGraphBuildRepository; + this.streetDetailsRepository = streetDetailsRepository; this.timetableRepository = timetableRepository; this.worldEnvelopeRepository = worldEnvelopeRepository; this.parkingRepository = parkingRepository; diff --git a/application/src/main/java/org/opentripplanner/service/streetdetails/StreetDetailsRepository.java b/application/src/main/java/org/opentripplanner/service/streetdetails/StreetDetailsRepository.java new file mode 100644 index 00000000000..3ad0d33b06c --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/streetdetails/StreetDetailsRepository.java @@ -0,0 +1,23 @@ +package org.opentripplanner.service.streetdetails; + +import java.io.Serializable; +import java.util.Optional; +import org.opentripplanner.service.streetdetails.model.EdgeLevelInfo; +import org.opentripplanner.street.model.edge.Edge; + +/** + * Store OSM level and incline data used for returning responses to requests. + *

+ * This is a repository to support the {@link StreetDetailsService}. + */ +public interface StreetDetailsRepository extends Serializable { + /** + * Associate the edge with level information. + */ + void addEdgeLevelInformation(Edge edge, EdgeLevelInfo edgeLevelInfo); + + /** + * Find level or incline information for a given edge. + */ + Optional findEdgeInformation(Edge edge); +} diff --git a/application/src/main/java/org/opentripplanner/service/streetdetails/StreetDetailsService.java b/application/src/main/java/org/opentripplanner/service/streetdetails/StreetDetailsService.java new file mode 100644 index 00000000000..122a554bc1e --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/streetdetails/StreetDetailsService.java @@ -0,0 +1,19 @@ +package org.opentripplanner.service.streetdetails; + +import java.util.Optional; +import org.opentripplanner.service.streetdetails.model.EdgeLevelInfo; +import org.opentripplanner.street.model.edge.Edge; + +/** + * The responsibility of this service is to provide details that can not be found in the street + * graph, like level and incline information. The information available from this service is not + * in the street graph because it is not relevant to routing. + * + * THIS SERVICE IS AVAILABLE WHEN THE USER MAKES A REQUEST. + */ +public interface StreetDetailsService { + /** + * Find level or incline information for a given edge. + */ + Optional findEdgeInformation(Edge edge); +} diff --git a/application/src/main/java/org/opentripplanner/service/streetdetails/configure/StreetDetailsRepositoryModule.java b/application/src/main/java/org/opentripplanner/service/streetdetails/configure/StreetDetailsRepositoryModule.java new file mode 100644 index 00000000000..cbfb7da2824 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/streetdetails/configure/StreetDetailsRepositoryModule.java @@ -0,0 +1,12 @@ +package org.opentripplanner.service.streetdetails.configure; + +import dagger.Binds; +import dagger.Module; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsRepository; + +@Module +public interface StreetDetailsRepositoryModule { + @Binds + StreetDetailsRepository bind(DefaultStreetDetailsRepository repository); +} diff --git a/application/src/main/java/org/opentripplanner/service/streetdetails/configure/StreetDetailsServiceModule.java b/application/src/main/java/org/opentripplanner/service/streetdetails/configure/StreetDetailsServiceModule.java new file mode 100644 index 00000000000..0c0c39eba14 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/streetdetails/configure/StreetDetailsServiceModule.java @@ -0,0 +1,12 @@ +package org.opentripplanner.service.streetdetails.configure; + +import dagger.Binds; +import dagger.Module; +import org.opentripplanner.service.streetdetails.StreetDetailsService; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsService; + +@Module +public interface StreetDetailsServiceModule { + @Binds + StreetDetailsService bind(DefaultStreetDetailsService service); +} diff --git a/application/src/main/java/org/opentripplanner/service/streetdetails/internal/DefaultStreetDetailsRepository.java b/application/src/main/java/org/opentripplanner/service/streetdetails/internal/DefaultStreetDetailsRepository.java new file mode 100644 index 00000000000..2c1288910ea --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/streetdetails/internal/DefaultStreetDetailsRepository.java @@ -0,0 +1,38 @@ +package org.opentripplanner.service.streetdetails.internal; + +import jakarta.inject.Inject; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; +import org.opentripplanner.service.streetdetails.model.EdgeLevelInfo; +import org.opentripplanner.street.model.edge.Edge; +import org.opentripplanner.utils.tostring.ToStringBuilder; + +public class DefaultStreetDetailsRepository implements StreetDetailsRepository, Serializable { + + private final Map edgeInformation = new HashMap<>(); + + @Inject + public DefaultStreetDetailsRepository() {} + + @Override + public void addEdgeLevelInformation(Edge edge, EdgeLevelInfo edgeLevelInfo) { + Objects.requireNonNull(edge); + this.edgeInformation.put(edge, edgeLevelInfo); + } + + @Override + public Optional findEdgeInformation(Edge edge) { + return Optional.ofNullable(edgeInformation.get(edge)); + } + + @Override + public String toString() { + return ToStringBuilder.of(DefaultStreetDetailsRepository.class) + .addNum("Edges with level information", edgeInformation.size()) + .toString(); + } +} diff --git a/application/src/main/java/org/opentripplanner/service/streetdetails/internal/DefaultStreetDetailsService.java b/application/src/main/java/org/opentripplanner/service/streetdetails/internal/DefaultStreetDetailsService.java new file mode 100644 index 00000000000..4ba31002330 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/streetdetails/internal/DefaultStreetDetailsService.java @@ -0,0 +1,28 @@ +package org.opentripplanner.service.streetdetails.internal; + +import jakarta.inject.Inject; +import java.util.Optional; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; +import org.opentripplanner.service.streetdetails.StreetDetailsService; +import org.opentripplanner.service.streetdetails.model.EdgeLevelInfo; +import org.opentripplanner.street.model.edge.Edge; + +public class DefaultStreetDetailsService implements StreetDetailsService { + + private final StreetDetailsRepository repository; + + @Inject + public DefaultStreetDetailsService(StreetDetailsRepository repository) { + this.repository = repository; + } + + @Override + public Optional findEdgeInformation(Edge edge) { + return repository.findEdgeInformation(edge); + } + + @Override + public String toString() { + return "DefaultStreetDetailsService{ repository=" + repository + '}'; + } +} diff --git a/application/src/main/java/org/opentripplanner/service/streetdetails/model/EdgeLevelInfo.java b/application/src/main/java/org/opentripplanner/service/streetdetails/model/EdgeLevelInfo.java new file mode 100644 index 00000000000..86db88453b1 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/streetdetails/model/EdgeLevelInfo.java @@ -0,0 +1,62 @@ +package org.opentripplanner.service.streetdetails.model; + +import java.io.Serializable; +import java.util.Objects; +import org.opentripplanner.street.model.edge.Edge; +import org.opentripplanner.street.model.vertex.OsmVertex; + +/** + * Represents level information for an edge. The information is represented as two + * {@link VertexLevelInfo} objects for the first and last vertices of an edge. The lower vertex + * is represented by lowerVertexInfo and the higher one by upperVertexInfo. + */ +public class EdgeLevelInfo implements Serializable { + + private final VertexLevelInfo lowerVertexInfo; + private final VertexLevelInfo upperVertexInfo; + + public EdgeLevelInfo(VertexLevelInfo lowerVertexInfo, VertexLevelInfo upperVertexInfo) { + this.lowerVertexInfo = Objects.requireNonNull(lowerVertexInfo); + this.upperVertexInfo = Objects.requireNonNull(upperVertexInfo); + } + + /** + * Checks if the vertices of the edge match the nodeIds found in the {@link VertexLevelInfo} + * objects. In other words this function checks if the edge level information in this object + * can be used for the given edge. + */ + public boolean canBeAppliedToEdge(Edge edge) { + return ( + edge.getToVertex() instanceof OsmVertex toVertex && + edge.getFromVertex() instanceof OsmVertex fromVertex && + ((lowerVertexInfo.osmNodeId() == fromVertex.nodeId() && + upperVertexInfo.osmNodeId() == toVertex.nodeId()) || + (lowerVertexInfo.osmNodeId() == toVertex.nodeId() && + upperVertexInfo.osmNodeId() == fromVertex.nodeId())) + ); + } + + public VertexLevelInfo lowerVertexInfo() { + return lowerVertexInfo; + } + + public VertexLevelInfo upperVertexInfo() { + return upperVertexInfo; + } + + @Override + public int hashCode() { + return Objects.hash(lowerVertexInfo, upperVertexInfo); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (o == null || o.getClass() != getClass()) return false; + EdgeLevelInfo that = (EdgeLevelInfo) o; + return ( + Objects.equals(lowerVertexInfo, that.lowerVertexInfo) && + Objects.equals(upperVertexInfo, that.upperVertexInfo) + ); + } +} diff --git a/application/src/main/java/org/opentripplanner/service/streetdetails/model/Level.java b/application/src/main/java/org/opentripplanner/service/streetdetails/model/Level.java new file mode 100644 index 00000000000..fffda080604 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/streetdetails/model/Level.java @@ -0,0 +1,45 @@ +package org.opentripplanner.service.streetdetails.model; + +import java.io.Serializable; +import java.util.Objects; +import org.opentripplanner.utils.tostring.ToStringBuilder; + +/** + * Represents a level with a comparable 0-based number and name. + */ +public class Level implements Serializable { + + private final double level; + private final String name; + + public Level(double level, String name) { + this.level = level; + this.name = name; + } + + public double level() { + return this.level; + } + + public String name() { + return this.name; + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (o == null || o.getClass() != this.getClass()) return false; + Level that = (Level) o; + return this.level == that.level && Objects.equals(this.name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(level, name); + } + + @Override + public String toString() { + return ToStringBuilder.of(getClass()).addNum("level", level).addStr("name", name).toString(); + } +} diff --git a/application/src/main/java/org/opentripplanner/service/streetdetails/model/VertexLevelInfo.java b/application/src/main/java/org/opentripplanner/service/streetdetails/model/VertexLevelInfo.java new file mode 100644 index 00000000000..37d6b85a8ee --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/streetdetails/model/VertexLevelInfo.java @@ -0,0 +1,53 @@ +package org.opentripplanner.service.streetdetails.model; + +import java.io.Serializable; +import java.util.Objects; +import javax.annotation.Nullable; +import org.opentripplanner.utils.tostring.ToStringBuilder; + +/** + * Represents level information for a vertex. The {@link Level} is nullable because sometimes only + * the vertical order is known. The osmNodeId is necessary to reliably map the level to the correct + * vertex. + */ +public class VertexLevelInfo implements Serializable { + + @Nullable + private final Level level; + + private final long osmNodeId; + + public VertexLevelInfo(@Nullable Level level, long osmNodeId) { + this.level = level; + this.osmNodeId = osmNodeId; + } + + public Level level() { + return this.level; + } + + public long osmNodeId() { + return this.osmNodeId; + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (o == null || o.getClass() != getClass()) return false; + VertexLevelInfo that = (VertexLevelInfo) o; + return Objects.equals(this.level, that.level) && osmNodeId == that.osmNodeId; + } + + @Override + public int hashCode() { + return Objects.hash(level, osmNodeId); + } + + @Override + public String toString() { + return ToStringBuilder.of(getClass()) + .addObj("level", level) + .addNum("osmNodeId", osmNodeId) + .toString(); + } +} diff --git a/application/src/main/java/org/opentripplanner/standalone/OTPMain.java b/application/src/main/java/org/opentripplanner/standalone/OTPMain.java index bff38d1588f..72affc722e4 100644 --- a/application/src/main/java/org/opentripplanner/standalone/OTPMain.java +++ b/application/src/main/java/org/opentripplanner/standalone/OTPMain.java @@ -147,6 +147,7 @@ private static void startOTPServer(CommandLineParameters cli) { new SerializedGraphObject( app.graph(), app.osmInfoGraphBuildRepository(), + app.streetDetailsRepository(), app.timetableRepository(), app.worldEnvelopeRepository(), app.vehicleParkingRepository(), diff --git a/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index 82cce821d4a..a76b233fe66 100644 --- a/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -30,6 +30,7 @@ import org.opentripplanner.routing.linking.VertexLinker; import org.opentripplanner.routing.via.ViaCoordinateTransferFactory; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; +import org.opentripplanner.service.streetdetails.StreetDetailsService; import org.opentripplanner.service.vehicleparking.VehicleParkingService; import org.opentripplanner.service.vehiclerental.VehicleRentalService; import org.opentripplanner.service.worldenvelope.WorldEnvelopeService; @@ -162,6 +163,8 @@ default List listExtensionRequestContexts(RouteRequest @Nullable ItineraryDecorator emissionItineraryDecorator(); + StreetDetailsService streetDetailsService(); + @Nullable EmpiricalDelayService empiricalDelayService(); diff --git a/application/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java b/application/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java index bae57b893bf..61ba48df4a5 100644 --- a/application/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java +++ b/application/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java @@ -7,6 +7,7 @@ import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_2; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_5; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_7; +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_9; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.MissingNode; @@ -178,6 +179,7 @@ public class BuildConfig implements OtpDataStoreConfig { public final TransitFeeds transitFeeds; public final boolean staticParkAndRide; public final boolean staticBikeParkAndRide; + public final boolean includeEdgeLevelInfo; public final double distanceBetweenElevationSamples; public final double maxElevationPropagationMeters; public final boolean readCachedElevations; @@ -345,16 +347,21 @@ When set to true (it is false by default), the elevation module will include the """ ) .asBoolean(true); - staticBikeParkAndRide = root - .of("staticBikeParkAndRide") - .since(V1_5) - .summary("Whether we should create bike P+R stations from OSM data.") - .asBoolean(false); staticParkAndRide = root .of("staticParkAndRide") .since(V1_5) .summary("Whether we should create car P+R stations from OSM data.") .asBoolean(true); + staticBikeParkAndRide = root + .of("staticBikeParkAndRide") + .since(V1_5) + .summary("Whether we should create bike P+R stations from OSM data.") + .asBoolean(false); + includeEdgeLevelInfo = root + .of("includeEdgeLevelInfo") + .since(V2_9) + .summary("Whether level info for edges should be stored in the StreetDetailsService.") + .asBoolean(false); subwayAccessTime = root .of("subwayAccessTime") .since(V1_5) diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java index 9baeae7b195..fd192bf3621 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java @@ -21,6 +21,7 @@ import org.opentripplanner.routing.linking.VertexLinker; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingService; import org.opentripplanner.service.vehiclerental.VehicleRentalRepository; @@ -80,6 +81,7 @@ public class ConstructApplication { CommandLineParameters cli, Graph graph, OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, + StreetDetailsRepository streetDetailsRepository, TimetableRepository timetableRepository, WorldEnvelopeRepository worldEnvelopeRepository, ConfigModel config, @@ -104,6 +106,7 @@ public class ConstructApplication { this.factory = builder .configModel(config) .graph(graph) + .streetDetailsRepository(streetDetailsRepository) .timetableRepository(timetableRepository) .graphVisualizer(graphVisualizer) .worldEnvelopeRepository(worldEnvelopeRepository) @@ -144,6 +147,7 @@ public GraphBuilder createGraphBuilder() { graphBuilderDataSources, graph(), osmInfoGraphBuildRepository, + factory.streetDetailsRepository(), fareServiceFactory(), factory.timetableRepository(), factory.worldEnvelopeRepository(), @@ -353,6 +357,10 @@ public EmissionRepository emissionRepository() { return factory.emissionRepository(); } + public StreetDetailsRepository streetDetailsRepository() { + return factory.streetDetailsRepository(); + } + @Nullable public EmpiricalDelayRepository empiricalDelayRepository() { return factory.empiricalDelayRepository(); diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java index b60725b52fb..876a53ccb56 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java @@ -35,6 +35,8 @@ import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; import org.opentripplanner.service.realtimevehicles.configure.RealtimeVehicleRepositoryModule; import org.opentripplanner.service.realtimevehicles.configure.RealtimeVehicleServiceModule; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; +import org.opentripplanner.service.streetdetails.configure.StreetDetailsServiceModule; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingService; import org.opentripplanner.service.vehicleparking.configure.VehicleParkingServiceModule; @@ -70,6 +72,7 @@ EmpiricalDelayServiceModule.class, GeocoderModule.class, InteractiveLauncherModule.class, + StreetDetailsServiceModule.class, RealtimeVehicleServiceModule.class, RealtimeVehicleRepositoryModule.class, RideHailingServicesModule.class, @@ -107,6 +110,8 @@ public interface ConstructApplicationFactory { @Nullable EmissionRepository emissionRepository(); + StreetDetailsRepository streetDetailsRepository(); + @Nullable EmpiricalDelayRepository empiricalDelayRepository(); @@ -172,6 +177,9 @@ Builder stopConsolidationRepository( @BindsInstance Builder emissionRepository(EmissionRepository emissionRepository); + @BindsInstance + Builder streetDetailsRepository(StreetDetailsRepository streetDetailsRepository); + @BindsInstance Builder empiricalDelayRepository(EmpiricalDelayRepository empiricalDelayRepository); diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java index 50191eddad0..b7d327aeab4 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java @@ -26,6 +26,7 @@ import org.opentripplanner.routing.linking.VertexLinker; import org.opentripplanner.routing.via.ViaCoordinateTransferFactory; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; +import org.opentripplanner.service.streetdetails.StreetDetailsService; import org.opentripplanner.service.vehicleparking.VehicleParkingService; import org.opentripplanner.service.vehiclerental.VehicleRentalService; import org.opentripplanner.service.worldenvelope.WorldEnvelopeService; @@ -58,6 +59,7 @@ OtpServerRequestContext providesServerContext( StreetLimitationParametersService streetLimitationParametersService, @Nullable TraverseVisitor traverseVisitor, @Nullable @EmissionDecorator ItineraryDecorator emissionItineraryDecorator, + StreetDetailsService streetDetailsService, @Nullable @GtfsSchema GraphQLSchema gtfsSchema, @Nullable @TransmodelSchema GraphQLSchema transmodelSchema, @Nullable EmpiricalDelayService empiricalDelayService, @@ -98,6 +100,7 @@ OtpServerRequestContext providesServerContext( worldEnvelopeService, // Optional Sandbox services emissionItineraryDecorator, + streetDetailsService, empiricalDelayService, luceneIndex, gtfsSchema, diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/LoadApplication.java b/application/src/main/java/org/opentripplanner/standalone/configure/LoadApplication.java index a8688e2c4a0..4cae3d21efe 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/LoadApplication.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/LoadApplication.java @@ -11,6 +11,7 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.SerializedGraphObject; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.worldenvelope.WorldEnvelopeRepository; import org.opentripplanner.standalone.config.CommandLineParameters; @@ -59,6 +60,7 @@ public ConstructApplication appConstruction(SerializedGraphObject obj) { return createAppConstruction( obj.graph, obj.osmInfoGraphBuildRepository, + obj.streetDetailsRepository, obj.timetableRepository, obj.worldEnvelopeRepository, obj.parkingRepository, @@ -76,6 +78,7 @@ public ConstructApplication appConstruction() { return createAppConstruction( factory.emptyGraph(), factory.emptyOsmInfoGraphBuildRepository(), + factory.emptyStreetDetailsRepository(), factory.emptyTimetableRepository(), factory.emptyWorldEnvelopeRepository(), factory.emptyVehicleParkingRepository(), @@ -102,6 +105,7 @@ public ConfigModel config() { private ConstructApplication createAppConstruction( Graph graph, OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, + StreetDetailsRepository streetDetailsRepository, TimetableRepository timetableRepository, WorldEnvelopeRepository worldEnvelopeRepository, VehicleParkingRepository parkingRepository, @@ -116,6 +120,7 @@ private ConstructApplication createAppConstruction( cli, graph, osmInfoGraphBuildRepository, + streetDetailsRepository, timetableRepository, worldEnvelopeRepository, config(), diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/LoadApplicationFactory.java b/application/src/main/java/org/opentripplanner/standalone/configure/LoadApplicationFactory.java index 241a3eebb5f..1735b16edfb 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/LoadApplicationFactory.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/LoadApplicationFactory.java @@ -19,6 +19,8 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; import org.opentripplanner.service.osminfo.configure.OsmInfoGraphBuildRepositoryModule; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; +import org.opentripplanner.service.streetdetails.configure.StreetDetailsRepositoryModule; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.vehicleparking.configure.VehicleParkingRepositoryModule; import org.opentripplanner.service.worldenvelope.WorldEnvelopeRepository; @@ -39,6 +41,7 @@ DataStoreModule.class, GsDataSourceModule.class, OsmInfoGraphBuildRepositoryModule.class, + StreetDetailsRepositoryModule.class, WorldEnvelopeRepositoryModule.class, EmissionRepositoryModule.class, EmpiricalDelayRepositoryModule.class, @@ -58,6 +61,9 @@ public interface LoadApplicationFactory { @Singleton OsmInfoGraphBuildRepository emptyOsmInfoGraphBuildRepository(); + @Singleton + StreetDetailsRepository emptyStreetDetailsRepository(); + @Singleton TimetableRepository emptyTimetableRepository(); diff --git a/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java b/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java index c36a6496d5d..e9009444e68 100644 --- a/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java +++ b/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java @@ -29,6 +29,7 @@ import org.opentripplanner.routing.service.DefaultRoutingService; import org.opentripplanner.routing.via.ViaCoordinateTransferFactory; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; +import org.opentripplanner.service.streetdetails.StreetDetailsService; import org.opentripplanner.service.vehicleparking.VehicleParkingService; import org.opentripplanner.service.vehiclerental.VehicleRentalService; import org.opentripplanner.service.worldenvelope.WorldEnvelopeService; @@ -69,6 +70,8 @@ public class DefaultServerRequestContext implements OtpServerRequestContext { @Nullable private final ItineraryDecorator emissionItineraryDecorator; + private final StreetDetailsService streetDetailsService; + @Nullable private final EmpiricalDelayService empiricalDelayService; @@ -129,6 +132,7 @@ public DefaultServerRequestContext( ViaCoordinateTransferFactory viaTransferResolver, WorldEnvelopeService worldEnvelopeService, @Nullable ItineraryDecorator emissionItineraryDecorator, + StreetDetailsService streetDetailsService, @Nullable EmpiricalDelayService empiricalDelayService, @Nullable LuceneIndex luceneIndex, @Nullable @GtfsSchema GraphQLSchema gtfsSchema, @@ -162,6 +166,7 @@ public DefaultServerRequestContext( // Optional fields this.emissionItineraryDecorator = emissionItineraryDecorator; + this.streetDetailsService = streetDetailsService; this.empiricalDelayService = empiricalDelayService; this.luceneIndex = luceneIndex; this.gtfsSchema = gtfsSchema; @@ -309,6 +314,11 @@ public ItineraryDecorator emissionItineraryDecorator() { return emissionItineraryDecorator; } + @Override + public StreetDetailsService streetDetailsService() { + return streetDetailsService; + } + @Override public EmpiricalDelayService empiricalDelayService() { return empiricalDelayService; diff --git a/application/src/main/java/org/opentripplanner/street/model/vertex/BarrierPassThroughVertex.java b/application/src/main/java/org/opentripplanner/street/model/vertex/BarrierPassThroughVertex.java index c2b7ec08f39..f55504348eb 100644 --- a/application/src/main/java/org/opentripplanner/street/model/vertex/BarrierPassThroughVertex.java +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/BarrierPassThroughVertex.java @@ -5,14 +5,19 @@ */ public class BarrierPassThroughVertex extends OsmVertex { - public final long wayId; + private final long wayId; public BarrierPassThroughVertex(double x, double y, long nodeId, long wayId) { super(x, y, nodeId); this.wayId = wayId; } + public long wayId() { + return this.wayId; + } + + @Override public VertexLabel getLabel() { - return new VertexLabel.NodeOnWayLabel(nodeId, wayId); + return new VertexLabel.NodeOnWayLabel(nodeId(), wayId); } } diff --git a/application/src/main/java/org/opentripplanner/street/model/vertex/OsmVertex.java b/application/src/main/java/org/opentripplanner/street/model/vertex/OsmVertex.java index 39d195f695e..846b2d3f11a 100644 --- a/application/src/main/java/org/opentripplanner/street/model/vertex/OsmVertex.java +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/OsmVertex.java @@ -9,8 +9,7 @@ */ public class OsmVertex extends IntersectionVertex { - /** The OSM node ID from whence this came */ - public final long nodeId; + private final long nodeId; public OsmVertex(double x, double y, long nodeId) { super(x, y); @@ -28,6 +27,13 @@ public OsmVertex( this.nodeId = nodeId; } + /** + * The OSM node ID from whence this came. + */ + public long nodeId() { + return this.nodeId; + } + @Override public I18NString getName() { return NO_NAME; diff --git a/application/src/main/java/org/opentripplanner/street/model/vertex/OsmVertexOnLevel.java b/application/src/main/java/org/opentripplanner/street/model/vertex/OsmVertexOnLevel.java index c96878e228a..14b7c273603 100644 --- a/application/src/main/java/org/opentripplanner/street/model/vertex/OsmVertexOnLevel.java +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/OsmVertexOnLevel.java @@ -19,6 +19,6 @@ public OsmVertexOnLevel(OsmNode node, String level) { @Override public VertexLabel getLabel() { - return VertexLabel.osm(nodeId, level); + return VertexLabel.osm(nodeId(), level); } } diff --git a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java index f768ac81baa..635dede65e6 100644 --- a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java @@ -29,7 +29,7 @@ public StationEntranceVertex( * The id of the entrance which may or may not be human-readable. */ public FeedScopedId id() { - return new FeedScopedId(FEED_ID, String.valueOf(nodeId)); + return new FeedScopedId(FEED_ID, String.valueOf(nodeId())); } /** @@ -44,7 +44,7 @@ public String code() { @Override public String toString() { return ToStringBuilder.of(StationEntranceVertex.class) - .addNum("nodeId", nodeId) + .addNum("nodeId", nodeId()) .addStr("code", code) .toString(); } diff --git a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 8d058cca7c2..8929af25967 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -97,7 +97,7 @@ union CallStopLocation = Location | LocationGroup | Stop union RentalPlace = RentalVehicle | VehicleRentalStation "A feature for a step" -union StepFeature = Entrance +union StepFeature = Entrance | EscalatorUse | StairsUse union StopPosition = PositionAtStop | PositionBetweenStops @@ -542,6 +542,15 @@ type Entrance { wheelchairAccessible: WheelchairBoarding } +"A single use of an escalator." +type EscalatorUse { + "The level the use begins at." + from: Level + "The level the use ends at." + to: Level + verticalDirection: VerticalDirection! +} + "Real-time estimates for an arrival or departure at a certain place." type EstimatedTime { """ @@ -902,6 +911,14 @@ type LegTime { scheduledTime: OffsetDateTime! } +"A level with a name and comparable number. Levels can sometimes contain half levels, e.g. '1.5'." +type Level { + "0-based comparable number where 0 is the ground level." + level: Float! + "Name of the level, e.g. 'M', 'P1', or '1'. Can be equal or different to the numerical representation." + name: String! +} + "A span of time." type LocalTimeSpan { "The start of the time timespan as seconds from midnight." @@ -2170,6 +2187,15 @@ type RoutingError { inputField: InputField } +"A single use of a set of stairs." +type StairsUse { + "The level the use begins at." + from: Level + "The level the use ends at." + to: Level + verticalDirection: VerticalDirection! +} + """ Stop can represent either a single public transport stop, where passengers can board and/or disembark vehicles, or a station, which contains multiple stops. @@ -2976,7 +3002,7 @@ type step { elevationProfile: [elevationProfileComponent] "When exiting a highway or traffic circle, the exit name/number." exit: String - "Information about an feature associated with a step e.g. an station entrance or exit" + "Information about a feature associated with a step e.g. a station entrance or exit." feature: StepFeature "The latitude of the start of the step." lat: Float @@ -3694,6 +3720,7 @@ enum RelativeDirection { More information about the entrance is in the `step.feature` field. """ ENTER_STATION + ESCALATOR """ Exiting a public transport station. If it's not known if the passenger is entering or exiting then `CONTINUE` is used. @@ -3709,6 +3736,7 @@ enum RelativeDirection { RIGHT SLIGHTLY_LEFT SLIGHTLY_RIGHT + STAIRS UTURN_LEFT UTURN_RIGHT } @@ -3924,6 +3952,12 @@ enum VertexType { TRANSIT } +"The vertical direction e.g. for a set of stairs." +enum VerticalDirection { + DOWN + UP +} + enum WheelchairBoarding { "Wheelchair boarding is not possible at this stop." NOT_POSSIBLE diff --git a/application/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/application/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index c55f3815b7f..f16b0a8c660 100644 --- a/application/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/application/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -1802,6 +1802,7 @@ enum RelativeDirection { depart elevator enterStation + escalator exitStation followSigns hardLeft @@ -1810,6 +1811,7 @@ enum RelativeDirection { right slightlyLeft slightlyRight + stairs uturnLeft uturnRight } diff --git a/application/src/test/java/org/opentripplanner/TestServerContext.java b/application/src/test/java/org/opentripplanner/TestServerContext.java index b9d5d02238a..243b65769c2 100644 --- a/application/src/test/java/org/opentripplanner/TestServerContext.java +++ b/application/src/test/java/org/opentripplanner/TestServerContext.java @@ -22,6 +22,9 @@ import org.opentripplanner.routing.via.service.DefaultViaCoordinateTransferFactory; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; +import org.opentripplanner.service.streetdetails.StreetDetailsService; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsRepository; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsService; import org.opentripplanner.service.vehicleparking.VehicleParkingService; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingService; @@ -116,6 +119,7 @@ public static OtpServerRequestContext createServerContext( createViaTransferResolver(graph, transitService), createWorldEnvelopeService(), createEmissionsItineraryDecorator(), + createStreetDetailsService(), null, null, null, @@ -160,6 +164,10 @@ public static ItineraryDecorator createEmissionsItineraryDecorator() { ); } + public static StreetDetailsService createStreetDetailsService() { + return new DefaultStreetDetailsService(new DefaultStreetDetailsRepository()); + } + public static StreetLimitationParametersService createStreetLimitationParametersService() { return new DefaultStreetLimitationParametersService(new StreetLimitationParameters()); } diff --git a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index 3cb65775850..e0ee773d6f7 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -66,6 +66,7 @@ import org.opentripplanner.model.plan.walkstep.RelativeDirection; import org.opentripplanner.model.plan.walkstep.WalkStep; import org.opentripplanner.model.plan.walkstep.WalkStepBuilder; +import org.opentripplanner.model.plan.walkstep.verticaltransportation.VerticalTransportationUseFactory; import org.opentripplanner.routing.alertpatch.AlertCause; import org.opentripplanner.routing.alertpatch.AlertEffect; import org.opentripplanner.routing.alertpatch.AlertSeverity; @@ -81,6 +82,11 @@ import org.opentripplanner.routing.services.TransitAlertService; import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsRepository; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsService; +import org.opentripplanner.service.streetdetails.model.EdgeLevelInfo; +import org.opentripplanner.service.streetdetails.model.Level; +import org.opentripplanner.service.streetdetails.model.VertexLevelInfo; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingService; @@ -91,6 +97,10 @@ import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; import org.opentripplanner.service.vehiclerental.model.VehicleRentalVehicle; import org.opentripplanner.standalone.config.framework.json.JsonSupport; +import org.opentripplanner.street.model.StreetTraversalPermission; +import org.opentripplanner.street.model.edge.EscalatorEdge; +import org.opentripplanner.street.model.edge.StreetEdgeBuilder; +import org.opentripplanner.street.model.vertex.OsmVertex; import org.opentripplanner.test.support.FilePatternSource; import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; import org.opentripplanner.transit.model.basic.Accessibility; @@ -350,6 +360,44 @@ public Collection listRoutes() { .withEntrance(entrance) .build(); + var streetDetailsRepository = new DefaultStreetDetailsRepository(); + OsmVertex from = new OsmVertex(10, 10, 0); + OsmVertex to = new OsmVertex(10.001, 10.001, 1); + var stairsEdge = new StreetEdgeBuilder<>() + .withFromVertex(from) + .withToVertex(to) + .withName("stairs") + .withMeterLength(1000) + .withPermission(StreetTraversalPermission.ALL) + .withBack(false) + .withStairs(true) + .buildAndConnect(); + var escalatorEdge = EscalatorEdge.createEscalatorEdge(from, to, 45, null); + var edgeLevelInfo = new EdgeLevelInfo( + new VertexLevelInfo(new Level(1.0, "1"), 1), + new VertexLevelInfo(new Level(2.0, "2"), 2) + ); + streetDetailsRepository.addEdgeLevelInformation(stairsEdge, edgeLevelInfo); + streetDetailsRepository.addEdgeLevelInformation(escalatorEdge, edgeLevelInfo); + VerticalTransportationUseFactory verticalTransportationUseFactory = + new VerticalTransportationUseFactory( + new DefaultStreetDetailsService(streetDetailsRepository) + ); + var step4 = walkStep("stairs") + .withRelativeDirection(RelativeDirection.STAIRS) + .withVerticalTransportationUse( + verticalTransportationUseFactory.createInclinedVerticalTransportationUse(stairsEdge) + ) + .addEdge(stairsEdge) + .build(); + var step5 = walkStep("escalators") + .withRelativeDirection(RelativeDirection.ESCALATOR) + .withVerticalTransportationUse( + verticalTransportationUseFactory.createInclinedVerticalTransportationUse(escalatorEdge) + ) + .addEdge(escalatorEdge) + .build(); + var entitySelector = new EntitySelector.Stop(A.stop.getId()); var stationEntitySelector = new EntitySelector.Stop(OMEGA.getId()); var alert = TransitAlert.of(id("an-alert")) @@ -375,7 +423,7 @@ public Collection listRoutes() { // the ItineraryBuilder and not going back and forth between the Itinerary and the // builder. var i1 = newItinerary(A, T11_00) - .walk(20, B, List.of(step1, step2, step3)) + .walk(20, B, List.of(step1, step2, step3, step4, step5)) .bus(busRoute, 122, T11_01, T11_15, C) .rail(439, T11_30, T11_50, D) .carHail(D10m, E) diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/OsmModuleTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/OsmModuleTest.java index 405d05147ce..8cfc5bc774e 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/OsmModuleTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/OsmModuleTest.java @@ -18,11 +18,13 @@ import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.opentripplanner.astar.model.GraphPath; import org.opentripplanner.framework.i18n.LocalizedString; import org.opentripplanner.framework.i18n.NonLocalizedString; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.graph_builder.issue.service.DefaultDataImportIssueStore; import org.opentripplanner.graph_builder.issues.BarrierIntersectingHighway; import org.opentripplanner.graph_builder.module.osm.moduletests._support.TestOsmProvider; @@ -44,6 +46,11 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.impl.GraphPathFinder; import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildRepository; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsRepository; +import org.opentripplanner.service.streetdetails.model.EdgeLevelInfo; +import org.opentripplanner.service.streetdetails.model.Level; +import org.opentripplanner.service.streetdetails.model.VertexLevelInfo; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingService; @@ -469,14 +476,14 @@ void testHighwayReachingBarrierOnArea() { assertEquals(3, graph.getVerticesOfType(BarrierPassThroughVertex.class).size()); assertEquals( 2, - graph.getVerticesOfType(OsmVertex.class).stream().filter(v -> v.nodeId == 1).toList().size() + graph.getVerticesOfType(OsmVertex.class).stream().filter(v -> v.nodeId() == 1).toList().size() ); // check traversal permission starting from node 2 var v2 = graph .getVerticesOfType(OsmVertex.class) .stream() - .filter(v -> v.nodeId == 2) + .filter(v -> v.nodeId() == 2) .findFirst() .orElseThrow(); assertEquals(1, v2.getOutgoing().size()); @@ -504,8 +511,6 @@ void testHighwayReachingBarrierOnArea() { @Test void testDifferentLevelsConnectingBarrier() { - Graph graph = new Graph(); - var n1 = new OsmNode(0, 0); n1.setId(1); var n2 = new OsmNode(0, 1); @@ -570,7 +575,7 @@ void testDifferentLevelsConnectingBarrier() { var osmModule = OsmModule.of( osmProvider, - graph, + new Graph(), new DefaultOsmInfoGraphBuildRepository(), new DefaultVehicleParkingRepository() ) @@ -584,6 +589,119 @@ void testDifferentLevelsConnectingBarrier() { assertEquals(4, issues[1].node().getId()); } + @Test + void testEdgeLevelInfo() { + var n1 = new OsmNode(0, 0); + n1.setId(1); + var n2 = new OsmNode(0, 1); + n2.setId(2); + + var levelStairs = new OsmWay(); + levelStairs.setId(1); + levelStairs.addTag("highway", "steps"); + levelStairs.addTag("incline", "up"); + levelStairs.addTag("level", "1;2"); + levelStairs.addNodeRef(1); + levelStairs.addNodeRef(2); + + var inclineStairs = new OsmWay(); + inclineStairs.setId(2); + inclineStairs.addTag("highway", "steps"); + inclineStairs.addTag("incline", "up"); + inclineStairs.addNodeRef(1); + inclineStairs.addNodeRef(2); + + var escalator = new OsmWay(); + escalator.setId(3); + escalator.addTag("highway", "steps"); + escalator.addTag("conveying", "yes"); + escalator.addTag("level", "1;-1"); + escalator.addTag("level:ref", "1;P1"); + escalator.addNodeRef(1); + escalator.addNodeRef(2); + + var osmProvider = new TestOsmProvider( + List.of(), + List.of(levelStairs, inclineStairs, escalator), + List.of(n1, n2) + ); + var osmDb = new OsmDatabase(DataImportIssueStore.NOOP); + osmProvider.readOsm(osmDb); + var graph = new Graph(); + var streetDetailsRepository = new DefaultStreetDetailsRepository(); + var osmModule = OsmModule.of( + osmProvider, + graph, + new DefaultOsmInfoGraphBuildRepository(), + new DefaultVehicleParkingRepository() + ) + .withStreetDetailsRepository(streetDetailsRepository) + // The build config field that needs to bet set for street info to be stored. + .withIncludeEdgeLevelInfo(true) + .build(); + osmModule.buildGraph(); + + var edgeLevelInfoSet = Set.of( + new EdgeLevelInfo( + new VertexLevelInfo(new Level(1.0, "1"), 1), + new VertexLevelInfo(new Level(2.0, "2"), 2) + ), + new EdgeLevelInfo(new VertexLevelInfo(null, 1), new VertexLevelInfo(null, 2)), + new EdgeLevelInfo( + new VertexLevelInfo(new Level(-1.0, "P1"), 2), + new VertexLevelInfo(new Level(1.0, "1"), 1) + ) + ); + assertEquals(edgeLevelInfoSet, getAllEdgeLevelInfoObjects(graph, streetDetailsRepository)); + } + + @Test + void testEdgeLevelInfoNotStoredWithoutIncludeEdgeLevelInfo() { + var n1 = new OsmNode(0, 0); + n1.setId(1); + var n2 = new OsmNode(0, 1); + n2.setId(2); + + var inclineStairs = new OsmWay(); + inclineStairs.setId(2); + inclineStairs.addTag("highway", "steps"); + inclineStairs.addTag("incline", "up"); + inclineStairs.addNodeRef(1); + inclineStairs.addNodeRef(2); + + var osmProvider = new TestOsmProvider(List.of(), List.of(inclineStairs), List.of(n1, n2)); + var osmDb = new OsmDatabase(DataImportIssueStore.NOOP); + osmProvider.readOsm(osmDb); + var graph = new Graph(); + var streetDetailsRepository = new DefaultStreetDetailsRepository(); + var osmModule = OsmModule.of( + osmProvider, + graph, + new DefaultOsmInfoGraphBuildRepository(), + new DefaultVehicleParkingRepository() + ) + .withStreetDetailsRepository(streetDetailsRepository) + // The build config field that needs to bet set for street info to be stored. + .withIncludeEdgeLevelInfo(false) + .build(); + osmModule.buildGraph(); + + assertEquals(Set.of(), getAllEdgeLevelInfoObjects(graph, streetDetailsRepository)); + } + + private Set getAllEdgeLevelInfoObjects( + Graph graph, + StreetDetailsRepository streetDetailsRepository + ) { + return graph + .getEdges() + .stream() + .flatMap(edge -> + streetDetailsRepository.findEdgeInformation(edge).map(Stream::of).orElseGet(Stream::empty) + ) + .collect(Collectors.toSet()); + } + private BuildResult buildParkingLots() { var graph = new Graph(); var service = new DefaultVehicleParkingRepository(); diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/VertexGeneratorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/VertexGeneratorTest.java index 3b5e28be9a1..2ca2d957b4d 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/VertexGeneratorTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/VertexGeneratorTest.java @@ -109,8 +109,8 @@ void testBarrierGenerator() { assertEquals(vertexForW1NotOnBarrier, vertexForW2NotOnBarrier); assertInstanceOf(BarrierPassThroughVertex.class, vertexForW2OnBarrier); - assertEquals(n3.getId(), ((BarrierPassThroughVertex) vertexForW2OnBarrier).nodeId); - assertEquals(w2.getId(), ((BarrierPassThroughVertex) vertexForW2OnBarrier).wayId); + assertEquals(n3.getId(), ((BarrierPassThroughVertex) vertexForW2OnBarrier).nodeId()); + assertEquals(w2.getId(), ((BarrierPassThroughVertex) vertexForW2OnBarrier).wayId()); assertFalse(vertexForW2NotOnBarrier instanceof BarrierPassThroughVertex); Map> splitVerticesOnBarriers = diff --git a/application/src/test/java/org/opentripplanner/osm/model/OsmWayTest.java b/application/src/test/java/org/opentripplanner/osm/model/OsmWayTest.java index 7fbf704a78c..90704b922ef 100644 --- a/application/src/test/java/org/opentripplanner/osm/model/OsmWayTest.java +++ b/application/src/test/java/org/opentripplanner/osm/model/OsmWayTest.java @@ -87,6 +87,21 @@ void testIsSteps() { assertTrue(way.isSteps()); } + @Test + void testIsStairs() { + OsmWay way = new OsmWay(); + assertFalse(way.isStairs()); + + way.addTag("highway", "primary"); + assertFalse(way.isStairs()); + + way.addTag("highway", "steps"); + assertTrue(way.isStairs()); + + way.addTag("conveying", "yes"); + assertFalse(way.isStairs()); + } + @Test void wheelchairAccessibleStairs() { var osm1 = new OsmWay(); @@ -213,7 +228,7 @@ void testIsOpposableCycleway() { } @Test - void escalator() { + void testIsEscalator() { assertFalse(WayTestData.highwayWithCycleLane().isEscalator()); var escalator = new OsmWay(); @@ -225,6 +240,12 @@ void escalator() { escalator.addTag("conveying", "whoknows?"); assertFalse(escalator.isEscalator()); + + escalator.addTag("conveying", "forward"); + assertTrue(escalator.isForwardEscalator()); + + escalator.addTag("conveying", "backward"); + assertTrue(escalator.isBackwardEscalator()); } @Test diff --git a/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapperTest.java b/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapperTest.java index 11ee30f8c8b..ebda8a84f39 100644 --- a/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapperTest.java +++ b/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapperTest.java @@ -9,6 +9,8 @@ import org.opentripplanner._support.time.ZoneIds; import org.opentripplanner.astar.model.GraphPath; import org.opentripplanner.routing.services.notes.StreetNotesService; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsRepository; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsService; import org.opentripplanner.street.search.state.State; import org.opentripplanner.street.search.state.TestStateBuilder; @@ -35,6 +37,7 @@ void isSearchWindowAware(State state) { id -> null, ZoneIds.UTC, new StreetNotesService(), + new DefaultStreetDetailsService(new DefaultStreetDetailsRepository()), 1 ); var itin = mapper.generateItinerary(new GraphPath<>(state)); diff --git a/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java b/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java index 1169257025a..da88a214597 100644 --- a/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java +++ b/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java @@ -57,6 +57,8 @@ import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsRepository; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsService; import org.opentripplanner.street.search.state.State; import org.opentripplanner.street.search.state.TestStateBuilder; import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; @@ -302,6 +304,7 @@ private RaptorPathToItineraryMapper getRaptorPathToItineraryMa return new RaptorPathToItineraryMapper<>( new Graph(), new DefaultTransitService(timetableRepository), + new DefaultStreetDetailsService(new DefaultStreetDetailsRepository()), getRaptorTransitData(), dateTime.atZone(ZoneIds.CET), RouteRequest.defaultValue() diff --git a/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapperTest.java b/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapperTest.java index 5c8056da2d1..7a38326e477 100644 --- a/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapperTest.java +++ b/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapperTest.java @@ -19,6 +19,8 @@ import org.opentripplanner.model.plan.walkstep.WalkStep; import org.opentripplanner.model.plan.walkstep.WalkStepBuilder; import org.opentripplanner.routing.services.notes.StreetNotesService; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsRepository; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsService; import org.opentripplanner.street.search.state.TestStateBuilder; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -41,6 +43,26 @@ void elevator() { assertTrue(elevatorStep.getAbsoluteDirection().isEmpty()); } + @Test + void stairs() { + var walkSteps = buildWalkSteps( + TestStateBuilder.ofWalking().streetEdge().stairsEdge().streetEdge() + ); + assertEquals(RelativeDirection.DEPART, walkSteps.get(0).getRelativeDirection()); + assertEquals(RelativeDirection.STAIRS, walkSteps.get(1).getRelativeDirection()); + assertEquals(RelativeDirection.CONTINUE, walkSteps.get(2).getRelativeDirection()); + } + + @Test + void escalator() { + var walkSteps = buildWalkSteps( + TestStateBuilder.ofWalking().streetEdge().escalatorEdge().streetEdge() + ); + assertEquals(RelativeDirection.DEPART, walkSteps.get(0).getRelativeDirection()); + assertEquals(RelativeDirection.ESCALATOR, walkSteps.get(1).getRelativeDirection()); + assertEquals(RelativeDirection.CONTINUE, walkSteps.get(2).getRelativeDirection()); + } + @Test void stationEntrance() { var walkSteps = buildWalkSteps( @@ -94,7 +116,13 @@ void signpostedPathway() { private static List buildWalkSteps(TestStateBuilder builder) { var result = builder.build(); var path = new GraphPath<>(result); - var mapper = new StatesToWalkStepsMapper(path.states, null, new StreetNotesService(), 0); + var mapper = new StatesToWalkStepsMapper( + path.states, + null, + new StreetNotesService(), + new DefaultStreetDetailsService(new DefaultStreetDetailsRepository()), + 0 + ); return mapper.generateWalkSteps(); } diff --git a/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/_support/mapping/RelativeDirectionMapper.java b/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/_support/mapping/RelativeDirectionMapper.java index 747f6fb541e..c8419fa295a 100644 --- a/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/_support/mapping/RelativeDirectionMapper.java +++ b/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/_support/mapping/RelativeDirectionMapper.java @@ -22,6 +22,8 @@ public static ApiRelativeDirection mapRelativeDirection(RelativeDirection domain case CIRCLE_CLOCKWISE -> ApiRelativeDirection.CIRCLE_CLOCKWISE; case CIRCLE_COUNTERCLOCKWISE -> ApiRelativeDirection.CIRCLE_COUNTERCLOCKWISE; case ELEVATOR -> ApiRelativeDirection.ELEVATOR; + case ESCALATOR -> ApiRelativeDirection.ESCALATOR; + case STAIRS -> ApiRelativeDirection.STAIRS; case UTURN_LEFT -> ApiRelativeDirection.UTURN_LEFT; case UTURN_RIGHT -> ApiRelativeDirection.UTURN_RIGHT; case ENTER_STATION -> ApiRelativeDirection.ENTER_STATION; diff --git a/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/_support/model/ApiRelativeDirection.java b/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/_support/model/ApiRelativeDirection.java index 91de9bac260..abc661d3ec4 100644 --- a/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/_support/model/ApiRelativeDirection.java +++ b/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/_support/model/ApiRelativeDirection.java @@ -18,6 +18,8 @@ public enum ApiRelativeDirection { CIRCLE_CLOCKWISE, CIRCLE_COUNTERCLOCKWISE, ELEVATOR, + ESCALATOR, + STAIRS, UTURN_LEFT, UTURN_RIGHT, ENTER_STATION, diff --git a/application/src/test/java/org/opentripplanner/routing/graph/GraphSerializationTest.java b/application/src/test/java/org/opentripplanner/routing/graph/GraphSerializationTest.java index cff523e330d..b1f8f9a0017 100644 --- a/application/src/test/java/org/opentripplanner/routing/graph/GraphSerializationTest.java +++ b/application/src/test/java/org/opentripplanner/routing/graph/GraphSerializationTest.java @@ -39,6 +39,8 @@ import org.opentripplanner.model.plan.Emission; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildRepository; +import org.opentripplanner.service.streetdetails.StreetDetailsRepository; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.service.worldenvelope.WorldEnvelopeRepository; @@ -89,6 +91,7 @@ public class GraphSerializationTest { public void testRoundTripSerializationForGTFSGraph() throws Exception { TestOtpModel model = ConstantsForTests.buildNewPortlandGraph(true); var osmGraphBuildRepository = new DefaultOsmInfoGraphBuildRepository(); + var streetDetailsRepository = new DefaultStreetDetailsRepository(); var weRepo = new DefaultWorldEnvelopeRepository(); var emissionRepository = createEmissionRepository(); var empiricalDelayRepository = empiricalDelayRepository(); @@ -96,6 +99,7 @@ public void testRoundTripSerializationForGTFSGraph() throws Exception { testRoundTrip( model.graph(), osmGraphBuildRepository, + streetDetailsRepository, model.timetableRepository(), weRepo, parkingRepository, @@ -111,6 +115,7 @@ public void testRoundTripSerializationForGTFSGraph() throws Exception { public void testRoundTripSerializationForNetexGraph() throws Exception { TestOtpModel model = ConstantsForTests.buildNewMinimalNetexGraph(); var osmGraphBuildRepository = new DefaultOsmInfoGraphBuildRepository(); + var streetDetailsRepository = new DefaultStreetDetailsRepository(); var worldEnvelopeRepository = new DefaultWorldEnvelopeRepository(); var emissionRepository = createEmissionRepository(); var empiricalDelayRepository = empiricalDelayRepository(); @@ -118,6 +123,7 @@ public void testRoundTripSerializationForNetexGraph() throws Exception { testRoundTrip( model.graph(), osmGraphBuildRepository, + streetDetailsRepository, model.timetableRepository(), worldEnvelopeRepository, parkingRepository, @@ -223,6 +229,7 @@ private static void assertNoDifferences(Graph g1, Graph g2) { private void testRoundTrip( Graph originalGraph, OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, + StreetDetailsRepository streetDetailsRepository, TimetableRepository originalTimetableRepository, WorldEnvelopeRepository worldEnvelopeRepository, VehicleParkingRepository vehicleParkingRepository, @@ -236,6 +243,7 @@ private void testRoundTrip( SerializedGraphObject serializedObj = new SerializedGraphObject( originalGraph, osmInfoGraphBuildRepository, + streetDetailsRepository, originalTimetableRepository, worldEnvelopeRepository, vehicleParkingRepository, diff --git a/application/src/test/java/org/opentripplanner/street/integration/BarrierRoutingTest.java b/application/src/test/java/org/opentripplanner/street/integration/BarrierRoutingTest.java index a44ca6915eb..fce3d5619d5 100644 --- a/application/src/test/java/org/opentripplanner/street/integration/BarrierRoutingTest.java +++ b/application/src/test/java/org/opentripplanner/street/integration/BarrierRoutingTest.java @@ -32,6 +32,8 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.impl.GraphPathFinder; import org.opentripplanner.routing.linking.VertexLinkerTestFactory; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsRepository; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsService; import org.opentripplanner.street.search.TemporaryVerticesContainer; import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.test.support.ResourceLoader; @@ -199,6 +201,7 @@ private static String computePolyline( id -> null, ZoneIds.BERLIN, graph.streetNotesService, + new DefaultStreetDetailsService(new DefaultStreetDetailsRepository()), graph.ellipsoidToGeoidDifference ); diff --git a/application/src/test/java/org/opentripplanner/street/integration/BicycleRoutingTest.java b/application/src/test/java/org/opentripplanner/street/integration/BicycleRoutingTest.java index 9813144e41c..338565a275b 100644 --- a/application/src/test/java/org/opentripplanner/street/integration/BicycleRoutingTest.java +++ b/application/src/test/java/org/opentripplanner/street/integration/BicycleRoutingTest.java @@ -22,6 +22,8 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.impl.GraphPathFinder; import org.opentripplanner.routing.linking.VertexLinkerTestFactory; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsRepository; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsService; import org.opentripplanner.street.search.TemporaryVerticesContainer; import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.test.support.ResourceLoader; @@ -102,6 +104,7 @@ private static String computePolyline(Graph graph, GenericLocation from, Generic id -> null, ZoneIds.BERLIN, graph.streetNotesService, + new DefaultStreetDetailsService(new DefaultStreetDetailsRepository()), graph.ellipsoidToGeoidDifference ); diff --git a/application/src/test/java/org/opentripplanner/street/integration/CarRoutingTest.java b/application/src/test/java/org/opentripplanner/street/integration/CarRoutingTest.java index 6828f64800a..ee053e67a37 100644 --- a/application/src/test/java/org/opentripplanner/street/integration/CarRoutingTest.java +++ b/application/src/test/java/org/opentripplanner/street/integration/CarRoutingTest.java @@ -23,6 +23,8 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.impl.GraphPathFinder; import org.opentripplanner.routing.linking.VertexLinkerTestFactory; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsRepository; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsService; import org.opentripplanner.street.search.TemporaryVerticesContainer; import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.test.support.ResourceLoader; @@ -150,6 +152,7 @@ private static String computePolyline(Graph graph, GenericLocation from, Generic id -> null, ZoneIds.BERLIN, graph.streetNotesService, + new DefaultStreetDetailsService(new DefaultStreetDetailsRepository()), graph.ellipsoidToGeoidDifference ); diff --git a/application/src/test/java/org/opentripplanner/street/integration/SplitEdgeTurnRestrictionsTest.java b/application/src/test/java/org/opentripplanner/street/integration/SplitEdgeTurnRestrictionsTest.java index 3f86588c0b5..d6d74cb6502 100644 --- a/application/src/test/java/org/opentripplanner/street/integration/SplitEdgeTurnRestrictionsTest.java +++ b/application/src/test/java/org/opentripplanner/street/integration/SplitEdgeTurnRestrictionsTest.java @@ -22,6 +22,8 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.impl.GraphPathFinder; import org.opentripplanner.routing.linking.VertexLinkerTestFactory; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsRepository; +import org.opentripplanner.service.streetdetails.internal.DefaultStreetDetailsService; import org.opentripplanner.street.search.TemporaryVerticesContainer; import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.test.support.ResourceLoader; @@ -183,6 +185,7 @@ private static String computeCarPolyline(Graph graph, GenericLocation from, Gene id -> null, ZoneIds.BERLIN, graph.streetNotesService, + new DefaultStreetDetailsService(new DefaultStreetDetailsRepository()), graph.ellipsoidToGeoidDifference ); diff --git a/application/src/test/java/org/opentripplanner/street/model/_data/StreetModelForTest.java b/application/src/test/java/org/opentripplanner/street/model/_data/StreetModelForTest.java index 1016a7123b5..bf065dc0708 100644 --- a/application/src/test/java/org/opentripplanner/street/model/_data/StreetModelForTest.java +++ b/application/src/test/java/org/opentripplanner/street/model/_data/StreetModelForTest.java @@ -2,8 +2,10 @@ import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id; +import java.time.Duration; import java.util.HashSet; import java.util.Set; +import javax.annotation.Nullable; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.LineString; import org.opentripplanner.framework.geometry.GeometryUtils; @@ -21,6 +23,7 @@ import org.opentripplanner.street.model.edge.AreaEdgeBuilder; import org.opentripplanner.street.model.edge.AreaGroup; import org.opentripplanner.street.model.edge.Edge; +import org.opentripplanner.street.model.edge.EscalatorEdge; import org.opentripplanner.street.model.edge.StreetEdge; import org.opentripplanner.street.model.edge.StreetEdgeBuilder; import org.opentripplanner.street.model.edge.TemporaryFreeEdge; @@ -71,7 +74,8 @@ public static StreetEdgeBuilder streetEdgeBuilder( StreetVertex vA, StreetVertex vB, double length, - StreetTraversalPermission perm + StreetTraversalPermission perm, + boolean stairs ) { var labelA = vA.getLabel(); var labelB = vB.getLabel(); @@ -87,17 +91,36 @@ public static StreetEdgeBuilder streetEdgeBuilder( .withGeometry(geom) .withName(name) .withMeterLength(length) + .withStairs(stairs) .withPermission(perm) .withBack(false); } + public static StreetEdgeBuilder streetEdgeBuilder( + StreetVertex vA, + StreetVertex vB, + double length, + StreetTraversalPermission perm + ) { + return streetEdgeBuilder(vA, vB, length, perm, false); + } + public static StreetEdge streetEdge( StreetVertex vA, StreetVertex vB, double length, StreetTraversalPermission perm ) { - return streetEdgeBuilder(vA, vB, length, perm).buildAndConnect(); + return streetEdgeBuilder(vA, vB, length, perm, false).buildAndConnect(); + } + + public static EscalatorEdge escalatorEdge( + StreetVertex vA, + StreetVertex vB, + double length, + @Nullable Duration duration + ) { + return EscalatorEdge.createEscalatorEdge(vA, vB, length, null); } public static StreetEdge areaEdge( diff --git a/application/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java b/application/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java index bc817f54910..17710df0354 100644 --- a/application/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java +++ b/application/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java @@ -152,6 +152,46 @@ public TestStateBuilder streetEdge(String name, int distance) { return this; } + /** + * Traverse a very plain street edge with stairs with no special characteristics. + */ + public TestStateBuilder stairsEdge() { + count++; + var from = (StreetVertex) currentState.vertex; + var to = StreetModelForTest.intersectionVertex(count, count); + var edge = StreetModelForTest.streetEdgeBuilder( + from, + to, + 30, + StreetTraversalPermission.PEDESTRIAN, + true + ).buildAndConnect(); + + var states = edge.traverse(currentState); + if (states.length != 1) { + throw new IllegalStateException("Only single state transitions are supported."); + } + currentState = states[0]; + return this; + } + + /** + * Traverse a very plain escalator edge with no special characteristics. + */ + public TestStateBuilder escalatorEdge() { + count++; + var from = (StreetVertex) currentState.vertex; + var to = StreetModelForTest.intersectionVertex(count, count); + var edge = StreetModelForTest.escalatorEdge(from, to, 30, null); + + var states = edge.traverse(currentState); + if (states.length != 1) { + throw new IllegalStateException("Only single state transitions are supported."); + } + currentState = states[0]; + return this; + } + public TestStateBuilder areaEdge(String name, int distance) { count++; var from = (StreetVertex) currentState.vertex; diff --git a/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java b/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java index 4168d204dbd..e6d2774d583 100644 --- a/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java +++ b/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java @@ -135,6 +135,7 @@ public SpeedTest( TestServerContext.createViaTransferResolver(graph, transitService), TestServerContext.createWorldEnvelopeService(), null, + TestServerContext.createStreetDetailsService(), null, null, null, diff --git a/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/walk-steps.json b/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/walk-steps.json index 95adec34ea8..08744baa937 100644 --- a/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/walk-steps.json +++ b/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/walk-steps.json @@ -32,6 +32,42 @@ "entranceId": "osm:123", "wheelchairAccessible": "POSSIBLE" } + }, + { + "streetName": "stairs", + "area": false, + "relativeDirection": "STAIRS", + "absoluteDirection": null, + "feature": { + "__typename": "StairsUse", + "from": { + "level": 2.0, + "name": "2" + }, + "verticalDirection": "DOWN", + "to": { + "level": 1.0, + "name": "1" + } + } + }, + { + "streetName": "escalators", + "area": false, + "relativeDirection": "ESCALATOR", + "absoluteDirection": null, + "feature": { + "__typename": "EscalatorUse", + "from": { + "level": 2.0, + "name": "2" + }, + "verticalDirection": "DOWN", + "to": { + "level": 1.0, + "name": "1" + } + } } ] }, diff --git a/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/walk-steps.graphql b/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/walk-steps.graphql index 18cb5a8d49d..c6c70e79c4f 100644 --- a/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/walk-steps.graphql +++ b/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/walk-steps.graphql @@ -27,6 +27,28 @@ entranceId wheelchairAccessible } + ... on EscalatorUse { + from { + level + name + } + verticalDirection + to { + level + name + } + } + ... on StairsUse { + from { + level + name + } + verticalDirection + to { + level + name + } + } } } } diff --git a/doc/user/BuildConfiguration.md b/doc/user/BuildConfiguration.md index d21c9b260f1..27de855e68d 100644 --- a/doc/user/BuildConfiguration.md +++ b/doc/user/BuildConfiguration.md @@ -26,6 +26,7 @@ Sections follow that describe particular settings in more depth. | [distanceBetweenElevationSamples](#distanceBetweenElevationSamples) | `double` | The distance between elevation samples in meters. | *Optional* | `10.0` | 2.0 | | embedRouterConfig | `boolean` | Embed the Router config in the graph, which allows it to be sent to a server fully configured over the wire. | *Optional* | `true` | 2.0 | | [graph](#graph) | `uri` | URI to the graph object file for reading and writing. | *Optional* | | 2.0 | +| includeEdgeLevelInfo | `boolean` | Whether level info for edges should be stored in the StreetDetailsService. | *Optional* | `false` | 2.9 | | [includeEllipsoidToGeoidDifference](#includeEllipsoidToGeoidDifference) | `boolean` | Include the Ellipsoid to Geoid difference in the calculations of every point along every StreetWithElevationEdge. | *Optional* | `false` | 2.0 | | maxAreaNodes | `integer` | Visibility calculations for an area will not be done if there are more nodes than this limit. | *Optional* | `200` | 2.1 | | [maxDataImportIssuesPerFile](#maxDataImportIssuesPerFile) | `integer` | When to split the import report. | *Optional* | `1000` | 2.0 |