diff --git a/application/src/ext-test/java/org/opentripplanner/ext/restapi/mapping/EnumMapperTest.java b/application/src/ext-test/java/org/opentripplanner/ext/restapi/mapping/EnumMapperTest.java index 35cc368fec4..5b03ada1c1f 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/restapi/mapping/EnumMapperTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/restapi/mapping/EnumMapperTest.java @@ -8,39 +8,11 @@ import java.util.Map; import java.util.function.Function; import org.junit.jupiter.api.Test; -import org.opentripplanner.ext.restapi.model.ApiAbsoluteDirection; -import org.opentripplanner.ext.restapi.model.ApiRelativeDirection; import org.opentripplanner.ext.restapi.model.ApiVertexType; -import org.opentripplanner.model.plan.AbsoluteDirection; -import org.opentripplanner.model.plan.RelativeDirection; import org.opentripplanner.model.plan.VertexType; public class EnumMapperTest { - private static final String MSG = - "Assert that the API enums have the exact same values that " + - "the domain enums of the same type, and that the specialized mapper is mapping all " + - "values. If this assumtion does not hold, create a new test."; - - @Test - public void map() { - try { - verifyExactMatch( - AbsoluteDirection.class, - ApiAbsoluteDirection.class, - AbsoluteDirectionMapper::mapAbsoluteDirection - ); - verifyExactMatch( - RelativeDirection.class, - ApiRelativeDirection.class, - RelativeDirectionMapper::mapRelativeDirection - ); - } catch (RuntimeException ex) { - System.out.println(MSG); - throw ex; - } - } - @Test public void testVertexTypeMapping() { verifyExplicitMatch( @@ -75,17 +47,4 @@ private , A extends Enum> void verifyExplicitMatch( assertTrue(rest.isEmpty()); } - private , A extends Enum> void verifyExactMatch( - Class domainClass, - Class apiClass, - Function mapper - ) { - List rest = new ArrayList<>(List.of(apiClass.getEnumConstants())); - for (D it : domainClass.getEnumConstants()) { - A result = mapper.apply(it); - assertEquals(result.name(), it.name()); - rest.remove(result); - } - assertTrue(rest.isEmpty()); - } } diff --git a/application/src/ext/java/org/opentripplanner/ext/restapi/mapping/RelativeDirectionMapper.java b/application/src/ext/java/org/opentripplanner/ext/restapi/mapping/RelativeDirectionMapper.java index a1bdd145a55..ab9abaa4481 100644 --- a/application/src/ext/java/org/opentripplanner/ext/restapi/mapping/RelativeDirectionMapper.java +++ b/application/src/ext/java/org/opentripplanner/ext/restapi/mapping/RelativeDirectionMapper.java @@ -14,7 +14,7 @@ public static ApiRelativeDirection mapRelativeDirection(RelativeDirection domain case HARD_LEFT -> ApiRelativeDirection.HARD_LEFT; case LEFT -> ApiRelativeDirection.LEFT; case SLIGHTLY_LEFT -> ApiRelativeDirection.SLIGHTLY_LEFT; - case CONTINUE -> ApiRelativeDirection.CONTINUE; + case CONTINUE, ENTER_OR_EXIT_STATION -> ApiRelativeDirection.CONTINUE; case SLIGHTLY_RIGHT -> ApiRelativeDirection.SLIGHTLY_RIGHT; case RIGHT -> ApiRelativeDirection.RIGHT; case HARD_RIGHT -> ApiRelativeDirection.HARD_RIGHT; diff --git a/application/src/ext/java/org/opentripplanner/ext/restapi/mapping/WalkStepMapper.java b/application/src/ext/java/org/opentripplanner/ext/restapi/mapping/WalkStepMapper.java index 8767abe7478..c4aa11904cc 100644 --- a/application/src/ext/java/org/opentripplanner/ext/restapi/mapping/WalkStepMapper.java +++ b/application/src/ext/java/org/opentripplanner/ext/restapi/mapping/WalkStepMapper.java @@ -39,7 +39,7 @@ public ApiWalkStep mapWalkStep(WalkStep domain) { api.streetName = domain.getDirectionText().toString(locale); api.absoluteDirection = domain.getAbsoluteDirection().map(AbsoluteDirectionMapper::mapAbsoluteDirection).orElse(null); - api.exit = domain.getExit(); + api.exit = domain.highwayExit().orElse(null); api.stayOn = domain.isStayOn(); api.area = domain.getArea(); api.bogusName = domain.nameIsDerived(); diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java b/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java index 302458ac656..d3f64288417 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java @@ -39,6 +39,7 @@ import org.opentripplanner.apis.gtfs.datafetchers.CurrencyImpl; import org.opentripplanner.apis.gtfs.datafetchers.DefaultFareProductImpl; import org.opentripplanner.apis.gtfs.datafetchers.DepartureRowImpl; +import org.opentripplanner.apis.gtfs.datafetchers.EntranceImpl; import org.opentripplanner.apis.gtfs.datafetchers.EstimatedTimeImpl; import org.opentripplanner.apis.gtfs.datafetchers.FareProductTypeResolver; import org.opentripplanner.apis.gtfs.datafetchers.FareProductUseImpl; @@ -64,6 +65,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.StepFeatureTypeResolver; import org.opentripplanner.apis.gtfs.datafetchers.StopCallImpl; import org.opentripplanner.apis.gtfs.datafetchers.StopGeometriesImpl; import org.opentripplanner.apis.gtfs.datafetchers.StopImpl; @@ -137,6 +139,7 @@ protected static GraphQLSchema buildSchema() { .type("AlertEntity", type -> type.typeResolver(new AlertEntityTypeResolver())) .type("CallStopLocation", type -> type.typeResolver(new CallStopLocationTypeResolver())) .type("CallScheduledTime", type -> type.typeResolver(new CallScheduledTimeTypeResolver())) + .type("StepFeature", type -> type.typeResolver(new StepFeatureTypeResolver())) .type(typeWiring.build(AgencyImpl.class)) .type(typeWiring.build(AlertImpl.class)) .type(typeWiring.build(BikeParkImpl.class)) @@ -195,6 +198,7 @@ protected static GraphQLSchema buildSchema() { .type(typeWiring.build(LegTimeImpl.class)) .type(typeWiring.build(RealTimeEstimateImpl.class)) .type(typeWiring.build(EstimatedTimeImpl.class)) + .type(typeWiring.build(EntranceImpl.class)) .build(); SchemaGenerator schemaGenerator = new SchemaGenerator(); return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring); diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java new file mode 100644 index 00000000000..f9faa9cc4d1 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java @@ -0,0 +1,45 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.schema.DataFetcher; +import org.opentripplanner.apis.gtfs.GraphQLUtils; +import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.transit.model.site.Entrance; + +public class EntranceImpl implements GraphQLDataFetchers.GraphQLEntrance { + + @Override + public DataFetcher publicCode() { + return environment -> { + Entrance entrance = environment.getSource(); + return entrance.getCode(); + }; + } + + @Override + public DataFetcher entranceId() { + return environment -> { + Entrance entrance = environment.getSource(); + return entrance.getId().toString(); + }; + } + + @Override + public DataFetcher name() { + return environment -> { + Entrance entrance = environment.getSource(); + return org.opentripplanner.framework.graphql.GraphQLUtils.getTranslation( + entrance.getName(), + environment + ); + }; + } + + @Override + public DataFetcher wheelchairAccessible() { + return environment -> { + Entrance entrance = environment.getSource(); + return GraphQLUtils.toGraphQL(entrance.getWheelchairAccessibility()); + }; + } +} 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 new file mode 100644 index 00000000000..714518cb9ea --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepFeatureTypeResolver.java @@ -0,0 +1,21 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.TypeResolutionEnvironment; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; +import graphql.schema.TypeResolver; +import org.opentripplanner.transit.model.site.Entrance; + +public class StepFeatureTypeResolver implements TypeResolver { + + @Override + public GraphQLObjectType getType(TypeResolutionEnvironment environment) { + Object o = environment.getObject(); + GraphQLSchema schema = environment.getSchema(); + + if (o instanceof Entrance) { + return schema.getObjectType("Entrance"); + } + 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 f14db6f213f..409bb2abb1d 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 @@ -50,7 +50,12 @@ public DataFetcher> elevationProfile() { @Override public DataFetcher exit() { - return environment -> getSource(environment).getExit(); + return environment -> getSource(environment).highwayExit().orElse(null); + } + + @Override + public DataFetcher feature() { + return environment -> getSource(environment).entrance().orElse(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 9c95ea65e91..26ace8fc66a 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 @@ -392,6 +392,17 @@ public interface GraphQLEmissions { public DataFetcher co2(); } + /** Station entrance or exit, originating from OSM or GTFS data. */ + public interface GraphQLEntrance { + public DataFetcher entranceId(); + + public DataFetcher name(); + + public DataFetcher publicCode(); + + public DataFetcher wheelchairAccessible(); + } + /** Real-time estimates for an arrival or departure at a certain place. */ public interface GraphQLEstimatedTime { public DataFetcher delay(); @@ -1024,6 +1035,9 @@ public interface GraphQLRoutingError { public DataFetcher inputField(); } + /** A feature for a step */ + public interface GraphQLStepFeature extends TypeResolver {} + /** * Stop can represent either a single public transport stop, where passengers can * board and/or disembark vehicles, or a station, which contains multiple stops. @@ -1521,6 +1535,8 @@ public interface GraphQLStep { public DataFetcher exit(); + public DataFetcher feature(); + public DataFetcher lat(); public DataFetcher lon(); 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 a969b5223b1..fc20625e18e 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 @@ -4327,7 +4327,11 @@ public enum GraphQLRealtimeState { UPDATED, } - /** Actions to take relative to the current position when engaging a walking/driving step. */ + /** + * A direction that is not absolute but rather fuzzy and context-dependent. + * It provides the passenger with information what they should do in this step depending on where they + * were in the previous one. + */ public enum GraphQLRelativeDirection { CIRCLE_CLOCKWISE, CIRCLE_COUNTERCLOCKWISE, 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 69a78b05f55..3f69047f94d 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 @@ -27,7 +27,7 @@ public static GraphQLRelativeDirection map(RelativeDirection relativeDirection) case HARD_LEFT -> GraphQLRelativeDirection.HARD_LEFT; case LEFT -> GraphQLRelativeDirection.LEFT; case SLIGHTLY_LEFT -> GraphQLRelativeDirection.SLIGHTLY_LEFT; - case CONTINUE -> GraphQLRelativeDirection.CONTINUE; + case CONTINUE, ENTER_OR_EXIT_STATION -> GraphQLRelativeDirection.CONTINUE; case SLIGHTLY_RIGHT -> GraphQLRelativeDirection.SLIGHTLY_RIGHT; case RIGHT -> GraphQLRelativeDirection.RIGHT; case HARD_RIGHT -> GraphQLRelativeDirection.HARD_RIGHT; 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 new file mode 100644 index 00000000000..3228cb914df --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/RelativeDirectionMapper.java @@ -0,0 +1,33 @@ +package org.opentripplanner.apis.transmodel.mapping; + +import org.opentripplanner.model.plan.RelativeDirection; + +/** + * This mapper makes sure that only those values are returned which have a mapping in the Transmodel API, + * as we don't really want to return all of them. + */ +public class RelativeDirectionMapper { + + public static RelativeDirection map(RelativeDirection relativeDirection) { + return switch (relativeDirection) { + case DEPART, + SLIGHTLY_LEFT, + HARD_LEFT, + LEFT, + CONTINUE, + SLIGHTLY_RIGHT, + RIGHT, + HARD_RIGHT, + CIRCLE_CLOCKWISE, + CIRCLE_COUNTERCLOCKWISE, + ELEVATOR, + UTURN_LEFT, + UTURN_RIGHT -> relativeDirection; + // for these the Transmodel API doesn't have a mapping. should it? + case ENTER_STATION, + EXIT_STATION, + ENTER_OR_EXIT_STATION, + FOLLOW_SIGNS -> RelativeDirection.CONTINUE; + }; + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/model/plan/PathGuidanceType.java b/application/src/main/java/org/opentripplanner/apis/transmodel/model/plan/PathGuidanceType.java index 68406f57d54..8840e8fedb8 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/model/plan/PathGuidanceType.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/model/plan/PathGuidanceType.java @@ -5,6 +5,7 @@ import graphql.schema.GraphQLList; import graphql.schema.GraphQLNonNull; import graphql.schema.GraphQLObjectType; +import org.opentripplanner.apis.transmodel.mapping.RelativeDirectionMapper; import org.opentripplanner.apis.transmodel.model.EnumTypes; import org.opentripplanner.framework.graphql.GraphQLUtils; import org.opentripplanner.model.plan.WalkStep; @@ -31,7 +32,9 @@ public static GraphQLObjectType create(GraphQLObjectType elevationStepType) { .name("relativeDirection") .description("The relative direction of this step.") .type(EnumTypes.RELATIVE_DIRECTION) - .dataFetcher(environment -> ((WalkStep) environment.getSource()).getRelativeDirection()) + .dataFetcher(environment -> + RelativeDirectionMapper.map(((WalkStep) environment.getSource()).getRelativeDirection()) + ) .build() ) .field( @@ -65,7 +68,9 @@ public static GraphQLObjectType create(GraphQLObjectType elevationStepType) { .name("exit") .description("When exiting a highway or traffic circle, the exit name/number.") .type(Scalars.GraphQLString) - .dataFetcher(environment -> ((WalkStep) environment.getSource()).getExit()) + .dataFetcher(environment -> + ((WalkStep) environment.getSource()).highwayExit().orElse(null) + ) .build() ) .field( 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 5371142d612..da6a8d890d0 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 @@ -71,6 +71,7 @@ static OsmModule provideOsmModule( osmConfiguredDataSource.dataSource(), osmConfiguredDataSource.config().osmTagMapper(), osmConfiguredDataSource.config().timeZone(), + osmConfiguredDataSource.config().includeOsmSubwayEntrances(), config.osmCacheDataInMem, issueStore ) @@ -86,6 +87,7 @@ static OsmModule provideOsmModule( .withStaticBikeParkAndRide(config.staticBikeParkAndRide) .withMaxAreaNodes(config.maxAreaNodes) .withBoardingAreaRefTags(config.boardingLocationTags) + .withIncludeOsmSubwayEntrances(config.osmDefaults.includeOsmSubwayEntrances()) .withIssueStore(issueStore) .withStreetLimitationParameters(streetLimitationParameters) .build(); diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/ElevatorProcessor.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/ElevatorProcessor.java index 490d6a266b9..45ed01e4568 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/ElevatorProcessor.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/ElevatorProcessor.java @@ -95,7 +95,7 @@ public void buildElevatorEdges(Graph graph) { } int travelTime = parseDuration(node).orElse(-1); - var wheelchair = node.getWheelchairAccessibility(); + var wheelchair = node.wheelchairAccessibility(); createElevatorHopEdges( onboardVertices, @@ -138,7 +138,7 @@ public void buildElevatorEdges(Graph graph) { int travelTime = parseDuration(elevatorWay).orElse(-1); int levels = nodes.size(); - var wheelchair = elevatorWay.getWheelchairAccessibility(); + var wheelchair = elevatorWay.wheelchairAccessibility(); createElevatorHopEdges( onboardVertices, 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 db495905041..1ff9d50f28a 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 @@ -73,7 +73,13 @@ public class OsmModule implements GraphBuilderModule { this.issueStore = issueStore; this.params = params; this.osmdb = new OsmDatabase(issueStore); - this.vertexGenerator = new VertexGenerator(osmdb, graph, params.boardingAreaRefTags()); + this.vertexGenerator = + new VertexGenerator( + osmdb, + graph, + params.boardingAreaRefTags(), + params.includeOsmSubwayEntrances() + ); this.normalizer = new SafetyValueNormalizer(graph, issueStore); this.streetLimitationParameters = Objects.requireNonNull(streetLimitationParameters); this.parkingRepository = parkingService; 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 2f7f4c506c9..2df13d0c6cb 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 @@ -26,6 +26,7 @@ public class OsmModuleBuilder { private boolean platformEntriesLinking = false; private boolean staticParkAndRide = false; private boolean staticBikeParkAndRide = false; + private boolean includeOsmSubwayEntrances = false; private int maxAreaNodes; private StreetLimitationParameters streetLimitationParameters = new StreetLimitationParameters(); @@ -79,6 +80,11 @@ public OsmModuleBuilder withMaxAreaNodes(int maxAreaNodes) { return this; } + public OsmModuleBuilder withIncludeOsmSubwayEntrances(boolean includeOsmSubwayEntrances) { + this.includeOsmSubwayEntrances = includeOsmSubwayEntrances; + return this; + } + public OsmModuleBuilder withStreetLimitationParameters(StreetLimitationParameters parameters) { this.streetLimitationParameters = parameters; return this; @@ -98,7 +104,8 @@ public OsmModule build() { areaVisibility, platformEntriesLinking, staticParkAndRide, - staticBikeParkAndRide + staticBikeParkAndRide, + includeOsmSubwayEntrances ) ); } diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java index e6fec74b798..8c707d005a9 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java @@ -33,12 +33,19 @@ class VertexGenerator { private final HashMap> multiLevelNodes = new HashMap<>(); private final OsmDatabase osmdb; private final Set boardingAreaRefTags; + private final Boolean includeOsmSubwayEntrances; private final VertexFactory vertexFactory; - public VertexGenerator(OsmDatabase osmdb, Graph graph, Set boardingAreaRefTags) { + public VertexGenerator( + OsmDatabase osmdb, + Graph graph, + Set boardingAreaRefTags, + boolean includeOsmSubwayEntrances + ) { this.osmdb = osmdb; this.vertexFactory = new VertexFactory(graph); this.boardingAreaRefTags = boardingAreaRefTags; + this.includeOsmSubwayEntrances = includeOsmSubwayEntrances; } /** @@ -95,6 +102,11 @@ IntersectionVertex getVertexForOsmNode(OsmNode node, OsmWithTags way) { iv = bv; } + if (includeOsmSubwayEntrances && node.isSubwayEntrance()) { + String ref = node.getTag("ref"); + iv = vertexFactory.stationEntrance(nid, coordinate, ref, node.wheelchairAccessibility()); + } + if (iv == null) { iv = vertexFactory.osm( diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmExtractParameters.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmExtractParameters.java index 175b9c04c5b..a59147137f6 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmExtractParameters.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmExtractParameters.java @@ -11,16 +11,28 @@ * Example: {@code "osm" : [ {source: "file:///path/to/otp/norway.pbf"} ] } * */ -public record OsmExtractParameters(URI source, OsmTagMapperSource osmTagMapper, ZoneId timeZone) +public record OsmExtractParameters( + URI source, + OsmTagMapperSource osmTagMapper, + ZoneId timeZone, + boolean includeOsmSubwayEntrances +) implements DataSourceConfig { public static final OsmTagMapperSource DEFAULT_OSM_TAG_MAPPER = OsmTagMapperSource.DEFAULT; public static final ZoneId DEFAULT_TIME_ZONE = null; + public static final boolean DEFAULT_INCLUDE_OSM_SUBWAY_ENTRANCES = false; + public static final OsmExtractParameters DEFAULT = new OsmExtractParametersBuilder().build(); OsmExtractParameters(OsmExtractParametersBuilder builder) { - this(builder.getSource(), builder.getOsmTagMapper(), builder.getTimeZone()); + this( + builder.getSource(), + builder.getOsmTagMapper(), + builder.getTimeZone(), + builder.includeOsmSubwayEntrances() + ); } @Override @@ -37,6 +49,10 @@ public ZoneId timeZone() { return timeZone; } + public boolean includeOsmSubwayEntrances() { + return includeOsmSubwayEntrances; + } + public OsmExtractParametersBuilder copyOf() { return new OsmExtractParametersBuilder(this); } diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmExtractParametersBuilder.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmExtractParametersBuilder.java index 2d9bb71d9f5..66c65e05d81 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmExtractParametersBuilder.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmExtractParametersBuilder.java @@ -24,14 +24,18 @@ public class OsmExtractParametersBuilder { */ private ZoneId timeZone; + private boolean includeOsmSubwayEntrances; + public OsmExtractParametersBuilder() { this.osmTagMapper = OsmExtractParameters.DEFAULT_OSM_TAG_MAPPER; this.timeZone = OsmExtractParameters.DEFAULT_TIME_ZONE; + this.includeOsmSubwayEntrances = OsmExtractParameters.DEFAULT_INCLUDE_OSM_SUBWAY_ENTRANCES; } public OsmExtractParametersBuilder(OsmExtractParameters original) { this.osmTagMapper = original.osmTagMapper(); this.timeZone = original.timeZone(); + this.includeOsmSubwayEntrances = original.includeOsmSubwayEntrances(); } public OsmExtractParametersBuilder withSource(URI source) { @@ -49,6 +53,13 @@ public OsmExtractParametersBuilder withTimeZone(ZoneId timeZone) { return this; } + public OsmExtractParametersBuilder withIncludeOsmSubwayEntrances( + boolean includeOsmSubwayEntrances + ) { + this.includeOsmSubwayEntrances = includeOsmSubwayEntrances; + return this; + } + public URI getSource() { return source; } @@ -61,6 +72,10 @@ public ZoneId getTimeZone() { return timeZone; } + public boolean includeOsmSubwayEntrances() { + return includeOsmSubwayEntrances; + } + public OsmExtractParameters build() { return new OsmExtractParameters(this); } 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 52bf8d65314..a3fd14020e8 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 includeOsmSubwayEntrances Whether we should create subway entrances from OSM data. */ public record OsmProcessingParameters( Set boardingAreaRefTags, @@ -21,7 +22,8 @@ public record OsmProcessingParameters( boolean areaVisibility, boolean platformEntriesLinking, boolean staticParkAndRide, - boolean staticBikeParkAndRide + boolean staticBikeParkAndRide, + boolean includeOsmSubwayEntrances ) { public OsmProcessingParameters { boardingAreaRefTags = Set.copyOf(Objects.requireNonNull(boardingAreaRefTags)); diff --git a/application/src/main/java/org/opentripplanner/model/plan/RelativeDirection.java b/application/src/main/java/org/opentripplanner/model/plan/RelativeDirection.java index ffc8993d0db..3ce16a45c11 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/RelativeDirection.java +++ b/application/src/main/java/org/opentripplanner/model/plan/RelativeDirection.java @@ -21,6 +21,13 @@ public enum RelativeDirection { UTURN_RIGHT, ENTER_STATION, EXIT_STATION, + /** + * We don't have a way to reliably tell if we are entering or exiting a station and therefore + * use this generic enum value. Please don't expose it in APIs. + *

+ * If we manage to figure it out in the future, we can remove this. + */ + ENTER_OR_EXIT_STATION, FOLLOW_SIGNS; public static RelativeDirection calculate( diff --git a/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java b/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java index c2c2b2c609e..7ade16de39a 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java +++ b/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java @@ -8,6 +8,7 @@ import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.note.StreetNote; +import org.opentripplanner.transit.model.site.Entrance; import org.opentripplanner.utils.lang.DoubleUtils; import org.opentripplanner.utils.tostring.ToStringBuilder; @@ -43,7 +44,8 @@ public final class WalkStep { private final double angle; private final boolean walkingBike; - private final String exit; + private final String highwayExit; + private final Entrance entrance; private final ElevationProfile elevationProfile; private final boolean stayOn; @@ -55,7 +57,8 @@ public final class WalkStep { AbsoluteDirection absoluteDirection, I18NString directionText, Set streetNotes, - String exit, + String highwayExit, + Entrance entrance, ElevationProfile elevationProfile, boolean nameIsDerived, boolean walkingBike, @@ -75,7 +78,8 @@ public final class WalkStep { this.angle = DoubleUtils.roundTo2Decimals(angle); this.walkingBike = walkingBike; this.area = area; - this.exit = exit; + this.highwayExit = highwayExit; + this.entrance = entrance; this.elevationProfile = elevationProfile; this.stayOn = stayOn; this.edges = List.copyOf(Objects.requireNonNull(edges)); @@ -126,8 +130,15 @@ public Optional getAbsoluteDirection() { /** * When exiting a highway or traffic circle, the exit name/number. */ - public String getExit() { - return exit; + public Optional highwayExit() { + return Optional.ofNullable(highwayExit); + } + + /** + * Get information about a subway station entrance or exit. + */ + public Optional entrance() { + return Optional.ofNullable(entrance); } /** diff --git a/application/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java b/application/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java index b2f9e1f7510..75589718861 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java +++ b/application/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java @@ -9,6 +9,7 @@ import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.note.StreetNote; +import org.opentripplanner.transit.model.site.Entrance; import org.opentripplanner.utils.lang.DoubleUtils; import org.opentripplanner.utils.lang.IntUtils; @@ -25,6 +26,7 @@ public class WalkStepBuilder { private RelativeDirection relativeDirection; private ElevationProfile elevationProfile; private String exit; + private Entrance entrance; private boolean stayOn = false; /** * Distance used for appending elevation profiles @@ -74,6 +76,11 @@ public WalkStepBuilder withExit(String exit) { return this; } + public WalkStepBuilder withEntrance(@Nullable Entrance entrance) { + this.entrance = entrance; + return this; + } + public WalkStepBuilder withStayOn(boolean stayOn) { this.stayOn = stayOn; return this; @@ -159,6 +166,7 @@ public WalkStep build() { directionText, streetNotes, exit, + entrance, elevationProfile, nameIsDerived, walkingBike, diff --git a/application/src/main/java/org/opentripplanner/osm/OsmProvider.java b/application/src/main/java/org/opentripplanner/osm/OsmProvider.java index 597fd516b0e..b04a84e51c5 100644 --- a/application/src/main/java/org/opentripplanner/osm/OsmProvider.java +++ b/application/src/main/java/org/opentripplanner/osm/OsmProvider.java @@ -37,6 +37,8 @@ public class OsmProvider { private final OsmTagMapper osmTagMapper; + private boolean includeOsmSubwayEntrances = false; + private final WayPropertySet wayPropertySet; private byte[] cachedBytes = null; @@ -46,6 +48,7 @@ public OsmProvider(File file, boolean cacheDataInMem) { new FileDataSource(file, FileType.OSM), OsmTagMapperSource.DEFAULT, null, + false, cacheDataInMem, DataImportIssueStore.NOOP ); @@ -55,11 +58,13 @@ public OsmProvider( DataSource dataSource, OsmTagMapperSource tagMapperSource, ZoneId zoneId, + boolean includeOsmSubwayEntrances, boolean cacheDataInMem, DataImportIssueStore issueStore ) { this.source = dataSource; this.zoneId = zoneId; + this.includeOsmSubwayEntrances = includeOsmSubwayEntrances; this.osmTagMapper = tagMapperSource.getInstance(); this.wayPropertySet = new WayPropertySet(issueStore); osmTagMapper.populateProperties(wayPropertySet); @@ -152,6 +157,10 @@ public OsmTagMapper getOsmTagMapper() { return osmTagMapper; } + public boolean getIncludeOsmSubwayEntrances() { + return includeOsmSubwayEntrances; + } + public WayPropertySet getWayPropertySet() { return wayPropertySet; } diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java index c5539d1296e..cb9fcd679f0 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java @@ -63,6 +63,15 @@ public boolean isBarrier() { ); } + /** + * Checks if this node is a subway station entrance. + * + * @return true if it is + */ + public boolean isSubwayEntrance() { + return hasTag("railway") && "subway_entrance".equals(getTag("railway")); + } + /** * Consider barrier tag in permissions. Leave the rest for the super class. */ diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmWithTags.java b/application/src/main/java/org/opentripplanner/osm/model/OsmWithTags.java index 3f47d4454bd..10214460b15 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmWithTags.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmWithTags.java @@ -139,7 +139,7 @@ public boolean isTagFalse(String tag) { /** * Returns the level of wheelchair access of the element. */ - public Accessibility getWheelchairAccessibility() { + public Accessibility wheelchairAccessibility() { if (isTagTrue("wheelchair")) { return Accessibility.POSSIBLE; } else if (isTagFalse("wheelchair")) { 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 4ce1c616e65..8ec6ac07e34 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 @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import javax.annotation.Nullable; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import org.opentripplanner.framework.geometry.DirectionUtils; @@ -25,9 +26,11 @@ import org.opentripplanner.street.model.edge.StreetEdge; import org.opentripplanner.street.model.edge.StreetTransitEntranceLink; import org.opentripplanner.street.model.vertex.ExitVertex; +import org.opentripplanner.street.model.vertex.StationEntranceVertex; import org.opentripplanner.street.model.vertex.Vertex; import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.street.search.state.State; +import org.opentripplanner.transit.model.site.Entrance; /** * Process a list of states into a list of walking/driving instructions for a street leg. @@ -158,7 +161,7 @@ private void processState(State backState, State forwardState) { return; } else if (edge instanceof StreetTransitEntranceLink link) { var direction = relativeDirectionForTransitLink(link); - createAndSaveStep(backState, forwardState, link.getName(), direction, edge); + createAndSaveStep(backState, forwardState, link.getName(), direction, edge, link.entrance()); return; } @@ -175,8 +178,18 @@ private void processState(State backState, State forwardState) { if (edge instanceof ElevatorAlightEdge) { addStep(createElevatorWalkStep(backState, forwardState, edge)); return; + } else if (backState.getVertex() instanceof StationEntranceVertex stationEntranceVertex) { + addStep(createStationEntranceWalkStep(backState, forwardState, stationEntranceVertex)); + return; } else if (edge instanceof PathwayEdge pwe && pwe.signpostedAs().isPresent()) { - createAndSaveStep(backState, forwardState, pwe.signpostedAs().get(), FOLLOW_SIGNS, edge); + createAndSaveStep( + backState, + forwardState, + pwe.signpostedAs().get(), + FOLLOW_SIGNS, + edge, + null + ); return; } @@ -515,12 +528,33 @@ private WalkStepBuilder createElevatorWalkStep(State backState, State forwardSta return step; } + private WalkStepBuilder createStationEntranceWalkStep( + State backState, + State forwardState, + StationEntranceVertex vertex + ) { + Entrance entrance = 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( State backState, State forwardState, I18NString name, RelativeDirection direction, - Edge edge + Edge edge, + @Nullable Entrance entrance ) { addStep( createWalkStep(forwardState, backState) @@ -528,6 +562,7 @@ private void createAndSaveStep( .withNameIsDerived(false) .withDirections(lastAngle, DirectionUtils.getFirstAngle(edge.getGeometry()), false) .withRelativeDirection(direction) + .withEntrance(entrance) .addDistance(edge.getDistanceMeters()) ); diff --git a/application/src/main/java/org/opentripplanner/standalone/config/buildconfig/OsmConfig.java b/application/src/main/java/org/opentripplanner/standalone/config/buildconfig/OsmConfig.java index 1b2ec0ed74d..5cc67844b50 100644 --- a/application/src/main/java/org/opentripplanner/standalone/config/buildconfig/OsmConfig.java +++ b/application/src/main/java/org/opentripplanner/standalone/config/buildconfig/OsmConfig.java @@ -1,6 +1,7 @@ package org.opentripplanner.standalone.config.buildconfig; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_2; +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_7; import org.opentripplanner.graph_builder.module.osm.parameters.OsmExtractParameters; import org.opentripplanner.graph_builder.module.osm.parameters.OsmExtractParametersBuilder; @@ -84,6 +85,14 @@ public static OsmExtractParametersBuilder mapOsmGenericParameters( ) .docDefaultValue(docDefaults.timeZone()) .asZoneId(defaults.timeZone()) + ) + .withIncludeOsmSubwayEntrances( + node + .of("includeOsmSubwayEntrances") + .since(V2_7) + .summary("Whether to include subway entrances from the OSM data." + documentationAddition) + .docDefaultValue(docDefaults.includeOsmSubwayEntrances()) + .asBoolean(defaults.includeOsmSubwayEntrances()) ); } } diff --git a/application/src/main/java/org/opentripplanner/street/model/edge/StreetTransitEntranceLink.java b/application/src/main/java/org/opentripplanner/street/model/edge/StreetTransitEntranceLink.java index 7145f6183e4..34ca3faeeb3 100644 --- a/application/src/main/java/org/opentripplanner/street/model/edge/StreetTransitEntranceLink.java +++ b/application/src/main/java/org/opentripplanner/street/model/edge/StreetTransitEntranceLink.java @@ -2,6 +2,7 @@ import org.opentripplanner.street.model.vertex.StreetVertex; import org.opentripplanner.street.model.vertex.TransitEntranceVertex; +import org.opentripplanner.transit.model.site.Entrance; /** * This represents the connection between a street vertex and a transit vertex belonging the street @@ -43,6 +44,18 @@ public boolean isExit() { return !isEntrance; } + /** + * Get the {@link Entrance} that this edge links to. + */ + public Entrance entrance() { + if (getToVertex() instanceof TransitEntranceVertex tev) { + return tev.getEntrance(); + } else if (getFromVertex() instanceof TransitEntranceVertex tev) { + return tev.getEntrance(); + } + throw new IllegalStateException("%s doesn't link to an entrance.".formatted(this)); + } + protected int getStreetToStopTime() { return 0; } 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 new file mode 100644 index 00000000000..7b9a94b0725 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java @@ -0,0 +1,58 @@ +package org.opentripplanner.street.model.vertex; + +import javax.annotation.Nullable; +import org.opentripplanner.transit.model.basic.Accessibility; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.utils.tostring.ToStringBuilder; + +/** + * A station entrance extracted from OSM and therefore not (yet) associated with the transit + * entity {@link org.opentripplanner.transit.model.site.Station}. + */ +public class StationEntranceVertex extends OsmVertex { + + private static final String FEED_ID = "osm"; + private final String code; + private final Accessibility wheelchairAccessibility; + + public StationEntranceVertex( + double lat, + double lon, + long nodeId, + String code, + Accessibility wheelchairAccessibility + ) { + super(lat, lon, nodeId); + this.code = code; + this.wheelchairAccessibility = wheelchairAccessibility; + } + + /** + * The id of the entrance which may or may not be human-readable. + */ + public FeedScopedId id() { + return new FeedScopedId(FEED_ID, String.valueOf(nodeId)); + } + + /** + * Short human-readable code of the exit, like A or H3. + * If we need a proper name like "Oranienplatz" we have to add a name field. + */ + @Nullable + public String code() { + return code; + } + + public Accessibility wheelchairAccessibility() { + return wheelchairAccessibility; + } + + @Override + public String toString() { + return ToStringBuilder + .of(StationEntranceVertex.class) + .addNum("nodeId", nodeId) + .addStr("code", code) + .toString(); + } +} diff --git a/application/src/main/java/org/opentripplanner/street/model/vertex/VertexFactory.java b/application/src/main/java/org/opentripplanner/street/model/vertex/VertexFactory.java index 422fc16c837..393502ba3be 100644 --- a/application/src/main/java/org/opentripplanner/street/model/vertex/VertexFactory.java +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/VertexFactory.java @@ -11,6 +11,7 @@ import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; import org.opentripplanner.service.vehiclerental.street.VehicleRentalPlaceVertex; import org.opentripplanner.street.model.edge.StreetEdge; +import org.opentripplanner.transit.model.basic.Accessibility; import org.opentripplanner.transit.model.site.BoardingArea; import org.opentripplanner.transit.model.site.Entrance; import org.opentripplanner.transit.model.site.PathwayNode; @@ -94,6 +95,17 @@ public ExitVertex exit(long nid, Coordinate coordinate, String exitName) { return addToGraph(new ExitVertex(coordinate.x, coordinate.y, nid, exitName)); } + public StationEntranceVertex stationEntrance( + long nid, + Coordinate coordinate, + String code, + Accessibility wheelchairAccessibility + ) { + return addToGraph( + new StationEntranceVertex(coordinate.x, coordinate.y, nid, code, wheelchairAccessibility) + ); + } + public OsmVertex osm( Coordinate coordinate, OsmNode node, diff --git a/application/src/main/java/org/opentripplanner/transit/model/site/StationElementBuilder.java b/application/src/main/java/org/opentripplanner/transit/model/site/StationElementBuilder.java index 7a7fc0e4621..ea90231bead 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/site/StationElementBuilder.java +++ b/application/src/main/java/org/opentripplanner/transit/model/site/StationElementBuilder.java @@ -1,5 +1,6 @@ package org.opentripplanner.transit.model.site; +import javax.annotation.Nullable; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.transit.model.basic.Accessibility; @@ -54,7 +55,7 @@ public String code() { return code; } - public B withCode(String code) { + public B withCode(@Nullable String code) { this.code = code; return instance(); } 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 537d9b680b4..ce808e546d1 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -82,6 +82,9 @@ union CallStopLocation = Stop "Rental place union that represents either a VehicleRentalStation or a RentalVehicle" union RentalPlace = RentalVehicle | VehicleRentalStation +"A feature for a step" +union StepFeature = Entrance + union StopPosition = PositionAtStop | PositionBetweenStops "A public transport agency" @@ -488,6 +491,18 @@ type Emissions { co2: Grams } +"Station entrance or exit, originating from OSM or GTFS data." +type Entrance { + "ID of the entrance in the format of `FeedId:EntranceId`. If the `FeedId` is `osm`, the entrance originates from OSM data." + entranceId: String! + "Name of the entrance or exit." + name: String + "Short text or a number that identifies the entrance or exit for passengers. For example, `A` or `B`." + publicCode: String + "Whether the entrance or exit is accessible by wheelchair" + wheelchairAccessible: WheelchairBoarding +} + "Real-time estimates for an arrival or departure at a certain place." type EstimatedTime { """ @@ -2844,6 +2859,8 @@ 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" + feature: StepFeature "The latitude of the start of the step." lat: Float "The longitude of the start of the step." @@ -3518,15 +3535,40 @@ enum RealtimeState { UPDATED } -"Actions to take relative to the current position when engaging a walking/driving step." +""" +A direction that is not absolute but rather fuzzy and context-dependent. +It provides the passenger with information what they should do in this step depending on where they +were in the previous one. +""" enum RelativeDirection { CIRCLE_CLOCKWISE CIRCLE_COUNTERCLOCKWISE + """ + Moving straight ahead in one of these cases + + - Passing through a crossing or intersection. + - Passing through a station entrance or exit when it is not know whether the passenger is + entering or exiting. If it _is_ known then `ENTER_STATION`/`EXIT_STATION` is used. + More information about the entrance is in the `step.feature` field. + """ CONTINUE DEPART ELEVATOR + """ + Entering a public transport station. If it's not known if the passenger is entering or exiting + then `CONTINUE` is used. + + More information about the entrance is in the `step.feature` field. + """ ENTER_STATION + """ + Exiting a public transport station. If it's not known if the passenger is entering or exiting + then `CONTINUE` is used. + + More information about the entrance is in the `step.feature` field. + """ EXIT_STATION + "Follow the signs indicating a specific location like \"platform 1\" or \"exit B\"." FOLLOW_SIGNS HARD_LEFT HARD_RIGHT 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 7e1bf24287a..2f190502ccc 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -66,7 +66,6 @@ import org.opentripplanner.routing.alertpatch.TimePeriod; import org.opentripplanner.routing.alertpatch.TransitAlert; import org.opentripplanner.routing.api.request.RouteRequest; -import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graphfinder.GraphFinder; import org.opentripplanner.routing.graphfinder.NearbyStop; import org.opentripplanner.routing.graphfinder.PlaceAtDistance; @@ -87,6 +86,7 @@ import org.opentripplanner.standalone.config.framework.json.JsonSupport; import org.opentripplanner.test.support.FilePatternSource; import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; +import org.opentripplanner.transit.model.basic.Accessibility; import org.opentripplanner.transit.model.basic.Money; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.AbstractBuilder; @@ -96,6 +96,7 @@ import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.organization.Agency; +import org.opentripplanner.transit.model.site.Entrance; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; @@ -137,8 +138,6 @@ class GraphQLIntegrationTest { .withSystem("Network-1", "https://foo.bar") .build(); - static final Graph GRAPH = new Graph(); - static final Instant ALERT_START_TIME = OffsetDateTime .parse("2023-02-15T12:03:28+01:00") .toInstant(); @@ -267,9 +266,20 @@ public Set findRoutes(StopLocation stop) { .withAbsoluteDirection(20) .build(); var step2 = walkStep("elevator").withRelativeDirection(RelativeDirection.ELEVATOR).build(); + FeedScopedId entranceId = new FeedScopedId("osm", "123"); + Entrance entrance = Entrance + .of(entranceId) + .withCoordinate(new WgsCoordinate(60, 80)) + .withCode("A") + .withWheelchairAccessibility(Accessibility.POSSIBLE) + .build(); + var step3 = walkStep("entrance") + .withRelativeDirection(RelativeDirection.ENTER_OR_EXIT_STATION) + .withEntrance(entrance) + .build(); Itinerary i1 = newItinerary(A, T11_00) - .walk(20, B, List.of(step1, step2)) + .walk(20, B, List.of(step1, step2, step3)) .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/apis/gtfs/mapping/DirectionMapperTest.java b/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/DirectionMapperTest.java index 2c69f3dca46..1dcd6e210a3 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/DirectionMapperTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/DirectionMapperTest.java @@ -23,6 +23,7 @@ void absoluteDirection() { void relativeDirection() { Arrays .stream(RelativeDirection.values()) + .filter(v -> v != RelativeDirection.ENTER_OR_EXIT_STATION) .forEach(d -> { var mapped = DirectionMapper.map(d); assertEquals(d.toString(), mapped.toString()); diff --git a/application/src/test/java/org/opentripplanner/apis/transmodel/model/EnumTypesTest.java b/application/src/test/java/org/opentripplanner/apis/transmodel/model/EnumTypesTest.java index 3d834fced58..9090cd1bdc5 100644 --- a/application/src/test/java/org/opentripplanner/apis/transmodel/model/EnumTypesTest.java +++ b/application/src/test/java/org/opentripplanner/apis/transmodel/model/EnumTypesTest.java @@ -1,14 +1,23 @@ package org.opentripplanner.apis.transmodel.model; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.opentripplanner.apis.transmodel.model.EnumTypes.RELATIVE_DIRECTION; import static org.opentripplanner.apis.transmodel.model.EnumTypes.ROUTING_ERROR_CODE; import static org.opentripplanner.apis.transmodel.model.EnumTypes.map; +import graphql.GraphQLContext; import java.util.EnumSet; import java.util.List; +import java.util.Locale; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.opentripplanner.apis.transmodel.mapping.RelativeDirectionMapper; import org.opentripplanner.framework.doc.DocumentedEnum; +import org.opentripplanner.model.plan.RelativeDirection; import org.opentripplanner.routing.api.response.RoutingErrorCode; class EnumTypesTest { @@ -75,6 +84,18 @@ void testMap() { assertEquals("DocumentedEnumMapping[apiName=iH, internal=Hi]", mapping.toString()); } + @ParameterizedTest + @EnumSource(RelativeDirection.class) + void serializeRelativeDirection(RelativeDirection direction) { + var value = RELATIVE_DIRECTION.serialize( + RelativeDirectionMapper.map(direction), + GraphQLContext.getDefault(), + Locale.ENGLISH + ); + assertInstanceOf(String.class, value); + assertNotNull(value); + } + @Test void assertAllRoutingErrorCodesAreMapped() { var expected = EnumSet.allOf(RoutingErrorCode.class); diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilderTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilderTest.java index b712ee48c5a..1516c9df91a 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilderTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilderTest.java @@ -48,7 +48,7 @@ public Graph buildGraph(final TestInfo testInfo) { final WalkableAreaBuilder walkableAreaBuilder = new WalkableAreaBuilder( graph, osmdb, - new VertexGenerator(osmdb, graph, Set.of()), + new VertexGenerator(osmdb, graph, Set.of(), false), new DefaultNamer(), new SafetyValueNormalizer(graph, DataImportIssueStore.NOOP), DataImportIssueStore.NOOP, diff --git a/application/src/test/java/org/opentripplanner/osm/model/OsmWithTagsTest.java b/application/src/test/java/org/opentripplanner/osm/model/OsmWithTagsTest.java index 84b74b8f655..597593f7333 100644 --- a/application/src/test/java/org/opentripplanner/osm/model/OsmWithTagsTest.java +++ b/application/src/test/java/org/opentripplanner/osm/model/OsmWithTagsTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.opentripplanner.osm.wayproperty.specifier.WayTestData; +import org.opentripplanner.transit.model.basic.Accessibility; public class OsmWithTagsTest { @@ -215,6 +216,20 @@ void isWheelchairAccessible() { assertTrue(osm3.isWheelchairAccessible()); } + @Test + void wheelchairAccessibility() { + var osm1 = new OsmWithTags(); + assertEquals(Accessibility.NO_INFORMATION, osm1.wheelchairAccessibility()); + + var osm2 = new OsmWithTags(); + osm2.addTag("wheelchair", "no"); + assertEquals(Accessibility.NOT_POSSIBLE, osm2.wheelchairAccessibility()); + + var osm3 = new OsmWithTags(); + osm3.addTag("wheelchair", "yes"); + assertEquals(Accessibility.POSSIBLE, osm3.wheelchairAccessibility()); + } + @Test void isRoutable() { assertFalse(WayTestData.zooPlatform().isRoutable()); 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 de9fe21718a..a2bb428a78c 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 @@ -13,6 +13,7 @@ import org.opentripplanner.model.plan.WalkStep; import org.opentripplanner.routing.services.notes.StreetNotesService; import org.opentripplanner.street.search.state.TestStateBuilder; +import org.opentripplanner.transit.model.framework.FeedScopedId; class StatesToWalkStepsMapperTest { @@ -42,6 +43,7 @@ void enterStation() { var walkSteps = buildWalkSteps(builder); assertEquals(2, walkSteps.size()); var enter = walkSteps.get(1); + assertEquals(new FeedScopedId("F", "Lichterfelde-Ost"), enter.entrance().get().getId()); assertEquals(ENTER_STATION, enter.getRelativeDirection()); } @@ -53,8 +55,9 @@ void exitStation() { .exitStation("Lichterfelde-Ost"); var walkSteps = buildWalkSteps(builder); assertEquals(3, walkSteps.size()); - var enter = walkSteps.get(2); - assertEquals(EXIT_STATION, enter.getRelativeDirection()); + var exit = walkSteps.get(2); + assertEquals(new FeedScopedId("F", "Lichterfelde-Ost"), exit.entrance().get().getId()); + assertEquals(EXIT_STATION, exit.getRelativeDirection()); } @Test 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 49908207d44..95adec34ea8 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 @@ -11,13 +11,27 @@ "streetName": "street", "area": false, "relativeDirection": "DEPART", - "absoluteDirection": "NORTHEAST" + "absoluteDirection": "NORTHEAST", + "feature": null }, { "streetName": "elevator", "area": false, "relativeDirection": "ELEVATOR", - "absoluteDirection": null + "absoluteDirection": null, + "feature": null + }, + { + "streetName": "entrance", + "area": false, + "relativeDirection": "CONTINUE", + "absoluteDirection": null, + "feature": { + "__typename": "Entrance", + "publicCode": "A", + "entranceId": "osm:123", + "wheelchairAccessible": "POSSIBLE" + } } ] }, 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 dd2b96395ad..18cb5a8d49d 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 @@ -20,6 +20,14 @@ area relativeDirection absoluteDirection + feature { + __typename + ... on Entrance { + publicCode + entranceId + wheelchairAccessible + } + } } } } diff --git a/doc/user/BuildConfiguration.md b/doc/user/BuildConfiguration.md index 99e98066e73..c5fdfa8095b 100644 --- a/doc/user/BuildConfiguration.md +++ b/doc/user/BuildConfiguration.md @@ -84,10 +84,12 @@ Sections follow that describe particular settings in more depth. |    [sharedGroupFilePattern](#nd_sharedGroupFilePattern) | `regexp` | Pattern for matching shared group NeTEx files in a NeTEx bundle. | *Optional* | `"(\w{3})-.*-shared\.xml"` | 2.0 | |    [ferryIdsNotAllowedForBicycle](#nd_ferryIdsNotAllowedForBicycle) | `string[]` | List ferries which do not allow bikes. | *Optional* | | 2.0 | | [osm](#osm) | `object[]` | Configure properties for a given OpenStreetMap feed. | *Optional* | | 2.2 | +|       includeOsmSubwayEntrances | `boolean` | Whether to include subway entrances from the OSM data. Overrides the value specified in `osmDefaults`. | *Optional* | `false` | 2.7 | |       [osmTagMapping](#osm_0_osmTagMapping) | `enum` | The named set of mapping rules applied when parsing OSM tags. Overrides the value specified in `osmDefaults`. | *Optional* | `"default"` | 2.2 | |       source | `uri` | The unique URI pointing to the data file. | *Required* | | 2.2 | |       timeZone | `time-zone` | The timezone used to resolve opening hours in OSM data. Overrides the value specified in `osmDefaults`. | *Optional* | | 2.2 | | osmDefaults | `object` | Default properties for OpenStreetMap feeds. | *Optional* | | 2.2 | +|    includeOsmSubwayEntrances | `boolean` | Whether to include subway entrances from the OSM data. | *Optional* | `false` | 2.7 | |    [osmTagMapping](#od_osmTagMapping) | `enum` | The named set of mapping rules applied when parsing OSM tags. | *Optional* | `"default"` | 2.2 | |    timeZone | `time-zone` | The timezone used to resolve opening hours in OSM data. | *Optional* | | 2.2 | | [transferRequests](RouteRequest.md) | `object[]` | Routing requests to use for pre-calculating stop-to-stop transfers. | *Optional* | | 2.1 |