diff --git a/.github/workflows/performance-test.yml b/.github/workflows/performance-test.yml index 5e55e158a6f..d4376c242c5 100644 --- a/.github/workflows/performance-test.yml +++ b/.github/workflows/performance-test.yml @@ -91,7 +91,7 @@ jobs: cp otp-shaded/target/otp-shaded-*-SNAPSHOT.jar otp.jar java -Xmx32G -jar otp.jar --build --save test/performance/${{ matrix.location }}/ - - name: Run speed test + - name: Run RAPTOR speed test if: matrix.profile == 'core' || github.ref == 'refs/heads/dev-2.x' env: PERFORMANCE_INFLUX_DB_PASSWORD: ${{ secrets.PERFORMANCE_INFLUX_DB_PASSWORD }} @@ -113,3 +113,12 @@ jobs: with: name: ${{ matrix.location }}-flight-recorder path: application/${{ matrix.location }}-speed-test.jfr + + - name: Run transfer cache speed test + if: matrix.profile == 'core' || github.ref == 'refs/heads/dev-2.x' + env: + PERFORMANCE_INFLUX_DB_PASSWORD: ${{ secrets.PERFORMANCE_INFLUX_DB_PASSWORD }} + SPEEDTEST_LOCATION: ${{ matrix.location }} + MAVEN_OPTS: "-Xmx50g -Dmaven.repo.local=/home/lenni/.m2/repository/" + run: | + mvn --projects application exec:java -Dexec.mainClass="org.opentripplanner.transit.speed_test.TransferCacheTest" -Dexec.classpathScope=test -Dexec.args="--dir=test/performance/${{ matrix.location }}" diff --git a/application/src/client/graphiql/index.html b/application/src/client/graphiql/index.html index 0e8d4afd1ea..7b1786d93cf 100644 --- a/application/src/client/graphiql/index.html +++ b/application/src/client/graphiql/index.html @@ -30,13 +30,14 @@ copy them directly into your environment, or perhaps include them in your favored resource bundler. --> - + OTP GraphQL Explorer
Loading...
- + + - + +
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-test/java/org/opentripplanner/ext/vehicleparking/bikely/BikelyUpdaterTest.java b/application/src/ext-test/java/org/opentripplanner/ext/vehicleparking/bikely/BikelyUpdaterTest.java index 66e0a0fda9a..dbaa5b2eb36 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/vehicleparking/bikely/BikelyUpdaterTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/vehicleparking/bikely/BikelyUpdaterTest.java @@ -7,6 +7,7 @@ import java.time.Duration; import java.util.Locale; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.opentripplanner.service.vehicleparking.model.VehicleParkingState; import org.opentripplanner.test.support.ResourceLoader; @@ -17,6 +18,7 @@ public class BikelyUpdaterTest { @Test + @Disabled void parseBikeBoxes() { var uri = ResourceLoader.of(this).uri("bikely.json"); var parameters = new BikelyUpdaterParameters( @@ -41,8 +43,9 @@ void parseBikeBoxes() { assertEquals( "First 12 hour(s) is NOK0.00, afterwards NOK10.00 per 1 hour(s)", - first.getNote().toString(Locale.ENGLISH) + first.getNote().toString(Locale.ROOT) ); + // This test fails in the entur ci pipline assertEquals( "Første 12 time(r) er kr 0,00. Deretter kr 10,00 per 1 time(r)", first.getNote().toString(Locales.NORWEGIAN_BOKMAL) diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java b/application/src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java index aeeab84259c..8dbcf4d785e 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java @@ -199,7 +199,7 @@ private FlexAccessEgress createFlexAccessEgress( return null; } - final var finalStateOpt = EdgeTraverser.traverseEdges(afterFlexState, transferEdges); + final var finalStateOpt = EdgeTraverser.traverseEdges(afterFlexState[0], transferEdges); return finalStateOpt .map(finalState -> { diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java b/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java index ae35c262a1e..f27a502911f 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java @@ -113,7 +113,7 @@ private Optional createDirectGraphPath( final State[] afterFlexState = flexEdge.traverse(accessNearbyStop.state); - var finalStateOpt = EdgeTraverser.traverseEdges(afterFlexState, egress.edges); + var finalStateOpt = EdgeTraverser.traverseEdges(afterFlexState[0], egress.edges); if (finalStateOpt.isEmpty()) { return Optional.empty(); 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/ext/resources/org/opentripplanner/ext/apis/transmodel/custom-documentation-entur.properties b/application/src/ext/resources/org/opentripplanner/ext/apis/transmodel/custom-documentation-entur.properties new file mode 100644 index 00000000000..29c44b67f96 --- /dev/null +++ b/application/src/ext/resources/org/opentripplanner/ext/apis/transmodel/custom-documentation-entur.properties @@ -0,0 +1,27 @@ +# Use: +# [.].(description|deprecated)[.append] +# +# Examples +# // Replace the existing type description +# Quay.description=The place for boarding/alighting a vehicle +# +# // Append to the existing type description +# Quay.description.append=Append +# +# // Replace the existing field description +# Quay.name.description=The public name +# +# // Append to the existing field description +# Quay.name.description.append=(Source NSR) +# +# // Insert deprecated reason. Due to a bug in the Java GraphQL lib, an existing deprecated +# // reason cannot be updated. Deleting the reason from the schema, and adding it back using +# // the "default" TransmodelApiDocumentationProfile is a workaround. +# Quay.name.deprecated=This field is deprecated ... + + +TariffZone.description=A **zone** used to define a zonal fare structure in a zone-counting or \ + zone-matrix system. This includes TariffZone, as well as the specialised FareZone elements. \ + TariffZones are deprecated, please use FareZones. \ + \ + **TariffZone data will not be maintained from 1. MAY 2025 (Entur).** 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/support/graphql/injectdoc/ApiDocumentationProfile.java b/application/src/main/java/org/opentripplanner/apis/support/graphql/injectdoc/ApiDocumentationProfile.java new file mode 100644 index 00000000000..93452589ace --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/support/graphql/injectdoc/ApiDocumentationProfile.java @@ -0,0 +1,30 @@ +package org.opentripplanner.apis.support.graphql.injectdoc; + +import org.opentripplanner.framework.doc.DocumentedEnum; + +public enum ApiDocumentationProfile implements DocumentedEnum { + DEFAULT, + ENTUR; + + private static final String TYPE_DOC = + """ + List of available custom documentation profiles. A profile is used to inject custom + documentation like type and field description or a deprecated reason. + + Currently, ONLY the Transmodel API supports this feature. + """; + + @Override + public String typeDescription() { + return TYPE_DOC; + } + + @Override + public String enumValueDescription() { + return switch (this) { + case DEFAULT -> "Default documentation is used."; + case ENTUR -> "Entur specific documentation. This deprecate features not supported at Entur," + + " Norway."; + }; + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/support/graphql/injectdoc/CustomDocumentation.java b/application/src/main/java/org/opentripplanner/apis/support/graphql/injectdoc/CustomDocumentation.java new file mode 100644 index 00000000000..4afe0cf6952 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/support/graphql/injectdoc/CustomDocumentation.java @@ -0,0 +1,173 @@ +package org.opentripplanner.apis.support.graphql.injectdoc; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import javax.annotation.Nullable; +import org.opentripplanner.framework.application.OtpAppException; +import org.opentripplanner.utils.text.TextVariablesSubstitution; + +/** + * Load custom documentation from a properties file and make it available to any + * consumer using the {@code type-name[.field-name]} as key for lookups. + */ +public class CustomDocumentation { + + private static final String APPEND_SUFFIX = ".append"; + private static final String DESCRIPTION_SUFFIX = ".description"; + private static final String DEPRECATED_SUFFIX = ".deprecated"; + + /** Put custom documentaion in the following sandbox package */ + private static final String DOC_PATH = "org/opentripplanner/ext/apis/transmodel/"; + private static final String FILE_NAME = "custom-documentation"; + private static final String FILE_EXTENSION = ".properties"; + + private static final CustomDocumentation EMPTY = new CustomDocumentation(Map.of()); + + private final Map textMap; + + /** + * Package local to be unit-testable + */ + CustomDocumentation(Map textMap) { + this.textMap = textMap; + } + + public static CustomDocumentation of(ApiDocumentationProfile profile) { + if (profile == ApiDocumentationProfile.DEFAULT) { + return EMPTY; + } + var map = loadCustomDocumentationFromPropertiesFile(profile); + return map.isEmpty() ? EMPTY : new CustomDocumentation(map); + } + + public boolean isEmpty() { + return textMap.isEmpty(); + } + + /** + * Get documentation for a type. The given {@code typeName} is used as the key. The + * documentation text is resolved by: + *
    + *
  1. + * first looking up the given {@code key} + {@code ".description"}. If a value is found, then + * the value is returned. + *
  2. + * then {@code key} + {@code ".description.append"} is used. If a value is found the + * {@code originalDoc} + {@code value} is returned. + *
  3. + *
+ * @param typeName Use {@code TYPE_NAME} or {@code TYPE_NAME.FIELD_NAME} as key. + */ + public Optional typeDescription(String typeName, @Nullable String originalDoc) { + return text(typeName, DESCRIPTION_SUFFIX, originalDoc); + } + + /** + * Same as {@link #typeDescription(String, String)} except the given {@code typeName} and + * {@code fieldName} is used as the key. + *
+   * key := typeName + "." fieldNAme
+   * 
+ */ + public Optional fieldDescription( + String typeName, + String fieldName, + @Nullable String originalDoc + ) { + return text(key(typeName, fieldName), DESCRIPTION_SUFFIX, originalDoc); + } + + /** + * Get deprecated reason for a field (types cannot be deprecated). The key + * ({@code key = typeName + '.' + fieldName} is used to retrieve the reason from the properties + * file. The deprecated documentation text is resolved by: + *
    + *
  1. + * first looking up the given {@code key} + {@code ".deprecated"}. If a value is found, then + * the value is returned. + *
  2. + * then {@code key} + {@code ".deprecated.append"} is used. If a value is found the + * {@code originalDoc} + {@code text} is returned. + *
  3. + *
+ * Any {@code null} values are excluded from the result and if both the input {@code originalDoc} + * and the resolved value is {@code null}, then {@code empty} is returned. + */ + public Optional fieldDeprecatedReason( + String typeName, + String fieldName, + @Nullable String originalDoc + ) { + return text(key(typeName, fieldName), DEPRECATED_SUFFIX, originalDoc); + } + + /* private methods */ + + /** + * Create a key from the given {@code typeName} and {@code fieldName} + */ + private static String key(String typeName, String fieldName) { + return typeName + "." + fieldName; + } + + private Optional text(String key, String suffix, @Nullable String originalText) { + final String k = key + suffix; + return text(k).or(() -> appendText(k, originalText)); + } + + private Optional text(String key) { + return Optional.ofNullable(textMap.get(key)); + } + + private Optional appendText(String key, @Nullable String originalText) { + String value = textMap.get(key + APPEND_SUFFIX); + if (value == null) { + return Optional.empty(); + } + return originalText == null ? Optional.of(value) : Optional.of(originalText + "\n\n" + value); + } + + /* private methods */ + + private static Map loadCustomDocumentationFromPropertiesFile( + ApiDocumentationProfile profile + ) { + try { + final String resource = resourceName(profile); + var input = ClassLoader.getSystemResourceAsStream(resource); + if (input == null) { + throw new OtpAppException("Resource not found: %s", resource); + } + var props = new Properties(); + props.load(new InputStreamReader(input, StandardCharsets.UTF_8)); + Map map = new HashMap<>(); + + for (String key : props.stringPropertyNames()) { + String value = props.getProperty(key); + if (value == null) { + value = ""; + } + map.put(key, value); + } + return TextVariablesSubstitution.insertVariables( + map, + varName -> errorHandlerVariableSubstitution(varName, resource) + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void errorHandlerVariableSubstitution(String name, String source) { + throw new OtpAppException("Variable substitution failed for '${%s}' in %s.", name, source); + } + + private static String resourceName(ApiDocumentationProfile profile) { + return DOC_PATH + FILE_NAME + "-" + profile.name().toLowerCase() + FILE_EXTENSION; + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentation.java b/application/src/main/java/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentation.java new file mode 100644 index 00000000000..4e98f202d90 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentation.java @@ -0,0 +1,173 @@ +package org.opentripplanner.apis.support.graphql.injectdoc; + +import static graphql.util.TraversalControl.CONTINUE; + +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLEnumValueDefinition; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLInterfaceType; +import graphql.schema.GraphQLNamedSchemaElement; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLTypeVisitor; +import graphql.schema.GraphQLTypeVisitorStub; +import graphql.schema.GraphQLUnionType; +import graphql.util.TraversalControl; +import graphql.util.TraverserContext; +import java.util.Optional; +import java.util.function.BiFunction; + +/** + * This is GraphQL visitor which injects custom documentation on types and fields. + */ +public class InjectCustomDocumentation + extends GraphQLTypeVisitorStub + implements GraphQLTypeVisitor { + + private final CustomDocumentation customDocumentation; + + public InjectCustomDocumentation(CustomDocumentation customDocumentation) { + this.customDocumentation = customDocumentation; + } + + @Override + public TraversalControl visitGraphQLScalarType( + GraphQLScalarType scalar, + TraverserContext context + ) { + return typeDoc(context, scalar, (s, doc) -> s.transform(b -> b.description(doc))); + } + + @Override + public TraversalControl visitGraphQLInterfaceType( + GraphQLInterfaceType interface_, + TraverserContext context + ) { + return typeDoc(context, interface_, (f, doc) -> f.transform(b -> b.description(doc))); + } + + @Override + public TraversalControl visitGraphQLEnumType( + GraphQLEnumType enumType, + TraverserContext context + ) { + return typeDoc(context, enumType, (f, doc) -> f.transform(b -> b.description(doc))); + } + + @Override + public TraversalControl visitGraphQLEnumValueDefinition( + GraphQLEnumValueDefinition enumValue, + TraverserContext context + ) { + return fieldDoc( + context, + enumValue, + enumValue.getDeprecationReason(), + (f, doc) -> f.transform(b -> b.description(doc)), + (f, reason) -> f.transform(b -> b.deprecationReason(reason)) + ); + } + + @Override + public TraversalControl visitGraphQLFieldDefinition( + GraphQLFieldDefinition field, + TraverserContext context + ) { + return fieldDoc( + context, + field, + field.getDeprecationReason(), + (f, doc) -> f.transform(b -> b.description(doc)), + (f, reason) -> f.transform(b -> b.deprecate(reason)) + ); + } + + @Override + public TraversalControl visitGraphQLInputObjectField( + GraphQLInputObjectField inputField, + TraverserContext context + ) { + return fieldDoc( + context, + inputField, + inputField.getDeprecationReason(), + (f, doc) -> f.transform(b -> b.description(doc)), + (f, reason) -> f.transform(b -> b.deprecate(reason)) + ); + } + + @Override + public TraversalControl visitGraphQLInputObjectType( + GraphQLInputObjectType inputType, + TraverserContext context + ) { + return typeDoc(context, inputType, (f, doc) -> f.transform(b -> b.description(doc))); + } + + @Override + public TraversalControl visitGraphQLObjectType( + GraphQLObjectType object, + TraverserContext context + ) { + return typeDoc(context, object, (f, doc) -> f.transform(b -> b.description(doc))); + } + + @Override + public TraversalControl visitGraphQLUnionType( + GraphQLUnionType union, + TraverserContext context + ) { + return typeDoc(context, union, (f, doc) -> f.transform(b -> b.description(doc))); + } + + /* private methods */ + + /** + * Set or append description on a Scalar, Object, InputType, Union, Interface or Enum. + */ + private TraversalControl typeDoc( + TraverserContext context, + T element, + BiFunction setDescription + ) { + customDocumentation + .typeDescription(element.getName(), element.getDescription()) + .map(doc -> setDescription.apply(element, doc)) + .ifPresent(f -> changeNode(context, f)); + return CONTINUE; + } + + /** + * Set or append description and deprecated reason on a field [Object, InputType, Interface, + * Union or Enum]. + */ + private TraversalControl fieldDoc( + TraverserContext context, + T field, + String originalDeprecatedReason, + BiFunction setDescription, + BiFunction setDeprecatedReason + ) { + // All fields need to be defined in a named element + if (!(context.getParentNode() instanceof GraphQLNamedSchemaElement parent)) { + throw new IllegalArgumentException("The field does not have a named parent: " + field); + } + var fieldName = field.getName(); + var typeName = parent.getName(); + + Optional withDescription = customDocumentation + .fieldDescription(typeName, fieldName, field.getDescription()) + .map(doc -> setDescription.apply(field, doc)); + + Optional withDeprecated = customDocumentation + .fieldDeprecatedReason(typeName, fieldName, originalDeprecatedReason) + .map(doc -> setDeprecatedReason.apply(withDescription.orElse(field), doc)); + + withDeprecated.or(() -> withDescription).ifPresent(f -> changeNode(context, f)); + + return CONTINUE; + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/TransmodelAPI.java b/application/src/main/java/org/opentripplanner/apis/transmodel/TransmodelAPI.java index 66377a56390..62b9b5f0a45 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/TransmodelAPI.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/TransmodelAPI.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; +import org.opentripplanner.apis.support.graphql.injectdoc.ApiDocumentationProfile; import org.opentripplanner.apis.transmodel.mapping.TransitIdMapper; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.standalone.api.OtpServerRequestContext; @@ -80,6 +81,7 @@ public static void setUp( TransmodelAPIParameters config, TimetableRepository timetableRepository, RouteRequest defaultRouteRequest, + ApiDocumentationProfile documentationProfile, TransitRoutingConfig transitRoutingConfig ) { if (config.hideFeedId()) { @@ -91,6 +93,7 @@ public static void setUp( TransmodelGraphQLSchema.create( defaultRouteRequest, timetableRepository.getTimeZone(), + documentationProfile, transitRoutingConfig ); } diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java b/application/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java index 0561ec9de85..0eed3d3fb84 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java @@ -9,6 +9,7 @@ import static org.opentripplanner.apis.transmodel.model.EnumTypes.MULTI_MODAL_MODE; import static org.opentripplanner.apis.transmodel.model.EnumTypes.TRANSPORT_MODE; import static org.opentripplanner.apis.transmodel.model.scalars.DateTimeScalarFactory.createMillisecondsSinceEpochAsDateTimeStringScalar; +import static org.opentripplanner.apis.transmodel.support.GqlUtil.toListNullSafe; import static org.opentripplanner.model.projectinfo.OtpProjectInfo.projectInfo; import graphql.Scalars; @@ -28,6 +29,7 @@ import graphql.schema.GraphQLOutputType; import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLSchema; +import graphql.schema.SchemaTransformer; import java.time.LocalDate; import java.time.ZoneId; import java.util.ArrayList; @@ -43,8 +45,12 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.Nullable; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; +import org.opentripplanner.apis.support.graphql.injectdoc.ApiDocumentationProfile; +import org.opentripplanner.apis.support.graphql.injectdoc.CustomDocumentation; +import org.opentripplanner.apis.support.graphql.injectdoc.InjectCustomDocumentation; import org.opentripplanner.apis.transmodel.mapping.PlaceMapper; import org.opentripplanner.apis.transmodel.mapping.TransitIdMapper; import org.opentripplanner.apis.transmodel.model.DefaultRouteRequestType; @@ -157,10 +163,12 @@ private TransmodelGraphQLSchema( public static GraphQLSchema create( RouteRequest defaultRequest, ZoneId timeZoneId, - TransitTuningParameters transitTuningParameters + ApiDocumentationProfile docProfile, + TransitTuningParameters transitTuning ) { - return new TransmodelGraphQLSchema(defaultRequest, timeZoneId, transitTuningParameters) - .create(); + var schema = new TransmodelGraphQLSchema(defaultRequest, timeZoneId, transitTuning).create(); + schema = decorateSchemaWithCustomDocumentation(schema, docProfile); + return schema; } @SuppressWarnings("unchecked") @@ -1310,11 +1318,11 @@ private GraphQLSchema create() { ); var privateCodes = FilterValues.ofEmptyIsEverything( "privateCodes", - environment.>getArgument("privateCodes") + toListNullSafe(environment.>getArgument("privateCodes")) ); var activeServiceDates = FilterValues.ofEmptyIsEverything( "activeDates", - environment.>getArgument("activeDates") + toListNullSafe(environment.>getArgument("activeDates")) ); TripRequest tripRequest = TripRequest @@ -1621,7 +1629,7 @@ private GraphQLSchema create() { .field(DatedServiceJourneyQuery.createQuery(datedServiceJourneyType)) .build(); - return GraphQLSchema + var schema = GraphQLSchema .newSchema() .query(queryType) .additionalType(placeInterface) @@ -1629,9 +1637,23 @@ private GraphQLSchema create() { .additionalType(Relay.pageInfoType) .additionalDirective(TransmodelDirectives.TIMING_DATA) .build(); + + return schema; + } + + private static GraphQLSchema decorateSchemaWithCustomDocumentation( + GraphQLSchema schema, + ApiDocumentationProfile docProfile + ) { + var customDocumentation = CustomDocumentation.of(docProfile); + if (customDocumentation.isEmpty()) { + return schema; + } + var visitor = new InjectCustomDocumentation(customDocumentation); + return SchemaTransformer.transformSchema(schema, visitor); } - private List toIdList(List ids) { + private List toIdList(@Nullable List ids) { if (ids == null) { return Collections.emptyList(); } 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/apis/transmodel/model/timetable/BookingArrangementType.java b/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/BookingArrangementType.java index 727dc37d99b..911ec8d9b0c 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/BookingArrangementType.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/BookingArrangementType.java @@ -109,7 +109,7 @@ public static GraphQLObjectType create() { .type(EnumTypes.PURCHASE_WHEN) .dataFetcher(environment -> { BookingInfo bookingInfo = bookingInfo(environment); - if (bookingInfo.getMinimumBookingNotice() != null) { + if (bookingInfo.getMinimumBookingNotice().isPresent()) { return null; } BookingTime latestBookingTime = bookingInfo.getLatestBookingTime(); diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/support/GqlUtil.java b/application/src/main/java/org/opentripplanner/apis/transmodel/support/GqlUtil.java index 8e34470ed6a..fa82ee0cd02 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/support/GqlUtil.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/support/GqlUtil.java @@ -6,8 +6,11 @@ import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLList; import graphql.schema.GraphQLNonNull; +import java.util.Collection; import java.util.List; import java.util.Locale; +import java.util.Objects; +import javax.annotation.Nullable; import org.opentripplanner.apis.transmodel.TransmodelRequestContext; import org.opentripplanner.apis.transmodel.mapping.TransitIdMapper; import org.opentripplanner.framework.graphql.GraphQLUtils; @@ -18,7 +21,7 @@ /** * Provide some of the commonly used "chain" of methods. Like all ids should be created the same - * wayThis + * way. */ public class GqlUtil { @@ -96,4 +99,15 @@ public static Locale getLocale(DataFetchingEnvironment environment) { ? GraphQLUtils.getLocale(environment, lang) : GraphQLUtils.getLocale(environment); } + + /** + * Null-safe handling of a collection of type T. Returns an empty list if the collection is null. + * Null elements are filtered out. + */ + public static List toListNullSafe(@Nullable Collection args) { + if (args == null) { + return List.of(); + } + return args.stream().filter(Objects::nonNull).toList(); + } } 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 0b071b64728..744a8209702 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java @@ -17,7 +17,9 @@ import org.opentripplanner.graph_builder.issue.api.DataImportIssueSummary; import org.opentripplanner.graph_builder.model.GraphBuilderModule; import org.opentripplanner.graph_builder.module.configure.DaggerGraphBuilderFactory; +import org.opentripplanner.graph_builder.module.configure.GraphBuilderFactory; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.worldenvelope.WorldEnvelopeRepository; import org.opentripplanner.standalone.config.BuildConfig; @@ -62,6 +64,7 @@ public static GraphBuilder create( BuildConfig config, GraphBuilderDataSources dataSources, Graph graph, + OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, TimetableRepository timetableRepository, WorldEnvelopeRepository worldEnvelopeRepository, VehicleParkingRepository vehicleParkingService, @@ -78,10 +81,11 @@ public static GraphBuilder create( timetableRepository.initTimeZone(config.transitModelTimeZone); - var builder = DaggerGraphBuilderFactory - .builder() + GraphBuilderFactory.Builder builder = DaggerGraphBuilderFactory.builder(); + builder .config(config) .graph(graph) + .osmInfoGraphBuildRepository(osmInfoGraphBuildRepository) .timetableRepository(timetableRepository) .worldEnvelopeRepository(worldEnvelopeRepository) .vehicleParkingRepository(vehicleParkingService) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/OsmBoardingLocationsModule.java b/application/src/main/java/org/opentripplanner/graph_builder/module/OsmBoardingLocationsModule.java index c4acabefd6c..25e33b9c057 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/OsmBoardingLocationsModule.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/OsmBoardingLocationsModule.java @@ -1,18 +1,27 @@ package org.opentripplanner.graph_builder.module; import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; +import javax.annotation.Nullable; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Point; import org.opentripplanner.framework.geometry.GeometryUtils; import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; +import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.LocalizedString; import org.opentripplanner.graph_builder.model.GraphBuilderModule; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.index.StreetIndex; import org.opentripplanner.routing.linking.LinkingDirection; import org.opentripplanner.routing.linking.VertexLinker; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildService; +import org.opentripplanner.service.osminfo.model.Platform; import org.opentripplanner.street.model.StreetTraversalPermission; import org.opentripplanner.street.model.edge.AreaEdge; import org.opentripplanner.street.model.edge.BoardingLocationToStopLink; @@ -24,9 +33,12 @@ import org.opentripplanner.street.model.vertex.OsmBoardingLocationVertex; import org.opentripplanner.street.model.vertex.StreetVertex; import org.opentripplanner.street.model.vertex.TransitStopVertex; +import org.opentripplanner.street.model.vertex.Vertex; import org.opentripplanner.street.model.vertex.VertexFactory; import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.street.search.TraverseModeSet; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.StationElement; import org.opentripplanner.transit.service.TimetableRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,14 +67,20 @@ public class OsmBoardingLocationsModule implements GraphBuilderModule { private final Graph graph; + private final OsmInfoGraphBuildService osmInfoGraphBuildService; private final TimetableRepository timetableRepository; private final VertexFactory vertexFactory; private VertexLinker linker; @Inject - public OsmBoardingLocationsModule(Graph graph, TimetableRepository timetableRepository) { + public OsmBoardingLocationsModule( + Graph graph, + OsmInfoGraphBuildService osmInfoGraphBuildService, + TimetableRepository timetableRepository + ) { this.graph = graph; + this.osmInfoGraphBuildService = osmInfoGraphBuildService; this.timetableRepository = timetableRepository; this.vertexFactory = new VertexFactory(graph); } @@ -99,54 +117,32 @@ public void buildGraph() { } private boolean connectVertexToStop(TransitStopVertex ts, StreetIndex index) { - var stopCode = ts.getStop().getCode(); - var stopId = ts.getStop().getId().getId(); + if (connectVertexToNode(ts, index)) return true; + + if (connectVertexToWay(ts, index)) return true; + + return connectVertexToArea(ts, index); + } + + private Envelope getEnvelope(TransitStopVertex ts) { Envelope envelope = new Envelope(ts.getCoordinate()); double xscale = Math.cos(ts.getCoordinate().y * Math.PI / 180); envelope.expandBy(searchRadiusDegrees / xscale, searchRadiusDegrees); + return envelope; + } - // if the boarding location is an OSM node it's generated in the OSM processing step but we need - // link it here - var nearbyBoardingLocations = index - .getVerticesForEnvelope(envelope) - .stream() - .filter(OsmBoardingLocationVertex.class::isInstance) - .map(OsmBoardingLocationVertex.class::cast) - .collect(Collectors.toSet()); - - for (var boardingLocation : nearbyBoardingLocations) { - if ( - (stopCode != null && boardingLocation.references.contains(stopCode)) || - boardingLocation.references.contains(stopId) - ) { - if (!boardingLocation.isConnectedToStreetNetwork()) { - linker.linkVertexPermanently( - boardingLocation, - new TraverseModeSet(TraverseMode.WALK), - LinkingDirection.BOTH_WAYS, - (osmBoardingLocationVertex, splitVertex) -> { - if (osmBoardingLocationVertex == splitVertex) { - return List.of(); - } - // the OSM boarding location vertex is not connected to the street network, so we - // need to link it first - return List.of( - linkBoardingLocationToStreetNetwork(boardingLocation, splitVertex), - linkBoardingLocationToStreetNetwork(splitVertex, boardingLocation) - ); - } - ); - } - linkBoardingLocationToStop(ts, stopCode, boardingLocation); - return true; - } - } - - // if the boarding location is an OSM way (an area) then we are generating the vertex here and - // use the AreaEdgeList to link it to the correct vertices of the platform edge - var nearbyEdgeLists = index - .getEdgesForEnvelope(envelope) + /** + * Connect a transit stop vertex into a boarding location area in the index. + *

+ * A centroid vertex is generated in the area and connected to the vertices on the platform edge. + * + * @return if the vertex has been connected + */ + private boolean connectVertexToArea(TransitStopVertex ts, StreetIndex index) { + RegularStop stop = ts.getStop(); + var nearbyAreaEdgeList = index + .getEdgesForEnvelope(getEnvelope(ts)) .stream() .filter(AreaEdge.class::isInstance) .map(AreaEdge.class::cast) @@ -155,33 +151,141 @@ private boolean connectVertexToStop(TransitStopVertex ts, StreetIndex index) { // Iterate over all nearby areas representing transit stops in OSM, linking to them if they have a stop code or id // in their ref= tag that matches the GTFS stop code of this StopVertex. - for (var edgeList : nearbyEdgeLists) { - if ( - (stopCode != null && edgeList.references.contains(stopCode)) || - edgeList.references.contains(stopId) - ) { + for (var edgeList : nearbyAreaEdgeList) { + if (matchesReference(stop, edgeList.references)) { var name = edgeList .getAreas() .stream() .findFirst() .map(NamedArea::getName) .orElse(LOCALIZED_PLATFORM_NAME); - var label = "platform-centroid/%s".formatted(ts.getStop().getId().toString()); - var centroid = edgeList.getGeometry().getCentroid(); - var boardingLocation = vertexFactory.osmBoardingLocation( - new Coordinate(centroid.getX(), centroid.getY()), - label, + var boardingLocation = makeBoardingLocation( + stop, + edgeList.getGeometry().getCentroid(), edgeList.references, name ); linker.addPermanentAreaVertex(boardingLocation, edgeList); - linkBoardingLocationToStop(ts, stopCode, boardingLocation); + linkBoardingLocationToStop(ts, stop.getCode(), boardingLocation); + return true; + } + } + return false; + } + + /** + * Connect a transit stop vertex to a boarding location way in the index. + *

+ * The vertex is connected to the center of the way if one is found, splitting it if needed. + * + * @return if the vertex has been connected + */ + private boolean connectVertexToWay(TransitStopVertex ts, StreetIndex index) { + var stop = ts.getStop(); + var nearbyEdges = new HashMap>(); + + for (var edge : index.getEdgesForEnvelope(getEnvelope(ts))) { + osmInfoGraphBuildService + .findPlatform(edge) + .ifPresent(platform -> { + if (matchesReference(stop, platform.references())) { + if (!nearbyEdges.containsKey(platform)) { + var list = new ArrayList(); + list.add(edge); + nearbyEdges.put(platform, list); + } else { + nearbyEdges.get(platform).add(edge); + } + } + }); + } + + for (var platformEdgeList : nearbyEdges.entrySet()) { + Platform platform = platformEdgeList.getKey(); + var name = platform.name(); + var boardingLocation = makeBoardingLocation( + stop, + platform.geometry().getCentroid(), + platform.references(), + name + ); + for (var vertex : linker.linkToSpecificStreetEdgesPermanently( + boardingLocation, + new TraverseModeSet(TraverseMode.WALK), + LinkingDirection.BOTH_WAYS, + platformEdgeList.getValue().stream().map(StreetEdge.class::cast).collect(Collectors.toSet()) + )) { + linkBoardingLocationToStop(ts, stop.getCode(), vertex); + } + return true; + } + return false; + } + + /** + * Connect a transit stop vertex to a boarding location node. + *

+ * The node is generated in the OSM processing step but we need to link it here. + * + * @return If the vertex has been connected. + */ + private boolean connectVertexToNode(TransitStopVertex ts, StreetIndex index) { + var nearbyBoardingLocations = index + .getVerticesForEnvelope(getEnvelope(ts)) + .stream() + .filter(OsmBoardingLocationVertex.class::isInstance) + .map(OsmBoardingLocationVertex.class::cast) + .collect(Collectors.toSet()); + + for (var boardingLocation : nearbyBoardingLocations) { + if (matchesReference(ts.getStop(), boardingLocation.references)) { + if (!boardingLocation.isConnectedToStreetNetwork()) { + linker.linkVertexPermanently( + boardingLocation, + new TraverseModeSet(TraverseMode.WALK), + LinkingDirection.BOTH_WAYS, + (osmBoardingLocationVertex, splitVertex) -> + getConnectingEdges(boardingLocation, osmBoardingLocationVertex, splitVertex) + ); + } + linkBoardingLocationToStop(ts, ts.getStop().getCode(), boardingLocation); return true; } } return false; } + private OsmBoardingLocationVertex makeBoardingLocation( + RegularStop stop, + Point centroid, + Set refs, + I18NString name + ) { + var label = "platform-centroid/%s".formatted(stop.getId().toString()); + return vertexFactory.osmBoardingLocation( + new Coordinate(centroid.getX(), centroid.getY()), + label, + refs, + name + ); + } + + private List getConnectingEdges( + OsmBoardingLocationVertex boardingLocation, + Vertex osmBoardingLocationVertex, + StreetVertex splitVertex + ) { + if (osmBoardingLocationVertex == splitVertex) { + return List.of(); + } + // the OSM boarding location vertex is not connected to the street network, so we + // need to link it first + return List.of( + linkBoardingLocationToStreetNetwork(boardingLocation, splitVertex), + linkBoardingLocationToStreetNetwork(splitVertex, boardingLocation) + ); + } + private StreetEdge linkBoardingLocationToStreetNetwork(StreetVertex from, StreetVertex to) { var line = GeometryUtils.makeLineString(List.of(from.getCoordinate(), to.getCoordinate())); return new StreetEdgeBuilder<>() @@ -197,8 +301,8 @@ private StreetEdge linkBoardingLocationToStreetNetwork(StreetVertex from, Street private void linkBoardingLocationToStop( TransitStopVertex ts, - String stopCode, - OsmBoardingLocationVertex boardingLocation + @Nullable String stopCode, + StreetVertex boardingLocation ) { BoardingLocationToStopLink.createBoardingLocationToStopLink(ts, boardingLocation); BoardingLocationToStopLink.createBoardingLocationToStopLink(boardingLocation, ts); @@ -210,4 +314,11 @@ private void linkBoardingLocationToStop( boardingLocation.getCoordinate() ); } + + private boolean matchesReference(StationElement stop, Collection references) { + var stopCode = stop.getCode(); + var stopId = stop.getId().getId(); + + return (stopCode != null && references.contains(stopCode)) || references.contains(stopId); + } } 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 d4d00fdc2a0..4155ecf9614 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 @@ -30,6 +30,8 @@ import org.opentripplanner.gtfs.graphbuilder.GtfsModule; import org.opentripplanner.netex.NetexModule; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; +import org.opentripplanner.service.osminfo.configure.OsmInfoGraphBuildServiceModule; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.worldenvelope.WorldEnvelopeRepository; import org.opentripplanner.standalone.config.BuildConfig; @@ -37,7 +39,7 @@ import org.opentripplanner.transit.service.TimetableRepository; @Singleton -@Component(modules = { GraphBuilderModules.class }) +@Component(modules = { GraphBuilderModules.class, OsmInfoGraphBuildServiceModule.class }) public interface GraphBuilderFactory { //DataImportIssueStore issueStore(); GraphBuilder graphBuilder(); @@ -80,6 +82,9 @@ interface Builder { @BindsInstance Builder timetableRepository(TimetableRepository timetableRepository); + @BindsInstance + Builder osmInfoGraphBuildRepository(OsmInfoGraphBuildRepository osmInfoGraphBuildRepository); + @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 5371142d612..d464523a61a 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 @@ -43,13 +43,14 @@ import org.opentripplanner.osm.OsmProvider; import org.opentripplanner.routing.api.request.preference.WalkPreferences; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.standalone.config.BuildConfig; import org.opentripplanner.street.model.StreetLimitationParameters; import org.opentripplanner.transit.service.TimetableRepository; /** - * Configure all modules which is not simple enough to be injected. + * Configure all modules that are not simple enough to be injected. */ @Module public class GraphBuilderModules { @@ -60,7 +61,8 @@ static OsmModule provideOsmModule( GraphBuilderDataSources dataSources, BuildConfig config, Graph graph, - VehicleParkingRepository parkingService, + OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, + VehicleParkingRepository vehicleParkingRepository, DataImportIssueStore issueStore, StreetLimitationParameters streetLimitationParameters ) { @@ -71,6 +73,7 @@ static OsmModule provideOsmModule( osmConfiguredDataSource.dataSource(), osmConfiguredDataSource.config().osmTagMapper(), osmConfiguredDataSource.config().timeZone(), + osmConfiguredDataSource.config().includeOsmSubwayEntrances(), config.osmCacheDataInMem, issueStore ) @@ -78,7 +81,7 @@ static OsmModule provideOsmModule( } return OsmModule - .of(providers, graph, parkingService) + .of(providers, graph, osmInfoGraphBuildRepository, vehicleParkingRepository) .withEdgeNamer(config.edgeNamer) .withAreaVisibility(config.areaVisibility) .withPlatformEntriesLinking(config.platformEntriesLinking) @@ -86,6 +89,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..3b9f411fec6 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 @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; @@ -25,6 +26,8 @@ import org.opentripplanner.osm.wayproperty.WayProperties; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.util.ElevationUtils; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; +import org.opentripplanner.service.osminfo.model.Platform; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.vehicleparking.model.VehicleParking; import org.opentripplanner.street.model.StreetLimitationParameters; @@ -52,7 +55,9 @@ public class OsmModule implements GraphBuilderModule { */ private final List providers; private final Graph graph; + private final OsmInfoGraphBuildRepository osmInfoGraphBuildRepository; private final VehicleParkingRepository parkingRepository; + private final DataImportIssueStore issueStore; private final OsmProcessingParameters params; private final SafetyValueNormalizer normalizer; @@ -63,36 +68,51 @@ public class OsmModule implements GraphBuilderModule { OsmModule( Collection providers, Graph graph, - VehicleParkingRepository parkingService, + OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, + VehicleParkingRepository parkingRepository, DataImportIssueStore issueStore, StreetLimitationParameters streetLimitationParameters, OsmProcessingParameters params ) { this.providers = List.copyOf(providers); this.graph = graph; + this.osmInfoGraphBuildRepository = osmInfoGraphBuildRepository; + this.parkingRepository = parkingRepository; 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; } public static OsmModuleBuilder of( Collection providers, Graph graph, - VehicleParkingRepository service + OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, + VehicleParkingRepository vehicleParkingRepository ) { - return new OsmModuleBuilder(providers, graph, service); + return new OsmModuleBuilder( + providers, + graph, + osmInfoGraphBuildRepository, + vehicleParkingRepository + ); } public static OsmModuleBuilder of( OsmProvider provider, Graph graph, - VehicleParkingRepository service + OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, + VehicleParkingRepository vehicleParkingRepository ) { - return of(List.of(provider), graph, service); + return of(List.of(provider), graph, osmInfoGraphBuildRepository, vehicleParkingRepository); } @Override @@ -319,6 +339,8 @@ private void buildBasicGraph() { // where the current edge should start OsmNode osmStartNode = null; + var platform = getPlatform(way); + for (int i = 0; i < nodes.size() - 1; i++) { OsmNode segmentStartOsmNode = osmdb.getNode(nodes.get(i)); @@ -341,7 +363,7 @@ private void buildBasicGraph() { * We split segments at intersections, self-intersections, nodes with ele tags, and transit stops; * the only processing we do on other nodes is to accumulate their geometry */ - if (segmentCoordinates.size() == 0) { + if (segmentCoordinates.isEmpty()) { segmentCoordinates.add(osmStartNode.getCoordinate()); } @@ -408,6 +430,11 @@ private void buildBasicGraph() { StreetEdge backStreet = streets.back(); normalizer.applyWayProperties(street, backStreet, wayData, way); + platform.ifPresent(plat -> { + osmInfoGraphBuildRepository.addPlatform(street, plat); + osmInfoGraphBuildRepository.addPlatform(backStreet, plat); + }); + applyEdgesToTurnRestrictions(way, startNode, endNode, street, backStreet); startNode = endNode; osmStartNode = osmdb.getNode(startNode); @@ -422,6 +449,33 @@ private void buildBasicGraph() { LOG.info(progress.completeMessage()); } + private Optional getPlatform(OsmWay way) { + if (way.isBoardingLocation()) { + var nodeRefs = way.getNodeRefs(); + var size = nodeRefs.size(); + var nodes = new Coordinate[size]; + for (int i = 0; i < size; i++) { + nodes[i] = osmdb.getNode(nodeRefs.get(i)).getCoordinate(); + } + + var geometryFactory = GeometryUtils.getGeometryFactory(); + + var geometry = geometryFactory.createLineString(nodes); + + var references = way.getMultiTagValues(params.boardingAreaRefTags()); + + return Optional.of( + new Platform( + params.edgeNamer().getNameForWay(way, "platform " + way.getId()), + geometry, + references + ) + ); + } else { + return Optional.empty(); + } + } + private void validateBarriers() { List vertices = graph.getVerticesOfType(BarrierVertex.class); vertices.forEach(bv -> bv.makeBarrierAtEndReachable()); 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..ce50dbbde64 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 @@ -8,6 +8,7 @@ import org.opentripplanner.graph_builder.services.osm.EdgeNamer; import org.opentripplanner.osm.OsmProvider; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.street.model.StreetLimitationParameters; @@ -19,6 +20,7 @@ public class OsmModuleBuilder { private final Collection providers; private final Graph graph; private final VehicleParkingRepository parkingRepository; + private final OsmInfoGraphBuildRepository osmInfoGraphBuildRepository; private Set boardingAreaRefTags = Set.of(); private DataImportIssueStore issueStore = DataImportIssueStore.NOOP; private EdgeNamer edgeNamer = new DefaultNamer(); @@ -26,16 +28,19 @@ 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(); OsmModuleBuilder( Collection providers, Graph graph, + OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, VehicleParkingRepository parkingRepository ) { this.providers = providers; this.graph = graph; + this.osmInfoGraphBuildRepository = osmInfoGraphBuildRepository; this.parkingRepository = parkingRepository; } @@ -79,6 +84,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; @@ -88,6 +98,7 @@ public OsmModule build() { return new OsmModule( providers, graph, + osmInfoGraphBuildRepository, parkingRepository, issueStore, streetLimitationParameters, @@ -98,7 +109,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 91944a95b86..53d6acc87b9 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/RaptorPathToItineraryMapper.java b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java index 15e3307b4e9..683acdd1a9e 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 @@ -6,7 +6,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.Set; import org.opentripplanner.astar.model.GraphPath; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.geometry.GeometryUtils; @@ -39,8 +38,8 @@ import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.street.search.request.StreetSearchRequest; import org.opentripplanner.street.search.request.StreetSearchRequestMapper; -import org.opentripplanner.street.search.state.EdgeTraverser; import org.opentripplanner.street.search.state.State; +import org.opentripplanner.street.search.state.StateEditor; import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.service.TransitService; @@ -361,15 +360,24 @@ private List mapNonTransitLeg( .build() ); } else { - var legTransferSearchRequest = transferStreetRequest - .copyOf(createZonedDateTime(pathLeg.fromTime()).toInstant()) - .build(); - var initialStates = State.getInitialStates( - Set.of(edges.getFirst().getFromVertex()), - legTransferSearchRequest - ); - var state = EdgeTraverser.traverseEdges(initialStates, edges); - var graphPath = new GraphPath<>(state.get()); + StateEditor se = new StateEditor(edges.get(0).getFromVertex(), transferStreetRequest); + se.setTimeSeconds(createZonedDateTime(pathLeg.fromTime()).toEpochSecond()); + + State s = se.makeState(); + ArrayList transferStates = new ArrayList<>(); + transferStates.add(s); + for (Edge e : edges) { + var states = e.traverse(s); + if (State.isEmpty(states)) { + s = null; + } else { + transferStates.add(states[0]); + s = states[0]; + } + } + + State[] states = transferStates.toArray(new State[0]); + var graphPath = new GraphPath<>(states[states.length - 1]); Itinerary subItinerary = graphPathToItineraryMapper.generateItinerary(graphPath); 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/routing/algorithm/raptoradapter/transit/RaptorTransferIndex.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/RaptorTransferIndex.java index 8676e863911..fa44d793664 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/RaptorTransferIndex.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/RaptorTransferIndex.java @@ -5,12 +5,19 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Function; +import java.util.stream.IntStream; +import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.raptor.api.model.RaptorTransfer; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.street.search.request.StreetSearchRequest; public class RaptorTransferIndex { + private enum RequestSource { + SETUP, + REQUEST_SCOPE, + } + private final List[] forwardTransfers; private final List[] reversedTransfers; @@ -24,19 +31,47 @@ public RaptorTransferIndex( this.reversedTransfers = reversedTransfers.stream().map(List::copyOf).toArray(List[]::new); } - public static RaptorTransferIndex create( + /** + * Create an index for a route request configured in router-config.json + */ + public static RaptorTransferIndex createInitialSetup( + List> transfersByStopIndex, + StreetSearchRequest request + ) { + return create(transfersByStopIndex, request, RequestSource.SETUP); + } + + /** + * Create an index for a route request originated from the client + */ + public static RaptorTransferIndex createRequestScope( List> transfersByStopIndex, StreetSearchRequest request + ) { + return create(transfersByStopIndex, request, RequestSource.REQUEST_SCOPE); + } + + private static RaptorTransferIndex create( + List> transfersByStopIndex, + StreetSearchRequest request, + RequestSource requestSource ) { var forwardTransfers = new ArrayList>(transfersByStopIndex.size()); var reversedTransfers = new ArrayList>(transfersByStopIndex.size()); StreetMode mode = request.mode(); for (int i = 0; i < transfersByStopIndex.size(); i++) { + forwardTransfers.add(new ArrayList<>()); reversedTransfers.add(new ArrayList<>()); } - for (int fromStop = 0; fromStop < transfersByStopIndex.size(); fromStop++) { + var stopIndices = IntStream.range(0, transfersByStopIndex.size()); + // we want to always parallelize the cache building during the startup + // and only parallelize during runtime requests if the feature flag is on + if (requestSource == RequestSource.SETUP || OTPFeature.ParallelRouting.isOn()) { + stopIndices = stopIndices.parallel(); + } + stopIndices.forEach(fromStop -> { // The transfers are filtered so that there is only one possible directional transfer // for a stop pair. var transfers = transfersByStopIndex @@ -49,15 +84,18 @@ public static RaptorTransferIndex create( ) .values(); - forwardTransfers.add(new ArrayList<>(transfers)); + // forwardTransfers is not modified here, and no two threads will access the same element + // in it, so this is still thread safe. + forwardTransfers.get(fromStop).addAll(transfers); + }); - for (RaptorTransfer forwardTransfer : transfers) { + for (int fromStop = 0; fromStop < transfersByStopIndex.size(); fromStop++) { + for (var forwardTransfer : forwardTransfers.get(fromStop)) { reversedTransfers .get(forwardTransfer.stop()) .add(DefaultRaptorTransfer.reverseOf(fromStop, forwardTransfer)); } } - return new RaptorTransferIndex(forwardTransfers, reversedTransfers); } diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/Transfer.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/Transfer.java index 2643067398e..20a36376ae7 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/Transfer.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/Transfer.java @@ -15,7 +15,7 @@ import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.search.request.StreetSearchRequest; import org.opentripplanner.street.search.state.EdgeTraverser; -import org.opentripplanner.street.search.state.State; +import org.opentripplanner.street.search.state.StateEditor; import org.opentripplanner.utils.logging.Throttle; import org.opentripplanner.utils.tostring.ToStringBuilder; import org.slf4j.Logger; @@ -97,8 +97,10 @@ public Optional asRaptorTransfer(StreetSearchRequest request) { ); } - var initialStates = State.getInitialStates(Set.of(edges.getFirst().getFromVertex()), request); - var state = EdgeTraverser.traverseEdges(initialStates, edges); + StateEditor se = new StateEditor(edges.get(0).getFromVertex(), request); + se.setTimeSeconds(0); + + var state = EdgeTraverser.traverseEdges(se.makeState(), edges); return state.map(s -> new DefaultRaptorTransfer( diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRequestTransferCache.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRequestTransferCache.java index d778f491142..80814fdeee2 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRequestTransferCache.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRequestTransferCache.java @@ -36,7 +36,7 @@ public LoadingCache getTransferCache() { public void put(List> transfersByStopIndex, RouteRequest request) { final CacheKey cacheKey = new CacheKey(transfersByStopIndex, request); - final RaptorTransferIndex raptorTransferIndex = RaptorTransferIndex.create( + final RaptorTransferIndex raptorTransferIndex = RaptorTransferIndex.createInitialSetup( transfersByStopIndex, cacheKey.request ); @@ -58,7 +58,10 @@ private CacheLoader cacheLoader() { @Override public RaptorTransferIndex load(CacheKey cacheKey) { LOG.info("Adding runtime request to cache: {}", cacheKey.options); - return RaptorTransferIndex.create(cacheKey.transfersByStopIndex, cacheKey.request); + return RaptorTransferIndex.createRequestScope( + cacheKey.transfersByStopIndex, + cacheKey.request + ); } }; } 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 a152b96682d..8565952e557 100644 --- a/application/src/main/java/org/opentripplanner/routing/graph/SerializedGraphObject.java +++ b/application/src/main/java/org/opentripplanner/routing/graph/SerializedGraphObject.java @@ -25,6 +25,7 @@ import org.opentripplanner.model.projectinfo.GraphFileHeader; import org.opentripplanner.model.projectinfo.OtpProjectInfo; import org.opentripplanner.routing.graph.kryosupport.KryoBuilder; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.worldenvelope.WorldEnvelopeRepository; import org.opentripplanner.standalone.config.BuildConfig; @@ -56,6 +57,10 @@ public class SerializedGraphObject implements Serializable { private static final Logger LOG = LoggerFactory.getLogger(SerializedGraphObject.class); public final Graph graph; + + @Nullable + public final OsmInfoGraphBuildRepository osmInfoGraphBuildRepository; + public final TimetableRepository timetableRepository; public final WorldEnvelopeRepository worldEnvelopeRepository; private final Collection edges; @@ -84,6 +89,7 @@ public class SerializedGraphObject implements Serializable { public SerializedGraphObject( Graph graph, + @Nullable OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, TimetableRepository timetableRepository, WorldEnvelopeRepository worldEnvelopeRepository, VehicleParkingRepository parkingRepository, @@ -96,6 +102,7 @@ public SerializedGraphObject( ) { this.graph = graph; this.edges = graph.getEdges(); + this.osmInfoGraphBuildRepository = osmInfoGraphBuildRepository; this.timetableRepository = timetableRepository; this.worldEnvelopeRepository = worldEnvelopeRepository; this.parkingRepository = parkingRepository; diff --git a/application/src/main/java/org/opentripplanner/routing/linking/VertexLinker.java b/application/src/main/java/org/opentripplanner/routing/linking/VertexLinker.java index 48f5ff997c8..2f09d618ffd 100644 --- a/application/src/main/java/org/opentripplanner/routing/linking/VertexLinker.java +++ b/application/src/main/java/org/opentripplanner/routing/linking/VertexLinker.java @@ -7,6 +7,7 @@ import java.util.Set; import java.util.function.BiFunction; import java.util.stream.Collectors; +import javax.annotation.Nullable; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; @@ -226,20 +227,43 @@ private DisposableEdgeCollection link( return tempEdges; } + /** + * Link a boarding location vertex to specific street edges. + *

+ * This is used if a platform is mapped as a linear way, where the given edges form the platform. + */ + public Set linkToSpecificStreetEdgesPermanently( + Vertex vertex, + TraverseModeSet traverseModes, + LinkingDirection direction, + Set edges + ) { + var xscale = getXscale(vertex); + return linkToCandidateEdges( + vertex, + traverseModes, + direction, + Scope.PERMANENT, + null, + edges.stream().map(e -> new DistanceTo<>(e, distance(vertex, e, xscale))).toList(), + xscale + ); + } + private Set linkToStreetEdges( Vertex vertex, TraverseModeSet traverseModes, LinkingDirection direction, Scope scope, int radiusMeters, - DisposableEdgeCollection tempEdges + @Nullable DisposableEdgeCollection tempEdges ) { final double radiusDeg = SphericalDistanceLibrary.metersToDegrees(radiusMeters); Envelope env = new Envelope(vertex.getCoordinate()); // Perform a simple local equirectangular projection, so distances are expressed in degrees latitude. - final double xscale = Math.cos(vertex.getLat() * Math.PI / 180); + final double xscale = getXscale(vertex); // Expand more in the longitude direction than the latitude direction to account for converging meridians. env.expandBy(radiusDeg / xscale, radiusDeg); @@ -257,6 +281,30 @@ private Set linkToStreetEdges( .filter(ead -> ead.distanceDegreesLat < radiusDeg) .toList(); + return linkToCandidateEdges( + vertex, + traverseModes, + direction, + scope, + tempEdges, + candidateEdges, + xscale + ); + } + + private static double getXscale(Vertex vertex) { + return Math.cos(vertex.getLat() * Math.PI / 180); + } + + private Set linkToCandidateEdges( + Vertex vertex, + TraverseModeSet traverseModes, + LinkingDirection direction, + Scope scope, + @Nullable DisposableEdgeCollection tempEdges, + List> candidateEdges, + double xscale + ) { if (candidateEdges.isEmpty()) { return Set.of(); } @@ -269,7 +317,7 @@ private Set linkToStreetEdges( return closestEdges .stream() .map(ce -> link(vertex, ce.item, xscale, scope, direction, tempEdges, linkedAreas)) - .filter(v -> v != null) + .filter(Objects::nonNull) .collect(Collectors.toSet()); } diff --git a/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildRepository.java b/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildRepository.java new file mode 100644 index 00000000000..ac8f7276072 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildRepository.java @@ -0,0 +1,23 @@ +package org.opentripplanner.service.osminfo; + +import java.io.Serializable; +import java.util.Optional; +import org.opentripplanner.service.osminfo.model.Platform; +import org.opentripplanner.street.model.edge.Edge; + +/** + * Store OSM data used during graph build, but discard it after it is complete. + *

+ * This is a repository to support the {@link OsmInfoGraphBuildService}. + */ +public interface OsmInfoGraphBuildRepository extends Serializable { + /** + * Associate the edge with a platform + */ + void addPlatform(Edge edge, Platform platform); + + /** + * Find the platform the edge belongs to + */ + Optional findPlatform(Edge edge); +} diff --git a/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildService.java b/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildService.java new file mode 100644 index 00000000000..6a50c3c92be --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildService.java @@ -0,0 +1,25 @@ +package org.opentripplanner.service.osminfo; + +import java.util.Optional; +import org.opentripplanner.service.osminfo.model.Platform; +import org.opentripplanner.street.model.edge.Edge; + +/** + * The responsibility of this service is to provide information from Open Street Map, which + * is NOT in the OTP street graph. The graph build happens in phases, and some data is read in + * from the OSM files, but needed later on. For example, we might need info from OSM to link street + * edges/vertexes with transit stops/platforms. We do not want to put data in the OTP street graph + * unless it is relevant for routing. So, for information that is read by the OsmGraphBuilder, but + * needed later on, we have this service. + * + * THIS SERVICE IS ONLY AVAILABLE DURING GRAPH BUILD, NOT DURING ROUTING. * + */ +public interface OsmInfoGraphBuildService { + /** + * Find the platform the given edge is part of. + *

+ * TODO: This service currently only stores linear platforms, but area platforms and + * node platforms should be supported as well. + */ + Optional findPlatform(Edge edge); +} diff --git a/application/src/main/java/org/opentripplanner/service/osminfo/configure/OsmInfoGraphBuildRepositoryModule.java b/application/src/main/java/org/opentripplanner/service/osminfo/configure/OsmInfoGraphBuildRepositoryModule.java new file mode 100644 index 00000000000..d8b9db5608e --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/osminfo/configure/OsmInfoGraphBuildRepositoryModule.java @@ -0,0 +1,12 @@ +package org.opentripplanner.service.osminfo.configure; + +import dagger.Binds; +import dagger.Module; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; +import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildRepository; + +@Module +public interface OsmInfoGraphBuildRepositoryModule { + @Binds + OsmInfoGraphBuildRepository bind(DefaultOsmInfoGraphBuildRepository repository); +} diff --git a/application/src/main/java/org/opentripplanner/service/osminfo/configure/OsmInfoGraphBuildServiceModule.java b/application/src/main/java/org/opentripplanner/service/osminfo/configure/OsmInfoGraphBuildServiceModule.java new file mode 100644 index 00000000000..c6ac5c31ec9 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/osminfo/configure/OsmInfoGraphBuildServiceModule.java @@ -0,0 +1,12 @@ +package org.opentripplanner.service.osminfo.configure; + +import dagger.Binds; +import dagger.Module; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildService; +import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildService; + +@Module +public interface OsmInfoGraphBuildServiceModule { + @Binds + OsmInfoGraphBuildService bind(DefaultOsmInfoGraphBuildService service); +} diff --git a/application/src/main/java/org/opentripplanner/service/osminfo/internal/DefaultOsmInfoGraphBuildRepository.java b/application/src/main/java/org/opentripplanner/service/osminfo/internal/DefaultOsmInfoGraphBuildRepository.java new file mode 100644 index 00000000000..6505fdd67a4 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/osminfo/internal/DefaultOsmInfoGraphBuildRepository.java @@ -0,0 +1,37 @@ +package org.opentripplanner.service.osminfo.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.osminfo.OsmInfoGraphBuildRepository; +import org.opentripplanner.service.osminfo.model.Platform; +import org.opentripplanner.street.model.edge.Edge; + +public class DefaultOsmInfoGraphBuildRepository + implements OsmInfoGraphBuildRepository, Serializable { + + private final Map platforms = new HashMap<>(); + + @Inject + public DefaultOsmInfoGraphBuildRepository() {} + + @Override + public void addPlatform(Edge edge, Platform platform) { + Objects.requireNonNull(edge); + Objects.requireNonNull(platform); + this.platforms.put(edge, platform); + } + + @Override + public Optional findPlatform(Edge edge) { + return Optional.ofNullable(platforms.get(edge)); + } + + @Override + public String toString() { + return "DefaultOsmInfoGraphBuildRepository{platforms size = " + platforms.size() + "}"; + } +} diff --git a/application/src/main/java/org/opentripplanner/service/osminfo/internal/DefaultOsmInfoGraphBuildService.java b/application/src/main/java/org/opentripplanner/service/osminfo/internal/DefaultOsmInfoGraphBuildService.java new file mode 100644 index 00000000000..42eb5bf364f --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/osminfo/internal/DefaultOsmInfoGraphBuildService.java @@ -0,0 +1,28 @@ +package org.opentripplanner.service.osminfo.internal; + +import jakarta.inject.Inject; +import java.util.Optional; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildService; +import org.opentripplanner.service.osminfo.model.Platform; +import org.opentripplanner.street.model.edge.Edge; + +public class DefaultOsmInfoGraphBuildService implements OsmInfoGraphBuildService { + + private final OsmInfoGraphBuildRepository repository; + + @Inject + public DefaultOsmInfoGraphBuildService(OsmInfoGraphBuildRepository repository) { + this.repository = repository; + } + + @Override + public Optional findPlatform(Edge edge) { + return repository.findPlatform(edge); + } + + @Override + public String toString() { + return "DefaultOsmInfoGraphBuildService{ repository=" + repository + '}'; + } +} diff --git a/application/src/main/java/org/opentripplanner/service/osminfo/model/Platform.java b/application/src/main/java/org/opentripplanner/service/osminfo/model/Platform.java new file mode 100644 index 00000000000..91d78385a34 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/osminfo/model/Platform.java @@ -0,0 +1,7 @@ +package org.opentripplanner.service.osminfo.model; + +import java.util.Set; +import org.locationtech.jts.geom.LineString; +import org.opentripplanner.framework.i18n.I18NString; + +public record Platform(I18NString name, LineString geometry, Set references) {} diff --git a/application/src/main/java/org/opentripplanner/standalone/OTPMain.java b/application/src/main/java/org/opentripplanner/standalone/OTPMain.java index ade5067a981..25eea6df473 100644 --- a/application/src/main/java/org/opentripplanner/standalone/OTPMain.java +++ b/application/src/main/java/org/opentripplanner/standalone/OTPMain.java @@ -150,6 +150,7 @@ private static void startOTPServer(CommandLineParameters cli) { // with using the embedded router config. new SerializedGraphObject( app.graph(), + app.osmInfoGraphBuildRepository(), app.timetableRepository(), app.worldEnvelopeRepository(), app.vehicleParkingRepository(), 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/standalone/config/framework/project/EnvironmentVariableReplacer.java b/application/src/main/java/org/opentripplanner/standalone/config/framework/project/EnvironmentVariableReplacer.java index 17910fa62ca..c71c1237d3f 100644 --- a/application/src/main/java/org/opentripplanner/standalone/config/framework/project/EnvironmentVariableReplacer.java +++ b/application/src/main/java/org/opentripplanner/standalone/config/framework/project/EnvironmentVariableReplacer.java @@ -3,12 +3,12 @@ import static java.util.Map.entry; import static org.opentripplanner.model.projectinfo.OtpProjectInfo.projectInfo; -import java.util.HashMap; import java.util.Map; import java.util.function.Function; -import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.annotation.Nullable; import org.opentripplanner.framework.application.OtpAppException; +import org.opentripplanner.utils.text.TextVariablesSubstitution; /** * Replaces environment variable placeholders specified on the format ${variable} in a text with the @@ -58,46 +58,47 @@ public class EnvironmentVariableReplacer { * Search for {@link #PATTERN}s and replace each placeholder with the value of the corresponding * environment variable. * - * @param source is used only to generate human friendly error message in case the text contain a - * placeholder which can not be found. - * @throws IllegalArgumentException if a placeholder exist in the {@code text}, but the - * environment variable do not exist. + * @param source is used only to generate a human friendly error message in case the text + * contains a placeholder which cannot be found. + * @throws IllegalArgumentException if a placeholder exists in the {@code text}, but the + * environment variable does not exist. */ public static String insertEnvironmentVariables(String text, String source) { - return insertVariables(text, source, System::getenv); + return insertVariables(text, source, EnvironmentVariableReplacer::getEnvVarOrProjectInfo); } + /** + * Same as {@link #insertEnvironmentVariables(String, String)}, but the caller mus provide the + * {@code variableResolver} - environment and project info variables are not available. + */ public static String insertVariables( String text, String source, - Function getEnvVar + Function variableResolver ) { - Map substitutions = new HashMap<>(); - Matcher matcher = PATTERN.matcher(text); + return TextVariablesSubstitution.insertVariables( + text, + variableResolver, + varName -> errorVariableNameNotFound(varName, source) + ); + } - while (matcher.find()) { - String subKey = matcher.group(0); - String nameOnly = matcher.group(1); - if (!substitutions.containsKey(nameOnly)) { - String value = getEnvVar.apply(nameOnly); - if (value != null) { - substitutions.put(subKey, value); - } else if (PROJECT_INFO.containsKey(nameOnly)) { - substitutions.put(subKey, PROJECT_INFO.get(nameOnly)); - } else { - throw new OtpAppException( - "Environment variable name '" + - nameOnly + - "' in config '" + - source + - "' not found in the system environment variables." - ); - } - } + @Nullable + private static String getEnvVarOrProjectInfo(String key) { + String value = System.getenv(key); + if (value == null) { + return PROJECT_INFO.get(key); } - for (Map.Entry entry : substitutions.entrySet()) { - text = text.replace(entry.getKey(), entry.getValue()); - } - return text; + return value; + } + + private static void errorVariableNameNotFound(String variableName, String source) { + throw new OtpAppException( + "Environment variable name '" + + variableName + + "' in config '" + + source + + "' not found in the system environment variables." + ); } } diff --git a/application/src/main/java/org/opentripplanner/standalone/config/routerconfig/ServerConfig.java b/application/src/main/java/org/opentripplanner/standalone/config/routerconfig/ServerConfig.java index a75300f62a2..64921eb813b 100644 --- a/application/src/main/java/org/opentripplanner/standalone/config/routerconfig/ServerConfig.java +++ b/application/src/main/java/org/opentripplanner/standalone/config/routerconfig/ServerConfig.java @@ -1,9 +1,12 @@ package org.opentripplanner.standalone.config.routerconfig; +import static org.opentripplanner.standalone.config.framework.json.EnumMapper.docEnumValueList; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_4; +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_7; import java.time.Duration; import java.util.List; +import org.opentripplanner.apis.support.graphql.injectdoc.ApiDocumentationProfile; import org.opentripplanner.framework.application.OtpAppException; import org.opentripplanner.standalone.config.framework.json.NodeAdapter; import org.opentripplanner.standalone.server.OTPWebApplicationParameters; @@ -13,6 +16,7 @@ public class ServerConfig implements OTPWebApplicationParameters { private final Duration apiProcessingTimeout; private final List traceParameters; + private final ApiDocumentationProfile apiDocumentationProfile; public ServerConfig(String parameterName, NodeAdapter root) { NodeAdapter c = root @@ -42,6 +46,14 @@ public ServerConfig(String parameterName, NodeAdapter root) { ) .asDuration(Duration.ofSeconds(-1)); + this.apiDocumentationProfile = + c + .of("apiDocumentationProfile") + .since(V2_7) + .summary(ApiDocumentationProfile.DEFAULT.typeDescription()) + .description(docEnumValueList(ApiDocumentationProfile.values())) + .asEnum(ApiDocumentationProfile.DEFAULT); + this.traceParameters = c .of("traceParameters") @@ -105,6 +117,15 @@ public Duration apiProcessingTimeout() { return apiProcessingTimeout; } + @Override + public List traceParameters() { + return traceParameters; + } + + public ApiDocumentationProfile apiDocumentationProfile() { + return apiDocumentationProfile; + } + public void validate(Duration streetRoutingTimeout) { if ( !apiProcessingTimeout.isNegative() && @@ -119,9 +140,4 @@ public void validate(Duration streetRoutingTimeout) { ); } } - - @Override - public List traceParameters() { - return traceParameters; - } } 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 b4edbb36299..eeaaf6427cb 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java @@ -18,6 +18,7 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerMapper; import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingService; @@ -64,6 +65,11 @@ public class ConstructApplication { private final CommandLineParameters cli; private final GraphBuilderDataSources graphBuilderDataSources; + /** + * The OSM Info is injected into the graph-builder, but not the web-server; Hence not part of + * the application context. + */ + private final OsmInfoGraphBuildRepository osmInfoGraphBuildRepository; private final ConstructApplicationFactory factory; /** @@ -72,6 +78,7 @@ public class ConstructApplication { ConstructApplication( CommandLineParameters cli, Graph graph, + OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, TimetableRepository timetableRepository, WorldEnvelopeRepository worldEnvelopeRepository, ConfigModel config, @@ -84,6 +91,7 @@ public class ConstructApplication { ) { this.cli = cli; this.graphBuilderDataSources = graphBuilderDataSources; + this.osmInfoGraphBuildRepository = osmInfoGraphBuildRepository; // We create the optional GraphVisualizer here, because it would be significant more complex to // use Dagger DI to do it - passing in a parameter to enable it or not. @@ -130,7 +138,8 @@ public GraphBuilder createGraphBuilder() { buildConfig(), graphBuilderDataSources, graph(), - timetableRepository(), + osmInfoGraphBuildRepository, + factory.timetableRepository(), factory.worldEnvelopeRepository(), factory.vehicleParkingRepository(), factory.emissionsDataModel(), @@ -183,6 +192,7 @@ private void setupTransitRoutingServer() { routerConfig().transmodelApi(), timetableRepository(), routerConfig().routingRequestDefaults(), + routerConfig().server().apiDocumentationProfile(), routerConfig().transitTuningConfig() ); } @@ -261,6 +271,10 @@ public DataImportIssueSummary dataImportIssueSummary() { return factory.dataImportIssueSummary(); } + public OsmInfoGraphBuildRepository osmInfoGraphBuildRepository() { + return osmInfoGraphBuildRepository; + } + public StopConsolidationRepository stopConsolidationRepository() { return factory.stopConsolidationRepository(); } 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 d6310c0c616..3d479f0fa63 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java @@ -51,21 +51,21 @@ @Component( modules = { ConfigModule.class, - TransitModule.class, - WorldEnvelopeServiceModule.class, + ConstructApplicationModule.class, + EmissionsServiceModule.class, + GeocoderModule.class, + InteractiveLauncherModule.class, RealtimeVehicleServiceModule.class, RealtimeVehicleRepositoryModule.class, - VehicleRentalServiceModule.class, - VehicleRentalRepositoryModule.class, - VehicleParkingServiceModule.class, - ConstructApplicationModule.class, RideHailingServicesModule.class, - EmissionsServiceModule.class, + TransitModule.class, + VehicleParkingServiceModule.class, + VehicleRentalRepositoryModule.class, + VehicleRentalServiceModule.class, SorlandsbanenNorwayModule.class, StopConsolidationServiceModule.class, - InteractiveLauncherModule.class, StreetLimitationParametersServiceModule.class, - GeocoderModule.class, + WorldEnvelopeServiceModule.class, } ) public interface ConstructApplicationFactory { 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 021af778345..ad1a7293855 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/LoadApplication.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/LoadApplication.java @@ -8,6 +8,7 @@ import org.opentripplanner.graph_builder.issue.api.DataImportIssueSummary; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.SerializedGraphObject; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.worldenvelope.WorldEnvelopeRepository; import org.opentripplanner.standalone.config.CommandLineParameters; @@ -20,7 +21,7 @@ * This is used to load the graph, and finally this class can create the * {@link ConstructApplication} for the next phase. *

- * By splitting the these two responsibilities into two separate phases we are sure all + * By splitting these two responsibilities into two separate phases we are sure all * components (graph and transit model) created in the load phase will be available for * creating the application using Dagger dependency injection. */ @@ -55,6 +56,7 @@ public DataSource getInputGraphDataStore() { public ConstructApplication appConstruction(SerializedGraphObject obj) { return createAppConstruction( obj.graph, + obj.osmInfoGraphBuildRepository, obj.timetableRepository, obj.worldEnvelopeRepository, obj.parkingRepository, @@ -69,6 +71,7 @@ public ConstructApplication appConstruction(SerializedGraphObject obj) { public ConstructApplication appConstruction() { return createAppConstruction( factory.emptyGraph(), + factory.emptyOsmInfoGraphBuildRepository(), factory.emptyTimetableRepository(), factory.emptyWorldEnvelopeRepository(), factory.emptyVehicleParkingRepository(), @@ -92,6 +95,7 @@ public ConfigModel config() { private ConstructApplication createAppConstruction( Graph graph, + OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, TimetableRepository timetableRepository, WorldEnvelopeRepository worldEnvelopeRepository, VehicleParkingRepository parkingRepository, @@ -103,6 +107,7 @@ private ConstructApplication createAppConstruction( return new ConstructApplication( cli, graph, + osmInfoGraphBuildRepository, 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 b054fac3ca5..9fdbf59bfda 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/LoadApplicationFactory.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/LoadApplicationFactory.java @@ -11,6 +11,8 @@ import org.opentripplanner.ext.stopconsolidation.configure.StopConsolidationRepositoryModule; import org.opentripplanner.graph_builder.GraphBuilderDataSources; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; +import org.opentripplanner.service.osminfo.configure.OsmInfoGraphBuildRepositoryModule; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.vehicleparking.configure.VehicleParkingRepositoryModule; import org.opentripplanner.service.worldenvelope.WorldEnvelopeRepository; @@ -30,6 +32,7 @@ LoadConfigModule.class, DataStoreModule.class, GsDataSourceModule.class, + OsmInfoGraphBuildRepositoryModule.class, WorldEnvelopeRepositoryModule.class, StopConsolidationRepositoryModule.class, VehicleParkingRepositoryModule.class, @@ -43,6 +46,9 @@ public interface LoadApplicationFactory { @Singleton Graph emptyGraph(); + @Singleton + OsmInfoGraphBuildRepository emptyOsmInfoGraphBuildRepository(); + @Singleton TimetableRepository emptyTimetableRepository(); diff --git a/application/src/main/java/org/opentripplanner/street/model/edge/BoardingLocationToStopLink.java b/application/src/main/java/org/opentripplanner/street/model/edge/BoardingLocationToStopLink.java index 2b306b63cf3..235ec7c6be5 100644 --- a/application/src/main/java/org/opentripplanner/street/model/edge/BoardingLocationToStopLink.java +++ b/application/src/main/java/org/opentripplanner/street/model/edge/BoardingLocationToStopLink.java @@ -3,7 +3,7 @@ import java.util.List; import org.locationtech.jts.geom.LineString; import org.opentripplanner.framework.geometry.GeometryUtils; -import org.opentripplanner.street.model.vertex.OsmBoardingLocationVertex; +import org.opentripplanner.street.model.vertex.StreetVertex; import org.opentripplanner.street.model.vertex.TransitStopVertex; /** @@ -12,16 +12,16 @@ */ public class BoardingLocationToStopLink extends StreetTransitEntityLink { - private BoardingLocationToStopLink(OsmBoardingLocationVertex fromv, TransitStopVertex tov) { + private BoardingLocationToStopLink(StreetVertex fromv, TransitStopVertex tov) { super(fromv, tov, tov.getWheelchairAccessibility()); } - private BoardingLocationToStopLink(TransitStopVertex fromv, OsmBoardingLocationVertex tov) { + private BoardingLocationToStopLink(TransitStopVertex fromv, StreetVertex tov) { super(fromv, tov, fromv.getWheelchairAccessibility()); } public static BoardingLocationToStopLink createBoardingLocationToStopLink( - OsmBoardingLocationVertex fromv, + StreetVertex fromv, TransitStopVertex tov ) { return connectToGraph(new BoardingLocationToStopLink(fromv, tov)); @@ -29,7 +29,7 @@ public static BoardingLocationToStopLink createBoardingLocationToStopLink( public static BoardingLocationToStopLink createBoardingLocationToStopLink( TransitStopVertex fromv, - OsmBoardingLocationVertex tov + StreetVertex tov ) { return connectToGraph(new BoardingLocationToStopLink(fromv, tov)); } 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/street/search/request/StreetSearchRequest.java b/application/src/main/java/org/opentripplanner/street/search/request/StreetSearchRequest.java index df8933cd22d..c93ea598256 100644 --- a/application/src/main/java/org/opentripplanner/street/search/request/StreetSearchRequest.java +++ b/application/src/main/java/org/opentripplanner/street/search/request/StreetSearchRequest.java @@ -124,10 +124,6 @@ public DataOverlayContext dataOverlayContext() { return dataOverlayContext; } - public StreetSearchRequestBuilder copyOf(Instant time) { - return copyOf(this).withStartTime(time); - } - public StreetSearchRequestBuilder copyOfReversed(Instant time) { return copyOf(this).withStartTime(time).withArriveBy(!arriveBy); } diff --git a/application/src/main/java/org/opentripplanner/street/search/state/EdgeTraverser.java b/application/src/main/java/org/opentripplanner/street/search/state/EdgeTraverser.java index 502d014e358..8755f014e14 100644 --- a/application/src/main/java/org/opentripplanner/street/search/state/EdgeTraverser.java +++ b/application/src/main/java/org/opentripplanner/street/search/state/EdgeTraverser.java @@ -2,10 +2,7 @@ import java.util.Collection; import java.util.Optional; -import org.opentripplanner.astar.model.ShortestPathTree; import org.opentripplanner.street.model.edge.Edge; -import org.opentripplanner.street.model.vertex.Vertex; -import org.opentripplanner.street.search.strategy.DominanceFunctions; /** * This is a very reduced version of the A* algorithm: from an initial state a number of edges are @@ -17,49 +14,24 @@ */ public class EdgeTraverser { - public static Optional traverseEdges( - final Collection initialStates, - final Collection edges - ) { - return traverseEdges(initialStates.toArray(new State[0]), edges); - } - - public static Optional traverseEdges( - final State[] initialStates, - final Collection edges - ) { - if (edges.isEmpty()) { - return Optional.of(initialStates[0]); - } - - // The shortest path tree is used to prune dominated parallel states. For example, - // CAR_PICKUP can return both a CAR/WALK state after each traversal of which only - // the optimal states need to be continued. - var dominanceFunction = new DominanceFunctions.MinimumWeight(); - var spt = new ShortestPathTree<>(dominanceFunction); - for (State initialState : initialStates) { - spt.add(initialState); - } - - Vertex lastVertex = null; - var isArriveBy = initialStates[0].getRequest().arriveBy(); + public static Optional traverseEdges(final State s, final Collection edges) { + var state = s; for (Edge e : edges) { - var vertex = isArriveBy ? e.getToVertex() : e.getFromVertex(); - var fromStates = spt.getStates(vertex); - if (fromStates == null || fromStates.isEmpty()) { - return Optional.empty(); + var afterTraversal = e.traverse(state); + if (afterTraversal.length > 1) { + throw new IllegalStateException( + "Expected only a single state returned from edge %s but received %s".formatted( + e, + afterTraversal.length + ) + ); } - - for (State fromState : fromStates) { - var newToStates = e.traverse(fromState); - for (State newToState : newToStates) { - spt.add(newToState); - } + if (State.isEmpty(afterTraversal)) { + return Optional.empty(); + } else { + state = afterTraversal[0]; } - - lastVertex = isArriveBy ? e.getFromVertex() : e.getToVertex(); } - - return Optional.ofNullable(lastVertex).map(spt::getState); + return Optional.ofNullable(state); } } 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/ConstantsForTests.java b/application/src/test/java/org/opentripplanner/ConstantsForTests.java index e5ab48cee54..3f188ff2c89 100644 --- a/application/src/test/java/org/opentripplanner/ConstantsForTests.java +++ b/application/src/test/java/org/opentripplanner/ConstantsForTests.java @@ -34,6 +34,7 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.linking.LinkingDirection; import org.opentripplanner.routing.linking.VertexLinker; +import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; @@ -137,9 +138,11 @@ public static TestOtpModel buildNewPortlandGraph(boolean withElevation) { var timetableRepository = new TimetableRepository(new SiteRepository(), deduplicator); // Add street data from OSM { - OsmProvider osmProvider = new OsmProvider(PORTLAND_CENTRAL_OSM, false); - OsmModule osmModule = OsmModule - .of(osmProvider, graph, new DefaultVehicleParkingRepository()) + var osmProvider = new OsmProvider(PORTLAND_CENTRAL_OSM, false); + var osmInfoRepository = new DefaultOsmInfoGraphBuildRepository(); + var vehicleParkingRepository = new DefaultVehicleParkingRepository(); + var osmModule = OsmModule + .of(osmProvider, graph, osmInfoRepository, vehicleParkingRepository) .withStaticParkAndRide(true) .withStaticBikeParkAndRide(true) .build(); @@ -195,9 +198,11 @@ public static TestOtpModel buildOsmGraph(File osmFile) { var graph = new Graph(deduplicator); var timetableRepository = new TimetableRepository(siteRepository, deduplicator); // Add street data from OSM - OsmProvider osmProvider = new OsmProvider(osmFile, true); - OsmModule osmModule = OsmModule - .of(osmProvider, graph, new DefaultVehicleParkingRepository()) + var osmProvider = new OsmProvider(osmFile, true); + var osmInfoRepository = new DefaultOsmInfoGraphBuildRepository(); + var vehicleParkingRepository = new DefaultVehicleParkingRepository(); + var osmModule = OsmModule + .of(osmProvider, graph, osmInfoRepository, vehicleParkingRepository) .build(); osmModule.buildGraph(); return new TestOtpModel(graph, timetableRepository); @@ -245,8 +250,9 @@ public static TestOtpModel buildNewMinimalNetexGraph() { var timetableRepository = new TimetableRepository(siteRepository, deduplicator); // Add street data from OSM { - OsmProvider osmProvider = new OsmProvider(OSLO_EAST_OSM, false); - OsmModule osmModule = OsmModule.of(osmProvider, graph, parkingService).build(); + var osmProvider = new OsmProvider(OSLO_EAST_OSM, false); + var osmInfoRepository = new DefaultOsmInfoGraphBuildRepository(); + var osmModule = OsmModule.of(osmProvider, graph, osmInfoRepository, parkingService).build(); osmModule.buildGraph(); } // Add transit data from Netex diff --git a/application/src/test/java/org/opentripplanner/_support/geometry/Coordinates.java b/application/src/test/java/org/opentripplanner/_support/geometry/Coordinates.java index 33569a34b2e..5a4526012c9 100644 --- a/application/src/test/java/org/opentripplanner/_support/geometry/Coordinates.java +++ b/application/src/test/java/org/opentripplanner/_support/geometry/Coordinates.java @@ -6,8 +6,6 @@ public class Coordinates { public static final Coordinate BERLIN = of(52.5212, 13.4105); public static final Coordinate BERLIN_BRANDENBURG_GATE = of(52.51627, 13.37770); - public static final Coordinate BERLIN_FERNSEHTURM = of(52.52084, 13.40934); - public static final Coordinate BERLIN_ADMIRALBRUCKE = of(52.49526, 13.415093); public static final Coordinate HAMBURG = of(53.5566, 10.0003); public static final Coordinate KONGSBERG_PLATFORM_1 = of(59.67216, 9.65107); public static final Coordinate BOSTON = of(42.36541, -71.06129); diff --git a/application/src/test/java/org/opentripplanner/_support/text/TextAssertions.java b/application/src/test/java/org/opentripplanner/_support/text/TextAssertions.java new file mode 100644 index 00000000000..a009b76237c --- /dev/null +++ b/application/src/test/java/org/opentripplanner/_support/text/TextAssertions.java @@ -0,0 +1,64 @@ +package org.opentripplanner._support.text; + +import org.junit.jupiter.api.Assertions; + +/** + * This class contains test assert methods not supported by the standard JUnit + * framework. + */ +public final class TextAssertions { + + private static final String LINE_DELIMITERS = "(\n|\r|\r\n)"; + private static final int END_OF_TEXT = -111; + + /** + + * Assert to texts are equals line by line. Empty lines and white-space in the start and end of + * a line is ignored. + */ + public static void assertLinesEquals(String expected, String actual) { + var expLines = expected.split(LINE_DELIMITERS); + var actLines = actual.split(LINE_DELIMITERS); + + int i = -1; + int j = -1; + + while (true) { + i = next(expLines, i); + j = next(actLines, j); + + if (i == END_OF_TEXT && j == END_OF_TEXT) { + return; + } + + var exp = getLine(expLines, i); + var act = getLine(actLines, j); + + if (i == END_OF_TEXT || j == END_OF_TEXT || !exp.equals(act)) { + Assertions.fail( + "Expected%s: <%s>%n".formatted(lineText(i), exp) + + "Actual %s: <%s>%n".formatted(lineText(j), act) + ); + } + } + } + + private static String lineText(int index) { + return index < 0 ? "(@end-of-text)" : "(@line %d)".formatted(index); + } + + private static String getLine(String[] lines, int i) { + return i == END_OF_TEXT ? "" : lines[i].trim(); + } + + private static int next(String[] lines, int index) { + ++index; + while (index < lines.length) { + if (!lines[index].isBlank()) { + return index; + } + ++index; + } + return END_OF_TEXT; + } +} diff --git a/application/src/test/java/org/opentripplanner/_support/text/TextAssertionsTest.java b/application/src/test/java/org/opentripplanner/_support/text/TextAssertionsTest.java new file mode 100644 index 00000000000..739b7b59c4b --- /dev/null +++ b/application/src/test/java/org/opentripplanner/_support/text/TextAssertionsTest.java @@ -0,0 +1,49 @@ +package org.opentripplanner._support.text; + +import static org.opentripplanner._support.text.TextAssertions.assertLinesEquals; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TextAssertionsTest { + + @Test + void testIgnoreWhiteSpace() { + // Empty text + assertLinesEquals("", "\n\n"); + + // Text with white-space inserted + assertLinesEquals( + """ + A Test + Line 2 + DOS\r\n + line-shift + """, + """ + + A Test \t + \t + + \tLine 2 + DOS\rline-shift + """ + ); + } + + @Test + void testEndOfText() { + var ex = Assertions.assertThrows( + org.opentest4j.AssertionFailedError.class, + () -> assertLinesEquals("A\n", "A\nExtra Line") + ); + Assertions.assertTrue( + ex.getMessage().contains("Expected(@end-of-text)"), + "<" + ex.getMessage() + "> does not contain expected line." + ); + Assertions.assertTrue( + ex.getMessage().contains("Actual (@line 1): "), + "<" + ex.getMessage() + "> does not contain actual line." + ); + } +} 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/support/graphql/injectdoc/CustomDocumentationTest.java b/application/src/test/java/org/opentripplanner/apis/support/graphql/injectdoc/CustomDocumentationTest.java new file mode 100644 index 00000000000..dc9356530b6 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/apis/support/graphql/injectdoc/CustomDocumentationTest.java @@ -0,0 +1,76 @@ +package org.opentripplanner.apis.support.graphql.injectdoc; + +import static java.util.Optional.empty; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Test; + +class CustomDocumentationTest { + + private static final String ORIGINAL_DOC = "Original"; + + // We use a HashMap to allow inserting 'null' values + private static final Map PROPERTIES = new HashMap<>(Map.ofEntries()); + + static { + PROPERTIES.put("Type1.description", "Doc 1"); + PROPERTIES.put("Type2.description.append", "Doc 2"); + PROPERTIES.put("Type3.description", null); + PROPERTIES.put("Type.field1.description", "Doc f1"); + PROPERTIES.put("Type.field2.deprecated", "Deprecated f2"); + PROPERTIES.put("Type.field3.description.append", "Doc f3"); + PROPERTIES.put("Type.field4.deprecated.append", "Deprecated f4"); + PROPERTIES.put("Type.field5.description", null); + } + + private final CustomDocumentation subject = new CustomDocumentation(PROPERTIES); + + @Test + void testCreate() { + var defaultDoc = CustomDocumentation.of(ApiDocumentationProfile.DEFAULT); + assertTrue(defaultDoc.isEmpty()); + + var enturDoc = CustomDocumentation.of(ApiDocumentationProfile.ENTUR); + assertFalse(enturDoc.isEmpty()); + } + + @Test + void testTypeDescriptionWithUnknownKey() { + assertEquals(empty(), subject.typeDescription("", ORIGINAL_DOC)); + assertEquals(empty(), subject.typeDescription("ANY_KEY", ORIGINAL_DOC)); + assertEquals(empty(), subject.typeDescription("ANY_KEY", null)); + } + + @Test + void testTypeDescription() { + assertEquals(Optional.of("Doc 1"), subject.typeDescription("Type1", ORIGINAL_DOC)); + assertEquals( + Optional.of(ORIGINAL_DOC + "\n\nDoc 2"), + subject.typeDescription("Type2", ORIGINAL_DOC) + ); + assertEquals(Optional.empty(), subject.typeDescription("Type3", ORIGINAL_DOC)); + } + + @Test + void testFieldDescription() { + assertEquals(Optional.of("Doc f1"), subject.fieldDescription("Type", "field1", ORIGINAL_DOC)); + assertEquals( + Optional.of("Deprecated f2"), + subject.fieldDeprecatedReason("Type", "field2", ORIGINAL_DOC) + ); + assertEquals( + Optional.of("Original\n\nDoc f3"), + subject.fieldDescription("Type", "field3", ORIGINAL_DOC) + ); + assertEquals( + Optional.of("Original\n\nDeprecated f4"), + subject.fieldDeprecatedReason("Type", "field4", ORIGINAL_DOC) + ); + assertEquals(Optional.empty(), subject.fieldDeprecatedReason("Type", "field5", ORIGINAL_DOC)); + } +} diff --git a/application/src/test/java/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.java b/application/src/test/java/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.java new file mode 100644 index 00000000000..44318d613a4 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.java @@ -0,0 +1,139 @@ +package org.opentripplanner.apis.support.graphql.injectdoc; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import graphql.schema.Coercing; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLSchema; +import graphql.schema.SchemaTransformer; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.SchemaPrinter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentripplanner._support.text.TextAssertions; + +/** + * This test reads in a schema file, injects documentation and convert the + * new schema to an SDL text string. The result is then compared to the + * "expected" SDL file. The input and expected files are found in the + * resources - with the same name as this test. + *

+ * Note! There is a bug in the Java GraphQL library. Existing deprecated reasons + * cannot be changed or replaced. This test adds test-cases for this, but excludes + * them from the expected result. If this is fixed in the GraphQL library, this + * test will fail, and should be updated by updating the expected result. + */ +class InjectCustomDocumentationTest { + + private GraphQLSchema schema; + private String sdlExpected; + + @BeforeEach + void setUp() throws IOException { + var sdl = loadSchemaResource(".graphql"); + sdlExpected = loadSchemaResource(".graphql.expected"); + + var parser = new SchemaParser(); + var generator = new SchemaGenerator(); + var typeRegistry = parser.parse(sdl); + schema = generator.makeExecutableSchema(typeRegistry, buildRuntimeWiring()); + } + + private static RuntimeWiring buildRuntimeWiring() { + return RuntimeWiring + .newRuntimeWiring() + .type("QueryType", b -> b.dataFetcher("listE", e -> List.of())) + .type("En", b -> b.enumValues(n -> n)) + .type("AB", b -> b.typeResolver(it -> null)) + .type("AC", b -> b.typeResolver(it -> null)) + .scalar( + GraphQLScalarType + .newScalar() + .name("Duration") + .coercing(new Coercing() {}) + .build() + ) + .build(); + } + + /** + * Return a map of documentation key/values. The + * value is the same as the key for easy recognition. + */ + static Map text() { + return Stream + .of( + "AB.description", + "AC.description.append", + "AType.description", + "AType.a.description", + "AType.b.deprecated", + "BType.description", + "BType.a.description", + "BType.a.deprecated", + "CType.description.append", + "CType.a.description.append", + "CType.b.deprecated.append", + "QueryType.findAB.description", + "QueryType.getAC.deprecated", + "AEnum.description", + "AEnum.E1.description", + "AEnum.E2.deprecated", + "AEnum.E3.deprecated", + "Duration.description", + "InputType.description", + "InputType.a.description", + "InputType.b.deprecated", + "InputType.c.deprecated" + ) + .collect(Collectors.toMap(e -> e, e -> e)); + } + + @Test + void test() { + Map texts = text(); + var customDocumentation = new CustomDocumentation(texts); + var visitor = new InjectCustomDocumentation(customDocumentation); + var newSchema = SchemaTransformer.transformSchema(schema, visitor); + var p = new SchemaPrinter(); + var result = p.print(newSchema); + + var missingValues = texts + .values() + .stream() + .sorted() + .filter(it -> !result.contains(it)) + .toList(); + + // There is a bug in the Java GraphQL API, existing deprecated + // doc is not updated or replaced. + var expected = List.of( + "AEnum.E3.deprecated", + "BType.a.deprecated", + "CType.b.deprecated.append", + "InputType.c.deprecated" + ); + + assertEquals(expected, missingValues); + + TextAssertions.assertLinesEquals(sdlExpected, result); + } + + @SuppressWarnings("DataFlowIssue") + String loadSchemaResource(String suffix) throws IOException { + var cl = getClass(); + var name = cl.getName().replace('.', '/') + suffix; + return new String( + ClassLoader.getSystemResourceAsStream(name).readAllBytes(), + StandardCharsets.UTF_8 + ); + } +} diff --git a/application/src/test/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchemaTest.java b/application/src/test/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchemaTest.java index 4cdb0586aa7..3fc33081cda 100644 --- a/application/src/test/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchemaTest.java +++ b/application/src/test/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchemaTest.java @@ -9,6 +9,7 @@ import java.io.File; import org.junit.jupiter.api.Test; import org.opentripplanner._support.time.ZoneIds; +import org.opentripplanner.apis.support.graphql.injectdoc.ApiDocumentationProfile; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitTuningParameters; import org.opentripplanner.routing.api.request.RouteRequest; @@ -23,6 +24,7 @@ void testSchemaBuild() { var schema = TransmodelGraphQLSchema.create( new RouteRequest(), ZoneIds.OSLO, + ApiDocumentationProfile.DEFAULT, TransitTuningParameters.FOR_TEST ); assertNotNull(schema); 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/OsmBoardingLocationsModuleTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/OsmBoardingLocationsModuleTest.java index c55e482e533..a4d5e86ced8 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/OsmBoardingLocationsModuleTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/OsmBoardingLocationsModuleTest.java @@ -2,18 +2,27 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; +import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; +import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.graph_builder.module.osm.OsmModule; import org.opentripplanner.osm.OsmProvider; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildRepository; +import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildService; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.street.model.edge.AreaEdge; import org.opentripplanner.street.model.edge.BoardingLocationToStopLink; @@ -32,25 +41,11 @@ import org.opentripplanner.transit.service.SiteRepository; import org.opentripplanner.transit.service.TimetableRepository; -/** - * We test that the platform area at Herrenberg station (https://www.openstreetmap.org/way/27558650) - * is correctly linked to the stop even though it is not the closest edge to the stop. - */ class OsmBoardingLocationsModuleTest { private final TimetableRepositoryForTest testModel = TimetableRepositoryForTest.of(); - File file = ResourceLoader - .of(OsmBoardingLocationsModuleTest.class) - .file("herrenberg-minimal.osm.pbf"); - RegularStop platform = testModel - .stop("de:08115:4512:4:101") - .withCoordinate(48.59328, 8.86128) - .build(); - RegularStop busStop = testModel.stop("de:08115:4512:5:C", 48.59434, 8.86452).build(); - RegularStop floatingBusStop = testModel.stop("floating-bus-stop", 48.59417, 8.86464).build(); - - static Stream testCases() { + static Stream herrenbergTestCases() { return Stream.of( Arguments.of( false, @@ -63,11 +58,25 @@ static Stream testCases() { ); } + /** + * We test that the platform area at Herrenberg station (https://www.openstreetmap.org/way/27558650) + * is correctly linked to the stop even though it is not the closest edge to the stop. + */ @ParameterizedTest( name = "add boarding locations and link them to platform edges when skipVisibility={0}" ) - @MethodSource("testCases") + @MethodSource("herrenbergTestCases") void addAndLinkBoardingLocations(boolean areaVisibility, Set linkedVertices) { + File file = ResourceLoader + .of(OsmBoardingLocationsModuleTest.class) + .file("herrenberg-minimal.osm.pbf"); + RegularStop platform = testModel + .stop("de:08115:4512:4:101") + .withCoordinate(48.59328, 8.86128) + .build(); + RegularStop busStop = testModel.stop("de:08115:4512:5:C", 48.59434, 8.86452).build(); + RegularStop floatingBusStop = testModel.stop("floating-bus-stop", 48.59417, 8.86464).build(); + var deduplicator = new Deduplicator(); var graph = new Graph(deduplicator); var timetableRepository = new TimetableRepository(new SiteRepository(), deduplicator); @@ -83,8 +92,10 @@ void addAndLinkBoardingLocations(boolean areaVisibility, Set linkedVerti Set.of(floatingBusVertex.getStop().getId().getId()), new NonLocalizedString("bus stop not connected to street network") ); + var osmInfoRepository = new DefaultOsmInfoGraphBuildRepository(); + var vehicleParkingRepository = new DefaultVehicleParkingRepository(); var osmModule = OsmModule - .of(provider, graph, new DefaultVehicleParkingRepository()) + .of(provider, graph, osmInfoRepository, vehicleParkingRepository) .withBoardingAreaRefTags(Set.of("ref", "ref:IFOPT")) .withAreaVisibility(areaVisibility) .build(); @@ -107,7 +118,8 @@ void addAndLinkBoardingLocations(boolean areaVisibility, Set linkedVerti assertEquals(0, platformVertex.getIncoming().size()); assertEquals(0, platformVertex.getOutgoing().size()); - new OsmBoardingLocationsModule(graph, timetableRepository).buildGraph(); + var osmService = new DefaultOsmInfoGraphBuildService(osmInfoRepository); + new OsmBoardingLocationsModule(graph, osmService, timetableRepository).buildGraph(); var boardingLocations = graph.getVerticesOfType(OsmBoardingLocationVertex.class); assertEquals(5, boardingLocations.size()); // 3 nodes connected to the street network, plus one "floating" and one area centroid created by the module @@ -141,13 +153,13 @@ void addAndLinkBoardingLocations(boolean areaVisibility, Set linkedVerti assertEquals(1, platformCentroids.size()); - var platform = platformCentroids.get(0); + var platformCentroid = platformCentroids.get(0); - assertConnections(platform, Set.of(BoardingLocationToStopLink.class, AreaEdge.class)); + assertConnections(platformCentroid, Set.of(BoardingLocationToStopLink.class, AreaEdge.class)); assertEquals( linkedVertices, - platform + platformCentroid .getOutgoingStreetEdges() .stream() .map(Edge::getToVertex) @@ -157,7 +169,7 @@ void addAndLinkBoardingLocations(boolean areaVisibility, Set linkedVerti assertEquals( linkedVertices, - platform + platformCentroid .getIncomingStreetEdges() .stream() .map(Edge::getFromVertex) @@ -177,6 +189,201 @@ void addAndLinkBoardingLocations(boolean areaVisibility, Set linkedVerti .forEach(e -> assertEquals("Platform 101;102", e.getName().toString())); } + /** + * We test that the underground platforms at Moorgate station (https://www.openstreetmap.org/way/1328222021) + * is correctly linked to the stop even though it is not the closest edge to the stop. + */ + @Test + void testLinearPlatforms() { + var deduplicator = new Deduplicator(); + var graph = new Graph(deduplicator); + var osmInfoRepository = new DefaultOsmInfoGraphBuildRepository(); + var osmModule = OsmModule + .of( + new OsmProvider( + ResourceLoader.of(OsmBoardingLocationsModuleTest.class).file("moorgate.osm.pbf"), + false + ), + graph, + osmInfoRepository, + new DefaultVehicleParkingRepository() + ) + .withBoardingAreaRefTags(Set.of("naptan:AtcoCode")) + .build(); + osmModule.buildGraph(); + + var factory = new VertexFactory(graph); + + class TestCase { + + /** + * The linear platform to be tested + */ + public final RegularStop platform; + + /** + * The label of a vertex where the centroid should be connected to + */ + public final VertexLabel beginLabel; + + /** + * The label of the other vertex where the centroid should be connected to + */ + public final VertexLabel endLabel; + + private TransitStopVertex platformVertex = null; + + public TestCase(RegularStop platform, VertexLabel beginLabel, VertexLabel endLabel) { + this.platform = platform; + this.beginLabel = beginLabel; + this.endLabel = endLabel; + } + + /** + * Get a TransitStopVertex for the platform in the graph. It is made and added to the graph + * on the first call. + */ + TransitStopVertex getPlatformVertex() { + if (platformVertex == null) { + platformVertex = factory.transitStop(TransitStopVertex.of().withStop(platform)); + } + return platformVertex; + } + } + + var testCases = List.of( + new TestCase( + testModel + .stop("9100MRGT9") + .withName(I18NString.of("Moorgate (Platform 9)")) + .withCoordinate(51.51922107872304, -0.08767468698832413) + .withPlatformCode("9") + .build(), + VertexLabel.osm(12288669589L), + VertexLabel.osm(12288675219L) + ), + new TestCase( + testModel + .stop("9400ZZLUMGT3") + .withName(I18NString.of("Moorgate (Platform 7)")) + .withCoordinate(51.51919235051611, -0.08769925990953176) + .withPlatformCode("7") + .build(), + VertexLabel.osm(12288669575L), + VertexLabel.osm(12288675230L) + ) + ); + + for (var testCase : testCases) { + // test that the platforms are not connected + var platformVertex = testCase.getPlatformVertex(); + assertEquals(0, platformVertex.getIncoming().size()); + assertEquals(0, platformVertex.getOutgoing().size()); + + // test that the vertices to be connected by the centroid are currently connected + var fromVertex = Objects.requireNonNull(graph.getVertex(testCase.beginLabel)); + var toVertex = Objects.requireNonNull(graph.getVertex(testCase.endLabel)); + assertTrue( + getEdge(fromVertex, toVertex).isPresent(), + "malformed test: the vertices where the centroid is supposed to be located between aren't connected" + ); + assertTrue( + getEdge(toVertex, fromVertex).isPresent(), + "malformed test: the vertices where the centroid is supposed to be located between aren't connected" + ); + } + + var timetableRepository = new TimetableRepository(new SiteRepository(), deduplicator); + new OsmBoardingLocationsModule( + graph, + new DefaultOsmInfoGraphBuildService(osmInfoRepository), + timetableRepository + ) + .buildGraph(); + + var boardingLocations = graph.getVerticesOfType(OsmBoardingLocationVertex.class); + + for (var testCase : testCases) { + var platformVertex = testCase.getPlatformVertex(); + var fromVertex = Objects.requireNonNull(graph.getVertex(testCase.beginLabel)); + var toVertex = Objects.requireNonNull(graph.getVertex(testCase.endLabel)); + + var centroid = boardingLocations + .stream() + .filter(b -> b.references.contains(testCase.platform.getId().getId())) + .findFirst() + .orElseThrow(); + + // TODO: we should ideally place the centroid vertex directly on the platform by splitting + // the platform edge, but it is too difficult to touch the splitter code to use a given + // centroid vertex instead of a generated split vertex, so what we actually do is to directly + // connect the platform vertex to the split vertex + + // the actual centroid isn't used + assertEquals(0, centroid.getDegreeIn()); + assertEquals(0, centroid.getDegreeOut()); + + for (var vertex : platformVertex.getIncoming()) { + assertSplitVertex(vertex.getFromVertex(), centroid, fromVertex, toVertex); + } + + for (var vertex : platformVertex.getOutgoing()) { + assertSplitVertex(vertex.getToVertex(), centroid, fromVertex, toVertex); + } + } + } + + /** + * Assert that a split vertex is near to the given centroid, and it is possible to travel between + * the original vertices through the split vertex in a straight line + */ + private static void assertSplitVertex( + Vertex splitVertex, + OsmBoardingLocationVertex centroid, + Vertex begin, + Vertex end + ) { + var distance = SphericalDistanceLibrary.distance( + splitVertex.getCoordinate(), + centroid.getCoordinate() + ); + // FIXME: I am not sure why the calculated centroid from the original OSM geometry is about 2 m + // from the platform + assertTrue(distance < 4, "The split vertex is more than 4 m apart from the centroid"); + assertConnections(splitVertex, begin, end); + + if (splitVertex != begin && splitVertex != end) { + var forwardEdges = getEdge(begin, splitVertex) + .flatMap(first -> getEdge(splitVertex, end).map(second -> List.of(first, second))); + var backwardEdges = getEdge(end, splitVertex) + .flatMap(first -> getEdge(splitVertex, begin).map(second -> List.of(first, second))); + for (var edgeList : List.of(forwardEdges, backwardEdges)) { + edgeList.ifPresent(edges -> + assertEquals( + edges.getFirst().getOutAngle(), + edges.getLast().getInAngle(), + "The split vertex is not on a straight line between the connected vertices" + ) + ); + } + } + } + + /** + * Assert that there is a one-way path from the beginning through the given vertex to the end + * or vice versa. + */ + private static void assertConnections(Vertex vertex, Vertex beginning, Vertex end) { + if (vertex == beginning || vertex == end) { + assertTrue(beginning.isConnected(end)); + } + + assertTrue( + (getEdge(beginning, vertex).isPresent() && getEdge(vertex, end).isPresent()) || + (getEdge(end, vertex).isPresent() && getEdge(vertex, beginning).isPresent()) + ); + } + private void assertConnections( OsmBoardingLocationVertex busBoardingLocation, Set> expected @@ -187,4 +394,12 @@ private void assertConnections( assertEquals(expected, edges.stream().map(Edge::getClass).collect(Collectors.toSet())) ); } + + private static Optional getEdge(Vertex from, Vertex to) { + return from + .getOutgoingStreetEdges() + .stream() + .filter(edge -> edge.getToVertex() == to) + .findFirst(); + } } diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/islandpruning/IslandPruningUtils.java b/application/src/test/java/org/opentripplanner/graph_builder/module/islandpruning/IslandPruningUtils.java index d71a60a972e..8e5a455095b 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/islandpruning/IslandPruningUtils.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/islandpruning/IslandPruningUtils.java @@ -5,6 +5,7 @@ import org.opentripplanner.graph_builder.module.osm.OsmModule; import org.opentripplanner.osm.OsmProvider; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.service.SiteRepository; @@ -24,9 +25,11 @@ static Graph buildOsmGraph( var graph = new Graph(deduplicator); var timetableRepository = new TimetableRepository(new SiteRepository(), deduplicator); // Add street data from OSM - OsmProvider osmProvider = new OsmProvider(osmFile, true); - OsmModule osmModule = OsmModule - .of(osmProvider, graph, new DefaultVehicleParkingRepository()) + var osmProvider = new OsmProvider(osmFile, true); + var osmInfoRepository = new DefaultOsmInfoGraphBuildRepository(); + var vehicleParkingRepository = new DefaultVehicleParkingRepository(); + var osmModule = OsmModule + .of(osmProvider, graph, osmInfoRepository, vehicleParkingRepository) .withEdgeNamer(new TestNamer()) .build(); diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/linking/LinkingTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/linking/LinkingTest.java index a6afa89707f..6c51235e703 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/linking/LinkingTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/linking/LinkingTest.java @@ -21,6 +21,7 @@ import org.opentripplanner.graph_builder.module.osm.OsmModule; import org.opentripplanner.osm.OsmProvider; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.street.model.StreetTraversalPermission; import org.opentripplanner.street.model._data.StreetModelForTest; @@ -152,16 +153,20 @@ public void testStopsLinkedIdentically() { public static TestOtpModel buildGraphNoTransit() { var deduplicator = new Deduplicator(); var siteRepository = new SiteRepository(); - var gg = new Graph(deduplicator); + var graph = new Graph(deduplicator); var timetableRepository = new TimetableRepository(siteRepository, deduplicator); File file = ResourceLoader.of(LinkingTest.class).file("columbus.osm.pbf"); - OsmProvider provider = new OsmProvider(file, false); + var provider = new OsmProvider(file, false); + var osmInfoRepository = new DefaultOsmInfoGraphBuildRepository(); + var vehicleParkingRepository = new DefaultVehicleParkingRepository(); - OsmModule osmModule = OsmModule.of(provider, gg, new DefaultVehicleParkingRepository()).build(); + var osmModule = OsmModule + .of(provider, graph, osmInfoRepository, vehicleParkingRepository) + .build(); osmModule.buildGraph(); - return new TestOtpModel(gg, timetableRepository); + return new TestOtpModel(graph, timetableRepository); } private static List outgoingStls(final TransitStopVertex tsv) { 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 833b14ade9d..6de345ddd9c 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 @@ -32,6 +32,7 @@ import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.impl.GraphPathFinder; +import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingService; @@ -53,30 +54,35 @@ public class OsmModuleTest { @Test public void testGraphBuilder() { var deduplicator = new Deduplicator(); - var gg = new Graph(deduplicator); + var graph = new Graph(deduplicator); File file = RESOURCE_LOADER.file("map.osm.pbf"); OsmProvider provider = new OsmProvider(file, true); OsmModule osmModule = OsmModule - .of(provider, gg, new DefaultVehicleParkingRepository()) + .of( + provider, + graph, + new DefaultOsmInfoGraphBuildRepository(), + new DefaultVehicleParkingRepository() + ) .withAreaVisibility(true) .build(); osmModule.buildGraph(); // Kamiennogorska at south end of segment - Vertex v1 = gg.getVertex(VertexLabel.osm(280592578)); + Vertex v1 = graph.getVertex(VertexLabel.osm(280592578)); // Kamiennogorska at Mariana Smoluchowskiego - Vertex v2 = gg.getVertex(VertexLabel.osm(288969929)); + Vertex v2 = graph.getVertex(VertexLabel.osm(288969929)); // Mariana Smoluchowskiego, north end - Vertex v3 = gg.getVertex(VertexLabel.osm(280107802)); + Vertex v3 = graph.getVertex(VertexLabel.osm(280107802)); // Mariana Smoluchowskiego, south end (of segment connected to v2) - Vertex v4 = gg.getVertex(VertexLabel.osm(288970952)); + Vertex v4 = graph.getVertex(VertexLabel.osm(288970952)); assertNotNull(v1); assertNotNull(v2); @@ -117,9 +123,11 @@ public void testBuildGraphDetailed() { var gg = new Graph(deduplicator); File file = RESOURCE_LOADER.file("NYC_small.osm.pbf"); - OsmProvider provider = new OsmProvider(file, true); - OsmModule osmModule = OsmModule - .of(provider, gg, new DefaultVehicleParkingRepository()) + var provider = new OsmProvider(file, true); + var osmInfoRepository = new DefaultOsmInfoGraphBuildRepository(); + var vehicleParkingRepository = new DefaultVehicleParkingRepository(); + var osmModule = OsmModule + .of(provider, gg, osmInfoRepository, vehicleParkingRepository) .withAreaVisibility(true) .build(); @@ -315,7 +323,14 @@ void testBarrierAtEnd() { File file = RESOURCE_LOADER.file("accessno-at-end.pbf"); OsmProvider provider = new OsmProvider(file, false); - OsmModule loader = OsmModule.of(provider, graph, new DefaultVehicleParkingRepository()).build(); + OsmModule loader = OsmModule + .of( + provider, + graph, + new DefaultOsmInfoGraphBuildRepository(), + new DefaultVehicleParkingRepository() + ) + .build(); loader.buildGraph(); Vertex start = graph.getVertex(VertexLabel.osm(1)); @@ -339,7 +354,7 @@ private BuildResult buildParkingLots() { .map(f -> new OsmProvider(f, false)) .toList(); var module = OsmModule - .of(providers, graph, service) + .of(providers, graph, new DefaultOsmInfoGraphBuildRepository(), service) .withStaticParkAndRide(true) .withStaticBikeParkAndRide(true) .build(); @@ -363,10 +378,12 @@ private void testBuildingAreas(boolean skipVisibility) { var graph = new Graph(deduplicator); File file = RESOURCE_LOADER.file("usf_area.osm.pbf"); - OsmProvider provider = new OsmProvider(file, false); + var provider = new OsmProvider(file, false); + var osmInfoRepository = new DefaultOsmInfoGraphBuildRepository(); + var vehicleParkingRepository = new DefaultVehicleParkingRepository(); - OsmModule loader = OsmModule - .of(provider, graph, new DefaultVehicleParkingRepository()) + var loader = OsmModule + .of(provider, graph, osmInfoRepository, vehicleParkingRepository) .withAreaVisibility(!skipVisibility) .build(); diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/PlatformLinkerTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/PlatformLinkerTest.java index f952bf90710..97ccbdd7719 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/PlatformLinkerTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/PlatformLinkerTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import org.opentripplanner.osm.OsmProvider; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.street.model.edge.AreaEdge; import org.opentripplanner.street.model.vertex.Vertex; @@ -24,20 +25,25 @@ public void testLinkEntriesToPlatforms() { var stairsEndpointLabel = VertexLabel.osm(1028861028); var deduplicator = new Deduplicator(); - var gg = new Graph(deduplicator); + var graph = new Graph(deduplicator); File file = ResourceLoader.of(this).file("skoyen.osm.pbf"); OsmProvider provider = new OsmProvider(file, false); OsmModule osmModule = OsmModule - .of(provider, gg, new DefaultVehicleParkingRepository()) + .of( + provider, + graph, + new DefaultOsmInfoGraphBuildRepository(), + new DefaultVehicleParkingRepository() + ) .withPlatformEntriesLinking(true) .build(); osmModule.buildGraph(); - Vertex stairsEndpoint = gg.getVertex(stairsEndpointLabel); + Vertex stairsEndpoint = graph.getVertex(stairsEndpointLabel); // verify outgoing links assertTrue(stairsEndpoint.getOutgoing().stream().anyMatch(AreaEdge.class::isInstance)); diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/TriangleInequalityTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/TriangleInequalityTest.java index ffc1f661dcc..1b8f7c9c58a 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/TriangleInequalityTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/TriangleInequalityTest.java @@ -21,6 +21,7 @@ import org.opentripplanner.routing.api.request.request.filter.AllowAllTransitFilter; import org.opentripplanner.routing.api.request.request.filter.TransitFilter; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.vertex.Vertex; @@ -52,7 +53,12 @@ public static void onlyOnce() { File file = ResourceLoader.of(TriangleInequalityTest.class).file("NYC_small.osm.pbf"); OsmProvider provider = new OsmProvider(file, true); OsmModule osmModule = OsmModule - .of(provider, graph, new DefaultVehicleParkingRepository()) + .of( + provider, + graph, + new DefaultOsmInfoGraphBuildRepository(), + new DefaultVehicleParkingRepository() + ) .withAreaVisibility(true) .build(); osmModule.buildGraph(); diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/UnconnectedAreasTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/UnconnectedAreasTest.java index 103dafa61b9..22b486d7cd6 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/UnconnectedAreasTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/UnconnectedAreasTest.java @@ -13,6 +13,7 @@ import org.opentripplanner.graph_builder.module.TestStreetLinkerModule; import org.opentripplanner.osm.OsmProvider; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.street.model.edge.StreetVehicleParkingLink; import org.opentripplanner.street.model.edge.VehicleParkingEdge; @@ -163,7 +164,12 @@ private Graph buildOsmGraph(String osmFileName, DataImportIssueStore issueStore) var timetableRepository = new TimetableRepository(siteRepository, deduplicator); OsmProvider provider = new OsmProvider(RESOURCE_LOADER.file(osmFileName), false); OsmModule loader = OsmModule - .of(provider, graph, new DefaultVehicleParkingRepository()) + .of( + provider, + graph, + new DefaultOsmInfoGraphBuildRepository(), + new DefaultVehicleParkingRepository() + ) .withIssueStore(issueStore) .withAreaVisibility(true) .withStaticParkAndRide(true) diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/UnroutableTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/UnroutableTest.java index 138c3e67181..835c5a7fbcb 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/UnroutableTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/UnroutableTest.java @@ -10,6 +10,7 @@ import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.vertex.Vertex; @@ -39,7 +40,12 @@ public void setUp() throws Exception { var osmDataFile = ResourceLoader.of(UnroutableTest.class).file("bridge_construction.osm.pbf"); OsmProvider provider = new OsmProvider(osmDataFile, true); OsmModule osmBuilder = OsmModule - .of(provider, graph, new DefaultVehicleParkingRepository()) + .of( + provider, + graph, + new DefaultOsmInfoGraphBuildRepository(), + new DefaultVehicleParkingRepository() + ) .withAreaVisibility(true) .build(); osmBuilder.buildGraph(); 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/java/org/opentripplanner/routing/core/MoneyTest.java b/application/src/test/java/org/opentripplanner/routing/core/MoneyTest.java index 4fdb74d5340..fdfa71c222b 100644 --- a/application/src/test/java/org/opentripplanner/routing/core/MoneyTest.java +++ b/application/src/test/java/org/opentripplanner/routing/core/MoneyTest.java @@ -11,6 +11,7 @@ import java.util.Currency; import java.util.Locale; import java.util.stream.Stream; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -39,6 +40,7 @@ static Stream testCases() { ); } + @Disabled @ParameterizedTest(name = "{0} with locale {1} should localise to \"{2}\"") @MethodSource("testCases") void localize(Money money, Locale locale, String expected) { 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 400a9eba2ba..9ccd6177cfd 100644 --- a/application/src/test/java/org/opentripplanner/routing/graph/GraphSerializationTest.java +++ b/application/src/test/java/org/opentripplanner/routing/graph/GraphSerializationTest.java @@ -23,6 +23,8 @@ import org.opentripplanner.ext.emissions.EmissionsDataModel; import org.opentripplanner.framework.geometry.HashGridSpatialIndex; import org.opentripplanner.graph_builder.issue.api.DataImportIssueSummary; +import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; +import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.service.worldenvelope.WorldEnvelopeRepository; @@ -67,11 +69,13 @@ public class GraphSerializationTest { @Test public void testRoundTripSerializationForGTFSGraph() throws Exception { TestOtpModel model = ConstantsForTests.buildNewPortlandGraph(true); + var osmGraphBuildRepository = new DefaultOsmInfoGraphBuildRepository(); var weRepo = new DefaultWorldEnvelopeRepository(); var emissionsDataModel = new EmissionsDataModel(); var parkingRepository = new DefaultVehicleParkingRepository(); testRoundTrip( model.graph(), + osmGraphBuildRepository, model.timetableRepository(), weRepo, parkingRepository, @@ -85,11 +89,13 @@ public void testRoundTripSerializationForGTFSGraph() throws Exception { @Test public void testRoundTripSerializationForNetexGraph() throws Exception { TestOtpModel model = ConstantsForTests.buildNewMinimalNetexGraph(); + var osmGraphBuildRepository = new DefaultOsmInfoGraphBuildRepository(); var worldEnvelopeRepository = new DefaultWorldEnvelopeRepository(); var emissionsDataModel = new EmissionsDataModel(); var parkingRepository = new DefaultVehicleParkingRepository(); testRoundTrip( model.graph(), + osmGraphBuildRepository, model.timetableRepository(), worldEnvelopeRepository, parkingRepository, @@ -191,6 +197,7 @@ private static void assertNoDifferences(Graph g1, Graph g2) { */ private void testRoundTrip( Graph originalGraph, + OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, TimetableRepository originalTimetableRepository, WorldEnvelopeRepository worldEnvelopeRepository, VehicleParkingRepository vehicleParkingRepository, @@ -202,6 +209,7 @@ private void testRoundTrip( streetLimitationParameters.initMaxCarSpeed(40); SerializedGraphObject serializedObj = new SerializedGraphObject( originalGraph, + osmInfoGraphBuildRepository, originalTimetableRepository, worldEnvelopeRepository, vehicleParkingRepository, diff --git a/application/src/test/java/org/opentripplanner/street/search/state/EdgeTraverserTest.java b/application/src/test/java/org/opentripplanner/street/search/state/EdgeTraverserTest.java deleted file mode 100644 index a2cd7e61b62..00000000000 --- a/application/src/test/java/org/opentripplanner/street/search/state/EdgeTraverserTest.java +++ /dev/null @@ -1,191 +0,0 @@ -package org.opentripplanner.street.search.state; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.opentripplanner.street.model._data.StreetModelForTest.intersectionVertex; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.function.Function; -import org.junit.jupiter.api.Test; -import org.opentripplanner._support.geometry.Coordinates; -import org.opentripplanner.routing.api.request.StreetMode; -import org.opentripplanner.street.model.StreetTraversalPermission; -import org.opentripplanner.street.model._data.StreetModelForTest; -import org.opentripplanner.street.model.edge.Edge; -import org.opentripplanner.street.model.vertex.IntersectionVertex; -import org.opentripplanner.street.search.TraverseMode; -import org.opentripplanner.street.search.request.StreetSearchRequest; - -class EdgeTraverserTest { - - private static final IntersectionVertex BERLIN_V = intersectionVertex(Coordinates.BERLIN); - private static final IntersectionVertex BRANDENBURG_GATE_V = intersectionVertex( - Coordinates.BERLIN_BRANDENBURG_GATE - ); - private static final IntersectionVertex FERNSEHTURM_V = intersectionVertex( - Coordinates.BERLIN_FERNSEHTURM - ); - private static final IntersectionVertex ADMIRALBRUCKE_V = intersectionVertex( - Coordinates.BERLIN_ADMIRALBRUCKE - ); - - @Test - void emptyEdges() { - var request = StreetSearchRequest - .of() - .withStartTime(Instant.EPOCH) - .withMode(StreetMode.WALK) - .build(); - var initialStates = State.getInitialStates(Set.of(BERLIN_V), request); - var traversedState = EdgeTraverser.traverseEdges(initialStates, List.of()); - - assertSame(initialStates.iterator().next(), traversedState.get()); - } - - @Test - void failedTraversal() { - var edge = StreetModelForTest - .streetEdge(BERLIN_V, BRANDENBURG_GATE_V) - .toBuilder() - .withPermission(StreetTraversalPermission.NONE) - .buildAndConnect(); - - var edges = List.of(edge); - var request = StreetSearchRequest - .of() - .withStartTime(Instant.EPOCH) - .withMode(StreetMode.WALK) - .build(); - var initialStates = State.getInitialStates(Set.of(edge.getFromVertex()), request); - var traversedState = EdgeTraverser.traverseEdges(initialStates, edges); - - assertTrue(traversedState.isEmpty()); - } - - @Test - void withSingleState() { - var edge = StreetModelForTest - .streetEdge(BERLIN_V, BRANDENBURG_GATE_V) - .toBuilder() - .withPermission(StreetTraversalPermission.ALL) - .buildAndConnect(); - - var edges = List.of(edge); - var request = StreetSearchRequest - .of() - .withStartTime(Instant.EPOCH) - .withMode(StreetMode.WALK) - .build(); - var initialStates = State.getInitialStates(Set.of(edge.getFromVertex()), request); - var traversedState = EdgeTraverser.traverseEdges(initialStates, edges).get(); - - assertEquals(List.of(TraverseMode.WALK), stateValues(traversedState, State::getBackMode)); - assertEquals(1719, traversedState.getElapsedTimeSeconds()); - } - - @Test - void withSingleArriveByState() { - var edge = StreetModelForTest - .streetEdge(BERLIN_V, BRANDENBURG_GATE_V) - .toBuilder() - .withPermission(StreetTraversalPermission.ALL) - .buildAndConnect(); - - var edges = List.of(edge); - var request = StreetSearchRequest - .of() - .withStartTime(Instant.EPOCH) - .withMode(StreetMode.WALK) - .withArriveBy(true) - .build(); - var initialStates = State.getInitialStates(Set.of(edge.getToVertex()), request); - var traversedState = EdgeTraverser.traverseEdges(initialStates, edges).get(); - - assertSame(BERLIN_V, traversedState.getVertex()); - assertEquals(List.of(TraverseMode.WALK), stateValues(traversedState, State::getBackMode)); - assertEquals(1719, traversedState.getElapsedTimeSeconds()); - } - - @Test - void withMultipleStates() { - // CAR_PICKUP creates parallel walking and driving states - // This tests that of the two states (WALKING, CAR) the least weight (CAR) is selected - var edge = StreetModelForTest - .streetEdge(BERLIN_V, BRANDENBURG_GATE_V) - .toBuilder() - .withPermission(StreetTraversalPermission.ALL) - .buildAndConnect(); - - var edges = List.of(edge); - var request = StreetSearchRequest - .of() - .withStartTime(Instant.EPOCH) - .withMode(StreetMode.CAR_PICKUP) - .build(); - var initialStates = State.getInitialStates(Set.of(edge.getFromVertex()), request); - var traversedState = EdgeTraverser.traverseEdges(initialStates, edges).get(); - - assertEquals(List.of(TraverseMode.CAR), stateValues(traversedState, State::getBackMode)); - assertEquals(205, traversedState.getElapsedTimeSeconds()); - } - - @Test - void withDominatedStates() { - // CAR_PICKUP creates parallel walking and driving states - // This tests that the most optimal (walking and driving the last stretch) is found after - // discarding the initial driving state for edge1 - var edge1 = StreetModelForTest - .streetEdge(FERNSEHTURM_V, BERLIN_V) - .toBuilder() - .withPermission(StreetTraversalPermission.ALL) - .buildAndConnect(); - var edge2 = StreetModelForTest - .streetEdge(BERLIN_V, BRANDENBURG_GATE_V) - .toBuilder() - .withPermission(StreetTraversalPermission.PEDESTRIAN) - .buildAndConnect(); - var edge3 = StreetModelForTest - .streetEdge(BRANDENBURG_GATE_V, ADMIRALBRUCKE_V) - .toBuilder() - .withPermission(StreetTraversalPermission.ALL) - .buildAndConnect(); - - var edges = List.of(edge1, edge2, edge3); - var request = StreetSearchRequest - .of() - .withStartTime(Instant.EPOCH) - .withMode(StreetMode.CAR_PICKUP) - .build(); - var initialStates = State.getInitialStates(Set.of(edge1.getFromVertex()), request); - var traversedState = EdgeTraverser.traverseEdges(initialStates, edges).get(); - - assertEquals( - List.of(88.103, 2286.029, 3444.28), - stateValues( - traversedState, - state -> state.getBackEdge() != null ? state.getBackEdge().getDistanceMeters() : null - ) - ); - assertEquals( - List.of(TraverseMode.WALK, TraverseMode.WALK, TraverseMode.CAR), - stateValues(traversedState, State::getBackMode) - ); - assertEquals(2169, traversedState.getElapsedTimeSeconds()); - } - - private List stateValues(State state, Function extractor) { - var values = new ArrayList(); - while (state != null) { - var value = extractor.apply(state); - if (value != null) { - values.add(value); - } - state = state.getBackState(); - } - return values.reversed(); - } -} 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 7ea56f66145..9605d950ae0 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 @@ -202,7 +202,7 @@ public TestStateBuilder elevator() { currentState = EdgeTraverser - .traverseEdges(new State[] { currentState }, List.of(link, boardEdge, hopEdge, alightEdge)) + .traverseEdges(currentState, List.of(link, boardEdge, hopEdge, alightEdge)) .orElseThrow(); return this; } diff --git a/application/src/test/java/org/opentripplanner/transit/speed_test/LoadModel.java b/application/src/test/java/org/opentripplanner/transit/speed_test/LoadModel.java new file mode 100644 index 00000000000..5e5823c842b --- /dev/null +++ b/application/src/test/java/org/opentripplanner/transit/speed_test/LoadModel.java @@ -0,0 +1,7 @@ +package org.opentripplanner.transit.speed_test; + +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.standalone.config.BuildConfig; +import org.opentripplanner.transit.service.TimetableRepository; + +record LoadModel(Graph graph, TimetableRepository timetableRepository, BuildConfig buildConfig) {} diff --git a/application/src/test/java/org/opentripplanner/transit/speed_test/SetupHelper.java b/application/src/test/java/org/opentripplanner/transit/speed_test/SetupHelper.java new file mode 100644 index 00000000000..86383d6d94e --- /dev/null +++ b/application/src/test/java/org/opentripplanner/transit/speed_test/SetupHelper.java @@ -0,0 +1,41 @@ +package org.opentripplanner.transit.speed_test; + +import java.io.File; +import java.net.URI; +import javax.annotation.Nullable; +import org.opentripplanner.datastore.OtpDataStore; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.graph.SerializedGraphObject; +import org.opentripplanner.standalone.config.ConfigModel; +import org.opentripplanner.standalone.config.OtpConfigLoader; +import org.opentripplanner.transit.service.TimetableRepository; +import org.opentripplanner.transit.speed_test.options.SpeedTestCmdLineOpts; + +/** + * A package-private helper class for setting up speed tests. + */ +class SetupHelper { + + static LoadModel loadGraph(File baseDir, @Nullable URI path) { + File file = path == null + ? OtpDataStore.graphFile(baseDir) + : path.isAbsolute() ? new File(path) : new File(baseDir, path.getPath()); + SerializedGraphObject serializedGraphObject = SerializedGraphObject.load(file); + Graph graph = serializedGraphObject.graph; + + if (graph == null) { + throw new IllegalStateException( + "Could not find graph at %s".formatted(file.getAbsolutePath()) + ); + } + + TimetableRepository timetableRepository = serializedGraphObject.timetableRepository; + timetableRepository.index(); + graph.index(timetableRepository.getSiteRepository()); + return new LoadModel(graph, timetableRepository, serializedGraphObject.buildConfig); + } + + static void loadOtpFeatures(SpeedTestCmdLineOpts opts) { + ConfigModel.initializeOtpFeatures(new OtpConfigLoader(opts.rootDir()).loadOtpConfig()); + } +} 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 ca4e85eed84..2a3add223e8 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 @@ -27,7 +27,6 @@ import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; import org.opentripplanner.standalone.OtpStartupInfo; import org.opentripplanner.standalone.api.OtpServerRequestContext; -import org.opentripplanner.standalone.config.BuildConfig; import org.opentripplanner.standalone.config.ConfigModel; import org.opentripplanner.standalone.config.DebugUiConfig; import org.opentripplanner.standalone.config.OtpConfigLoader; @@ -153,8 +152,8 @@ public static void main(String[] args) { // Given the following setup SpeedTestCmdLineOpts opts = new SpeedTestCmdLineOpts(args); var config = SpeedTestConfig.config(opts.rootDir()); - loadOtpFeatures(opts); - var model = loadGraph(opts.rootDir(), config.graph); + SetupHelper.loadOtpFeatures(opts); + var model = SetupHelper.loadGraph(opts.rootDir(), config.graph); var timetableRepository = model.timetableRepository(); var buildConfig = model.buildConfig(); var graph = model.graph(); @@ -192,6 +191,9 @@ public void runTest() { } updateTimersWithGlobalCounters(); + + timer.finishUp(); + printProfileStatistics(); saveTestCasesToResultFile(); System.err.println("\nSpeedTest done! " + projectInfo().getVersionString()); @@ -267,27 +269,6 @@ private RoutingResponse performRouting(TestCase testCase) { /* setup helper methods */ - private static void loadOtpFeatures(SpeedTestCmdLineOpts opts) { - ConfigModel.initializeOtpFeatures(new OtpConfigLoader(opts.rootDir()).loadOtpConfig()); - } - - private static LoadModel loadGraph(File baseDir, URI path) { - File file = path == null - ? OtpDataStore.graphFile(baseDir) - : path.isAbsolute() ? new File(path) : new File(baseDir, path.getPath()); - SerializedGraphObject serializedGraphObject = SerializedGraphObject.load(file); - Graph graph = serializedGraphObject.graph; - - if (graph == null) { - throw new IllegalStateException(); - } - - TimetableRepository timetableRepository = serializedGraphObject.timetableRepository; - timetableRepository.index(); - graph.index(timetableRepository.getSiteRepository()); - return new LoadModel(graph, timetableRepository, serializedGraphObject.buildConfig); - } - private void initProfileStatistics() { for (SpeedTestProfile key : opts.profiles()) { workerResults.put(key, new ArrayList<>()); @@ -352,7 +333,6 @@ private void updateTimersWithGlobalCounters() { timer.globalCount("jvm_max_memory", runtime.maxMemory()); timer.globalCount("jvm_total_memory", runtime.totalMemory()); timer.globalCount("jvm_used_memory", runtime.totalMemory() - runtime.freeMemory()); - timer.finishUp(); } /** @@ -368,8 +348,4 @@ private List trimItineraries(RoutingResponse routingResponse) { } return stream.limit(opts.numOfItineraries()).toList(); } - - /* inline classes */ - - record LoadModel(Graph graph, TimetableRepository timetableRepository, BuildConfig buildConfig) {} } diff --git a/application/src/test/java/org/opentripplanner/transit/speed_test/TransferCacheTest.java b/application/src/test/java/org/opentripplanner/transit/speed_test/TransferCacheTest.java new file mode 100644 index 00000000000..e6c8de67688 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/transit/speed_test/TransferCacheTest.java @@ -0,0 +1,68 @@ +package org.opentripplanner.transit.speed_test; + +import static org.opentripplanner.standalone.configure.ConstructApplication.creatTransitLayerForRaptor; +import static org.opentripplanner.transit.speed_test.support.AssertSpeedTestSetup.assertTestDateHasData; + +import java.util.stream.IntStream; +import org.opentripplanner.framework.application.OtpAppException; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.standalone.OtpStartupInfo; +import org.opentripplanner.transit.service.TimetableRepository; +import org.opentripplanner.transit.speed_test.model.timer.SpeedTestTimer; +import org.opentripplanner.transit.speed_test.options.SpeedTestCmdLineOpts; +import org.opentripplanner.transit.speed_test.options.SpeedTestConfig; + +/** + * Test how long it takes to compute the transfer cache. + */ +public class TransferCacheTest { + + public static void main(String[] args) { + try { + OtpStartupInfo.logInfo("Run transfer cache test"); + // Given the following setup + SpeedTestCmdLineOpts opts = new SpeedTestCmdLineOpts(args); + var config = SpeedTestConfig.config(opts.rootDir()); + SetupHelper.loadOtpFeatures(opts); + var model = SetupHelper.loadGraph(opts.rootDir(), config.graph); + var timetableRepository = model.timetableRepository(); + var buildConfig = model.buildConfig(); + + var timer = new SpeedTestTimer(); + timer.setUp(false); + + // Creating transitLayerForRaptor should be integrated into the TimetableRepository, but for now + // we do it manually here + creatTransitLayerForRaptor(timetableRepository, config.transitRoutingParams); + + assertTestDateHasData(timetableRepository, config, buildConfig); + + measureTransferCacheComputation(timer, timetableRepository); + + timer.finishUp(); + } catch (Exception e) { + System.err.println(e.getMessage()); + e.printStackTrace(System.err); + System.exit(1); + } + } + + /** + * Measure how long it takes to compute the transfer cache. + */ + private static void measureTransferCacheComputation( + SpeedTestTimer timer, + TimetableRepository timetableRepository + ) { + IntStream + .range(1, 7) + .forEach(reluctance -> { + RouteRequest routeRequest = new RouteRequest(); + routeRequest.withPreferences(b -> b.withWalk(c -> c.withReluctance(reluctance))); + timer.recordTimer( + "transfer_cache_computation", + () -> timetableRepository.getTransitLayer().initTransferCacheForRequest(routeRequest) + ); + }); + } +} diff --git a/application/src/test/java/org/opentripplanner/transit/speed_test/model/timer/SpeedTestTimer.java b/application/src/test/java/org/opentripplanner/transit/speed_test/model/timer/SpeedTestTimer.java index 48a0548d28e..80970eaad0a 100644 --- a/application/src/test/java/org/opentripplanner/transit/speed_test/model/timer/SpeedTestTimer.java +++ b/application/src/test/java/org/opentripplanner/transit/speed_test/model/timer/SpeedTestTimer.java @@ -39,6 +39,7 @@ public class SpeedTestTimer { List.of(loggerRegistry) ); private final MeterRegistry uploadRegistry = MeterRegistrySetup.getRegistry().orElse(null); + private boolean groupResultByTestCaseCategory = false; public static int nanosToMillisecond(long nanos) { @@ -136,6 +137,18 @@ public void globalCount(String meterName, long count) { } } + /** + * Execute the runnable and record its runtime in the meter name passed in. + */ + public void recordTimer(String meterName, Runnable runnable) { + if (uploadRegistry != null) { + registry.add(uploadRegistry); + var timer = registry.timer(meterName); + timer.record(runnable); + registry.remove(uploadRegistry); + } + } + /** * Calculate the total time mean for the given timer. If the timer is not * found {@link #NOT_AVAILABLE} is returned. This can be the case in unit tests, @@ -175,7 +188,7 @@ public String name(String name, Meter.Type type, String unit) { } private String capitalize(String name) { - if (name.length() != 0 && !Character.isUpperCase(name.charAt(0))) { + if (!name.isEmpty() && !Character.isUpperCase(name.charAt(0))) { char[] chars = name.toCharArray(); chars[0] = Character.toUpperCase(chars[0]); return new String(chars); @@ -208,8 +221,8 @@ public static Result merge(Collection results) { for (Result it : results) { any = it; - min = it.min < min ? it.min : min; - max = it.max > max ? it.max : max; + min = Math.min(it.min, min); + max = Math.max(it.max, max); totTime += it.totTime; count += it.count; } 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/application/src/test/resources/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.graphql b/application/src/test/resources/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.graphql new file mode 100644 index 00000000000..33deaa2a364 --- /dev/null +++ b/application/src/test/resources/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.graphql @@ -0,0 +1,54 @@ +schema { + query: QueryType +} + +"REPLACE" +union AB = AType | BType + +"APPEND TO" +union AC = AType | BType + +# Add doc to an undocumented type +type AType { + a: Duration + b: String +} + +# Replace existing doc +"REPLACE" +type BType { + a: String @deprecated(reason: "REPLACE") +} + +# Append doc to existing documentation +"APPEND TO" +type CType { + "APPENT TO" + a: Duration + b: String @deprecated(reason: "APPEND TO") +} + +type QueryType { + # Add doc to method - args is currently not supported + findAB(args: InputType): AB + getAC: AC + listCs: CType + listEs: [AEnum] +} + +# Add doc to enums +enum AEnum { + E1 + E2 + E3 @deprecated(reason: "REPLACE") +} + +# Add doc to scalar +scalar Duration + +# Add doc to input type +input InputType { + a: String + b: String + c: String @deprecated(reason: "REPLACE") +} diff --git a/application/src/test/resources/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.graphql.expected b/application/src/test/resources/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.graphql.expected new file mode 100644 index 00000000000..47319e07ae0 --- /dev/null +++ b/application/src/test/resources/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.graphql.expected @@ -0,0 +1,95 @@ +schema { + query: QueryType +} + +"Marks the field, argument, input field or enum value as deprecated" +directive @deprecated( + "The reason for the deprecation" + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION + +"Directs the executor to include this field or fragment only when the `if` argument is true" +directive @include( + "Included when true." + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +"Indicates an Input Object is a OneOf Input Object." +directive @oneOf on INPUT_OBJECT + +"Directs the executor to skip this field or fragment when the `if` argument is true." +directive @skip( + "Skipped when true." + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +"Exposes a URL that specifies the behaviour of this scalar." +directive @specifiedBy( + "The URL that specifies the behaviour of this scalar." + url: String! + ) on SCALAR + +"AB.description" +union AB = AType | BType + +""" +APPEND TO + +AC.description.append +""" +union AC = AType | BType + +"AType.description" +type AType { + "AType.a.description" + a: Duration + b: String @deprecated(reason : "AType.b.deprecated") +} + +"BType.description" +type BType { + "BType.a.description" + a: String @deprecated(reason : "REPLACE") +} + +""" +APPEND TO + +CType.description.append +""" +type CType { + """ + APPENT TO + + CType.a.description.append + """ + a: Duration + b: String @deprecated(reason : "APPEND TO") +} + +type QueryType { + "QueryType.findAB.description" + findAB(args: InputType): AB + getAC: AC @deprecated(reason : "QueryType.getAC.deprecated") + listCs: CType + listEs: [AEnum] +} + +"AEnum.description" +enum AEnum { + "AEnum.E1.description" + E1 + E2 @deprecated(reason : "AEnum.E2.deprecated") + E3 @deprecated(reason : "REPLACE") +} + +"Duration.description" +scalar Duration + +"InputType.description" +input InputType { + "InputType.a.description" + a: String + b: String @deprecated(reason : "InputType.b.deprecated") + c: String @deprecated(reason : "REPLACE") +} diff --git a/application/src/test/resources/org/opentripplanner/graph_builder/module/moorgate.osm.pbf b/application/src/test/resources/org/opentripplanner/graph_builder/module/moorgate.osm.pbf new file mode 100644 index 00000000000..ee95a0e7c51 Binary files /dev/null and b/application/src/test/resources/org/opentripplanner/graph_builder/module/moorgate.osm.pbf differ diff --git a/client/package-lock.json b/client/package-lock.json index bb145274bd4..b82002a5b50 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -30,9 +30,9 @@ "@types/react": "19.0.4", "@types/react-dom": "19.0.2", "@vitejs/plugin-react": "4.3.4", - "@vitest/coverage-v8": "2.1.8", + "@vitest/coverage-v8": "3.0.2", "eslint": "9.18.0", - "eslint-config-prettier": "9.1.0", + "eslint-config-prettier": "10.0.1", "eslint-plugin-import": "2.31.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-react": "7.37.3", @@ -44,7 +44,7 @@ "typescript": "5.7.3", "typescript-eslint": "8.19.1", "vite": "6.0.7", - "vitest": "2.1.8" + "vitest": "3.0.2" } }, "node_modules/@ampproject/remapping": { @@ -1026,10 +1026,14 @@ } }, "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/@csstools/color-helpers": { "version": "5.0.1", @@ -4166,30 +4170,31 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.8.tgz", - "integrity": "sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.2.tgz", + "integrity": "sha512-U+hZYb0FtgNDb6B3E9piAHzXXIuxuBw2cd6Lvepc9sYYY4KjgiwCBmo3Sird9ZRu3ggLpLBTfw1ZRr77ipiSfw==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.7", + "@bcoe/v8-coverage": "^1.0.2", + "debug": "^4.4.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.12", + "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.8.0", "test-exclude": "^7.0.1", - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "2.1.8", - "vitest": "2.1.8" + "@vitest/browser": "3.0.2", + "vitest": "3.0.2" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -4198,64 +4203,96 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", - "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.2.tgz", + "integrity": "sha512-dKSHLBcoZI+3pmP5hiZ7I5grNru2HRtEW8Z5Zp4IXog8QYcxhlox7JUPyIIFWfN53+3HW3KPLIl6nSzUGgKSuQ==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.8", - "@vitest/utils": "2.1.8", + "@vitest/spy": "3.0.2", + "@vitest/utils": "3.0.2", "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.2.tgz", + "integrity": "sha512-Hr09FoBf0jlwwSyzIF4Xw31OntpO3XtZjkccpcBf8FeVW3tpiyKlkeUzxS/txzHqpUCNIX157NaTySxedyZLvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" }, "funding": { "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", - "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.2.tgz", + "integrity": "sha512-yBohcBw/T/p0/JRgYD+IYcjCmuHzjC3WLAKsVE4/LwiubzZkE8N49/xIQ/KGQwDRA8PaviF8IRO8JMWMngdVVQ==", "dev": true, + "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz", - "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.2.tgz", + "integrity": "sha512-GHEsWoncrGxWuW8s405fVoDfSLk6RF2LCXp6XhevbtDjdDme1WV/eNmUueDfpY1IX3MJaCRelVCEXsT9cArfEg==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.8", - "pathe": "^1.1.2" + "@vitest/utils": "3.0.2", + "pathe": "^2.0.1" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", - "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.2.tgz", + "integrity": "sha512-h9s67yD4+g+JoYG0zPCo/cLTabpDqzqNdzMawmNPzDStTiwxwkyYM1v5lWE8gmGv3SVJ2DcxA2NpQJZJv9ym3g==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.8", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "3.0.2", + "magic-string": "^0.30.17", + "pathe": "^2.0.1" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", - "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.2.tgz", + "integrity": "sha512-8mI2iUn+PJFMT44e3ISA1R+K6ALVs47W6eriDTfXe6lFqlflID05MB4+rIFhmDSLBj8iBsZkzBYlgSkinxLzSQ==", "dev": true, + "license": "MIT", "dependencies": { "tinyspy": "^3.0.2" }, @@ -4264,14 +4301,15 @@ } }, "node_modules/@vitest/utils": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", - "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.2.tgz", + "integrity": "sha512-Qu01ZYZlgHvDP02JnMBRpX43nRaZtNpIzw3C1clDXmn8eakgX6iQVGzTQ/NjkIr64WD8ioqOjkaYRVvHQI5qiw==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.8", + "@vitest/pretty-format": "3.0.2", "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -5992,12 +6030,13 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", + "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", "dev": true, + "license": "MIT", "bin": { - "eslint-config-prettier": "bin/cli.js" + "eslint-config-prettier": "build/bin/cli.js" }, "peerDependencies": { "eslint": ">=7.0.0" @@ -9055,10 +9094,11 @@ } }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", + "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "dev": true, + "license": "MIT" }, "node_modules/pathval": { "version": "2.0.0", @@ -10457,10 +10497,11 @@ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==" }, "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -10470,6 +10511,7 @@ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -10995,533 +11037,70 @@ } }, "node_modules/vite-node": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", - "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.2.tgz", + "integrity": "sha512-hsEQerBAHvVAbv40m3TFQe/lTEbOp7yDpyqMJqr2Tnd+W58+DEYOt+fluQgekOePcsNBmR77lpVAnIU2Xu4SvQ==", "dev": true, + "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.1", + "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], + "node_modules/vitest": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.2.tgz", + "integrity": "sha512-5bzaHakQ0hmVVKLhfh/jXf6oETDBtgPo8tQCHYB+wftNgFJ+Hah67IsWc8ivx4vFL025Ow8UiuTf4W57z4izvQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.0.2", + "@vitest/mocker": "3.0.2", + "@vitest/pretty-format": "^3.0.2", + "@vitest/runner": "3.0.2", + "@vitest/snapshot": "3.0.2", + "@vitest/spy": "3.0.2", + "@vitest/utils": "3.0.2", + "chai": "^5.1.2", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.1", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.2", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vite-node/node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", - "dev": true, - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", - "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", - "dev": true, - "dependencies": { - "@vitest/expect": "2.1.8", - "@vitest/mocker": "2.1.8", - "@vitest/pretty-format": "^2.1.8", - "@vitest/runner": "2.1.8", - "@vitest/snapshot": "2.1.8", - "@vitest/spy": "2.1.8", - "@vitest/utils": "2.1.8", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.8", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.8", - "@vitest/ui": "2.1.8", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.2", + "@vitest/ui": "3.0.2", "happy-dom": "*", "jsdom": "*" }, @@ -11546,497 +11125,6 @@ } } }, - "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", - "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", - "dev": true, - "dependencies": { - "@vitest/spy": "2.1.8", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vitest/node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", - "dev": true, - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, "node_modules/vt-pbf": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", diff --git a/client/package.json b/client/package.json index 78180de6b6a..f34ae361197 100644 --- a/client/package.json +++ b/client/package.json @@ -39,9 +39,9 @@ "@types/react": "19.0.4", "@types/react-dom": "19.0.2", "@vitejs/plugin-react": "4.3.4", - "@vitest/coverage-v8": "2.1.8", + "@vitest/coverage-v8": "3.0.2", "eslint": "9.18.0", - "eslint-config-prettier": "9.1.0", + "eslint-config-prettier": "10.0.1", "eslint-plugin-import": "2.31.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-react": "7.37.3", @@ -53,6 +53,6 @@ "typescript": "5.7.3", "typescript-eslint": "8.19.1", "vite": "6.0.7", - "vitest": "2.1.8" + "vitest": "3.0.2" } } 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 | diff --git a/doc/user/Changelog.md b/doc/user/Changelog.md index 5f4e1a054e2..9a5f2731733 100644 --- a/doc/user/Changelog.md +++ b/doc/user/Changelog.md @@ -72,6 +72,12 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Add a matcher API for filters in the transit service used for regularStop lookup [#6234](https://github.com/opentripplanner/OpenTripPlanner/pull/6234) - Make all polling updaters wait for graph update finish [#6262](https://github.com/opentripplanner/OpenTripPlanner/pull/6262) - When using ScheduledTransitLeg's copy builder, also copy alerts [#6368](https://github.com/opentripplanner/OpenTripPlanner/pull/6368) +- Process boarding location for OSM ways (linear platforms) [#6247](https://github.com/opentripplanner/OpenTripPlanner/pull/6247) +- Fix `bookWhen` field is `null` in the Transmodel API [#6385](https://github.com/opentripplanner/OpenTripPlanner/pull/6385) +- Make it possible to add custom API documentation based on the deployment location [#6355](https://github.com/opentripplanner/OpenTripPlanner/pull/6355) +- If configured, add subway station entrances from OSM to walk steps [#6343](https://github.com/opentripplanner/OpenTripPlanner/pull/6343) +- Revert allow multiple states during transfer edge traversals [#6357](https://github.com/opentripplanner/OpenTripPlanner/pull/6357) +- Generate Raptor transfer cache in parallel [#6326](https://github.com/opentripplanner/OpenTripPlanner/pull/6326) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.6.0 (2024-09-18) diff --git a/doc/user/RouterConfiguration.md b/doc/user/RouterConfiguration.md index 82d14f36392..b5cbf15a4a5 100644 --- a/doc/user/RouterConfiguration.md +++ b/doc/user/RouterConfiguration.md @@ -31,45 +31,46 @@ A full list of them can be found in the [RouteRequest](RouteRequest.md). -| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | -|-------------------------------------------------------------------------------------------|:---------------------:|-------------------------------------------------------------------------------------------------------|:----------:|---------------|:-----:| -| [configVersion](#configVersion) | `string` | Deployment version of the *router-config.json*. | *Optional* | | 2.1 | -| [flex](sandbox/Flex.md) | `object` | Configuration for flex routing. | *Optional* | | 2.1 | -| [rideHailingServices](sandbox/RideHailing.md) | `object[]` | Configuration for interfaces to external ride hailing services like Uber. | *Optional* | | 2.3 | -| [routingDefaults](RouteRequest.md) | `object` | The default parameters for the routing query. | *Optional* | | 2.0 | -| [server](#server) | `object` | Configuration for router server. | *Optional* | | 2.4 | -|    [apiProcessingTimeout](#server_apiProcessingTimeout) | `duration` | Maximum processing time for an API request | *Optional* | `"PT-1S"` | 2.4 | -|    [traceParameters](#server_traceParameters) | `object[]` | Trace OTP request using HTTP request/response parameter(s) combined with logging. | *Optional* | | 2.4 | -|          generateIdIfMissing | `boolean` | If `true` a unique value is generated if no http request header is provided, or the value is missing. | *Optional* | `false` | 2.4 | -|          httpRequestHeader | `string` | The header-key to use when fetching the trace parameter value | *Optional* | | 2.4 | -|          httpResponseHeader | `string` | The header-key to use when saving the value back into the http response | *Optional* | | 2.4 | -|          [logKey](#server_traceParameters_0_logKey) | `string` | The log event key used. | *Optional* | | 2.4 | -| timetableUpdates | `object` | Global configuration for timetable updaters. | *Optional* | | 2.2 | -|    [maxSnapshotFrequency](#timetableUpdates_maxSnapshotFrequency) | `duration` | How long a snapshot should be cached. | *Optional* | `"PT1S"` | 2.2 | -|    purgeExpiredData | `boolean` | Should expired real-time data be purged from the graph. Apply to GTFS-RT and Siri updates. | *Optional* | `true` | 2.2 | -| [transit](#transit) | `object` | Configuration for transit searches with RAPTOR. | *Optional* | | na | -|    [iterationDepartureStepInSeconds](#transit_iterationDepartureStepInSeconds) | `integer` | Step for departure times between each RangeRaptor iterations. | *Optional* | `60` | na | -|    [maxNumberOfTransfers](#transit_maxNumberOfTransfers) | `integer` | This parameter is used to allocate enough memory space for Raptor. | *Optional* | `12` | na | -|    [maxSearchWindow](#transit_maxSearchWindow) | `duration` | Upper limit of the request parameter searchWindow. | *Optional* | `"PT24H"` | 2.4 | -|    [scheduledTripBinarySearchThreshold](#transit_scheduledTripBinarySearchThreshold) | `integer` | This threshold is used to determine when to perform a binary trip schedule search. | *Optional* | `50` | na | -|    [searchThreadPoolSize](#transit_searchThreadPoolSize) | `integer` | Split a travel search in smaller jobs and run them in parallel to improve performance. | *Optional* | `0` | na | -|    [transferCacheMaxSize](#transit_transferCacheMaxSize) | `integer` | The maximum number of distinct transfers parameters to cache pre-calculated transfers for. | *Optional* | `25` | na | -|    [dynamicSearchWindow](#transit_dynamicSearchWindow) | `object` | The dynamic search window coefficients used to calculate the EDT, LAT and SW. | *Optional* | | 2.1 | -|       [maxWindow](#transit_dynamicSearchWindow_maxWindow) | `duration` | Upper limit for the search-window calculation. | *Optional* | `"PT3H"` | 2.2 | -|       [minTransitTimeCoefficient](#transit_dynamicSearchWindow_minTransitTimeCoefficient) | `double` | The coefficient to multiply with `minTransitTime`. | *Optional* | `0.5` | 2.1 | -|       [minWaitTimeCoefficient](#transit_dynamicSearchWindow_minWaitTimeCoefficient) | `double` | The coefficient to multiply with `minWaitTime`. | *Optional* | `0.5` | 2.1 | -|       [minWindow](#transit_dynamicSearchWindow_minWindow) | `duration` | The constant minimum duration for a raptor-search-window. | *Optional* | `"PT40M"` | 2.2 | -|       [stepMinutes](#transit_dynamicSearchWindow_stepMinutes) | `integer` | Used to set the steps the search-window is rounded to. | *Optional* | `10` | 2.1 | -|    [pagingSearchWindowAdjustments](#transit_pagingSearchWindowAdjustments) | `duration[]` | The provided array of durations is used to increase the search-window for the next/previous page. | *Optional* | | na | -|    [stopBoardAlightDuringTransferCost](#transit_stopBoardAlightDuringTransferCost) | `enum map of integer` | Costs for boarding and alighting during transfers at stops with a given transfer priority. | *Optional* | | 2.0 | -|    [transferCacheRequests](#transit_transferCacheRequests) | `object[]` | Routing requests to use for pre-filling the stop-to-stop transfer cache. | *Optional* | | 2.3 | -| transmodelApi | `object` | Configuration for the Transmodel GraphQL API. | *Optional* | | 2.1 | -|    [hideFeedId](#transmodelApi_hideFeedId) | `boolean` | Hide the FeedId in all API output, and add it to input. | *Optional* | `false` | na | -|    [maxNumberOfResultFields](#transmodelApi_maxNumberOfResultFields) | `integer` | The maximum number of fields in a GraphQL result | *Optional* | `1000000` | 2.6 | -|    [tracingHeaderTags](#transmodelApi_tracingHeaderTags) | `string[]` | Used to group requests when monitoring OTP. | *Optional* | | na | -| [updaters](UpdaterConfig.md) | `object[]` | Configuration for the updaters that import various types of data into OTP. | *Optional* | | 1.5 | -| [vectorTiles](sandbox/MapboxVectorTilesApi.md) | `object` | Vector tile configuration | *Optional* | | na | -| [vehicleRentalServiceDirectory](sandbox/VehicleRentalServiceDirectory.md) | `object` | Configuration for the vehicle rental service directory. | *Optional* | | 2.0 | +| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | +|-------------------------------------------------------------------------------------------|:---------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------:|---------------|:-----:| +| [configVersion](#configVersion) | `string` | Deployment version of the *router-config.json*. | *Optional* | | 2.1 | +| [flex](sandbox/Flex.md) | `object` | Configuration for flex routing. | *Optional* | | 2.1 | +| [rideHailingServices](sandbox/RideHailing.md) | `object[]` | Configuration for interfaces to external ride hailing services like Uber. | *Optional* | | 2.3 | +| [routingDefaults](RouteRequest.md) | `object` | The default parameters for the routing query. | *Optional* | | 2.0 | +| [server](#server) | `object` | Configuration for router server. | *Optional* | | 2.4 | +|    [apiDocumentationProfile](#server_apiDocumentationProfile) | `enum` | List of available custom documentation profiles. A profile is used to inject custom documentation like type and field description or a deprecated reason. Currently, ONLY the Transmodel API supports this feature. | *Optional* | `"default"` | 2.7 | +|    [apiProcessingTimeout](#server_apiProcessingTimeout) | `duration` | Maximum processing time for an API request | *Optional* | `"PT-1S"` | 2.4 | +|    [traceParameters](#server_traceParameters) | `object[]` | Trace OTP request using HTTP request/response parameter(s) combined with logging. | *Optional* | | 2.4 | +|          generateIdIfMissing | `boolean` | If `true` a unique value is generated if no http request header is provided, or the value is missing. | *Optional* | `false` | 2.4 | +|          httpRequestHeader | `string` | The header-key to use when fetching the trace parameter value | *Optional* | | 2.4 | +|          httpResponseHeader | `string` | The header-key to use when saving the value back into the http response | *Optional* | | 2.4 | +|          [logKey](#server_traceParameters_0_logKey) | `string` | The log event key used. | *Optional* | | 2.4 | +| timetableUpdates | `object` | Global configuration for timetable updaters. | *Optional* | | 2.2 | +|    [maxSnapshotFrequency](#timetableUpdates_maxSnapshotFrequency) | `duration` | How long a snapshot should be cached. | *Optional* | `"PT1S"` | 2.2 | +|    purgeExpiredData | `boolean` | Should expired real-time data be purged from the graph. Apply to GTFS-RT and Siri updates. | *Optional* | `true` | 2.2 | +| [transit](#transit) | `object` | Configuration for transit searches with RAPTOR. | *Optional* | | na | +|    [iterationDepartureStepInSeconds](#transit_iterationDepartureStepInSeconds) | `integer` | Step for departure times between each RangeRaptor iterations. | *Optional* | `60` | na | +|    [maxNumberOfTransfers](#transit_maxNumberOfTransfers) | `integer` | This parameter is used to allocate enough memory space for Raptor. | *Optional* | `12` | na | +|    [maxSearchWindow](#transit_maxSearchWindow) | `duration` | Upper limit of the request parameter searchWindow. | *Optional* | `"PT24H"` | 2.4 | +|    [scheduledTripBinarySearchThreshold](#transit_scheduledTripBinarySearchThreshold) | `integer` | This threshold is used to determine when to perform a binary trip schedule search. | *Optional* | `50` | na | +|    [searchThreadPoolSize](#transit_searchThreadPoolSize) | `integer` | Split a travel search in smaller jobs and run them in parallel to improve performance. | *Optional* | `0` | na | +|    [transferCacheMaxSize](#transit_transferCacheMaxSize) | `integer` | The maximum number of distinct transfers parameters to cache pre-calculated transfers for. | *Optional* | `25` | na | +|    [dynamicSearchWindow](#transit_dynamicSearchWindow) | `object` | The dynamic search window coefficients used to calculate the EDT, LAT and SW. | *Optional* | | 2.1 | +|       [maxWindow](#transit_dynamicSearchWindow_maxWindow) | `duration` | Upper limit for the search-window calculation. | *Optional* | `"PT3H"` | 2.2 | +|       [minTransitTimeCoefficient](#transit_dynamicSearchWindow_minTransitTimeCoefficient) | `double` | The coefficient to multiply with `minTransitTime`. | *Optional* | `0.5` | 2.1 | +|       [minWaitTimeCoefficient](#transit_dynamicSearchWindow_minWaitTimeCoefficient) | `double` | The coefficient to multiply with `minWaitTime`. | *Optional* | `0.5` | 2.1 | +|       [minWindow](#transit_dynamicSearchWindow_minWindow) | `duration` | The constant minimum duration for a raptor-search-window. | *Optional* | `"PT40M"` | 2.2 | +|       [stepMinutes](#transit_dynamicSearchWindow_stepMinutes) | `integer` | Used to set the steps the search-window is rounded to. | *Optional* | `10` | 2.1 | +|    [pagingSearchWindowAdjustments](#transit_pagingSearchWindowAdjustments) | `duration[]` | The provided array of durations is used to increase the search-window for the next/previous page. | *Optional* | | na | +|    [stopBoardAlightDuringTransferCost](#transit_stopBoardAlightDuringTransferCost) | `enum map of integer` | Costs for boarding and alighting during transfers at stops with a given transfer priority. | *Optional* | | 2.0 | +|    [transferCacheRequests](#transit_transferCacheRequests) | `object[]` | Routing requests to use for pre-filling the stop-to-stop transfer cache. | *Optional* | | 2.3 | +| transmodelApi | `object` | Configuration for the Transmodel GraphQL API. | *Optional* | | 2.1 | +|    [hideFeedId](#transmodelApi_hideFeedId) | `boolean` | Hide the FeedId in all API output, and add it to input. | *Optional* | `false` | na | +|    [maxNumberOfResultFields](#transmodelApi_maxNumberOfResultFields) | `integer` | The maximum number of fields in a GraphQL result | *Optional* | `1000000` | 2.6 | +|    [tracingHeaderTags](#transmodelApi_tracingHeaderTags) | `string[]` | Used to group requests when monitoring OTP. | *Optional* | | na | +| [updaters](UpdaterConfig.md) | `object[]` | Configuration for the updaters that import various types of data into OTP. | *Optional* | | 1.5 | +| [vectorTiles](sandbox/MapboxVectorTilesApi.md) | `object` | Vector tile configuration | *Optional* | | na | +| [vehicleRentalServiceDirectory](sandbox/VehicleRentalServiceDirectory.md) | `object` | Configuration for the vehicle rental service directory. | *Optional* | | 2.0 | @@ -108,6 +109,22 @@ These parameters are used to configure the router server. Many parameters are sp domain, these are set in the routing request. +

apiDocumentationProfile

+ +**Since version:** `2.7` ∙ **Type:** `enum` ∙ **Cardinality:** `Optional` ∙ **Default value:** `"default"` +**Path:** /server +**Enum values:** `default` | `entur` + +List of available custom documentation profiles. A profile is used to inject custom +documentation like type and field description or a deprecated reason. + +Currently, ONLY the Transmodel API supports this feature. + + + - `default` Default documentation is used. + - `entur` Entur specific documentation. This deprecate features not supported at Entur, Norway. + +

apiProcessingTimeout

**Since version:** `2.4` ∙ **Type:** `duration` ∙ **Cardinality:** `Optional` ∙ **Default value:** `"PT-1S"` diff --git a/pom.xml b/pom.xml index e68b6ae48a0..3a7d7e3703e 100644 --- a/pom.xml +++ b/pom.xml @@ -58,11 +58,11 @@ - 176 + 177 32.1 - 2.53 + 2.54 2.18.2 4.0.5 3.1.10 diff --git a/renovate.json5 b/renovate.json5 index 100a91d959a..30a5c6a99c1 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -104,6 +104,7 @@ "io.micrometer:micrometer-registry-influx", "com.google.protobuf:protobuf-java" ], + "matchUpdateTypes": ["major", "minor"], "schedule": "on the 7th through 8th day of the month" }, { @@ -144,11 +145,19 @@ "schedule": "on the 4th day of the month" }, { - "groupName": "Low-risk dependencies (patch)", + "groupName": "highly trusted dependencies (patch)", "matchUpdateTypes": ["patch"], "schedule": ["on the 27th day of the month"], "matchPackageNames": [ + "org.onebusaway:onebusaway-gtfs", "org.glassfish.jersey.{/,}**", + "com.google.guava:guava", + "com.google.cloud:libraries-bom", + "com.google.protobuf:protobuf-java", + "io.micrometer:micrometer-registry-prometheus", + "io.micrometer:micrometer-registry-influx", + "com.fasterxml.jackson:{/,}**", + "com.fasterxml.jackson.datatype::{/,}**" ] }, { diff --git a/utils/src/main/java/org/opentripplanner/utils/text/TextVariablesSubstitution.java b/utils/src/main/java/org/opentripplanner/utils/text/TextVariablesSubstitution.java new file mode 100644 index 00000000000..95226ed4bd0 --- /dev/null +++ b/utils/src/main/java/org/opentripplanner/utils/text/TextVariablesSubstitution.java @@ -0,0 +1,102 @@ +package org.opentripplanner.utils.text; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This utility class substitute variable placeholders in a given text on the format ${variable}. + * + * The pattern matching a placeholder must start with '${' and end with '}'. The variable name + * must consist of only alphanumerical characters (a-z, A-Z, 0-9), dot `.` and underscore '_'. + */ +public class TextVariablesSubstitution { + + private static final Pattern PATTERN = Pattern.compile("\\$\\{([.\\w]+)}"); + + /** + * This method uses the {@link #insertVariables(String, Function, Consumer)} to substitute + * all variable tokens in all values in the given {@code properties}. It supports nesting, but + * you must avoid cyclic references. + *

+ * Example: + *

+   *   a -> My car is a ${b} car, with an ${c} look.
+   *   b -> good old ${c}
+   *   c -> fancy
+   * 
+ * This will resolve to: + *
+   *   a -> My car is a good old fancy car, with an fancy look.
+   *   b -> good old fancy
+   *   c -> fancy
+   * 
+ */ + public static Map insertVariables( + Map properties, + Consumer errorHandler + ) { + var result = new HashMap(properties); + + for (String key : result.keySet()) { + var value = result.get(key); + var sub = insertVariables(value, result::get, errorHandler); + if (!value.equals(sub)) { + result.put(key, sub); + } + } + return result; + } + + /** + * Replace all variables({@code ${variable.name}}) in the given {@code text}. The given + * {@code variableProvider} is used to look up values to insert into the text replacing the + * variable token. + * + * @param errorHandler The error handler is called if a variable key does not exist in the + * {@code variableProvider}. + * @return the new value with all variables replaced. + */ + public static String insertVariables( + String text, + Function variableProvider, + Consumer errorHandler + ) { + return insert(text, PATTERN.matcher(text), variableProvider, errorHandler); + } + + private static String insert( + String text, + Matcher matcher, + Function variableProvider, + Consumer errorHandler + ) { + boolean matchFound = matcher.find(); + if (!matchFound) { + return text; + } + + Map substitutions = new HashMap<>(); + + while (matchFound) { + String subKey = matcher.group(0); + String nameOnly = matcher.group(1); + if (!substitutions.containsKey(nameOnly)) { + String value = variableProvider.apply(nameOnly); + if (value != null) { + substitutions.put(subKey, value); + } else { + errorHandler.accept(nameOnly); + } + } + matchFound = matcher.find(); + } + for (Map.Entry entry : substitutions.entrySet()) { + text = text.replace(entry.getKey(), entry.getValue()); + } + return insert(text, PATTERN.matcher(text), variableProvider, errorHandler); + } +} diff --git a/utils/src/test/java/org/opentripplanner/utils/text/TextVariablesSubstitutionTest.java b/utils/src/test/java/org/opentripplanner/utils/text/TextVariablesSubstitutionTest.java new file mode 100644 index 00000000000..5c1c2014cc2 --- /dev/null +++ b/utils/src/test/java/org/opentripplanner/utils/text/TextVariablesSubstitutionTest.java @@ -0,0 +1,56 @@ +package org.opentripplanner.utils.text; + +import static java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.opentripplanner.utils.text.TextVariablesSubstitution.insertVariables; + +import java.util.Map; +import org.junit.jupiter.api.Test; + +class TextVariablesSubstitutionTest { + + @Test + void testInsertVariablesInProperties() { + Map map = Map.ofEntries( + entry("a", "A"), + entry("b", "B"), + entry("ab", "${a}${b}"), + entry("ab2", "${ab} - ${a} - ${b}") + ); + + var result = insertVariables(map, this::errorHandler); + + assertEquals("A", result.get("a")); + assertEquals("B", result.get("b")); + assertEquals("AB", result.get("ab")); + assertEquals("AB - A - B", result.get("ab2")); + } + + @Test + void testInsertVariablesInValue() { + var map = Map.ofEntries( + entry("a", "A"), + entry("b", "B"), + entry("ab", "${a}${b}"), + entry("ab2", "${ab} - ${a} - ${b}") + ); + + assertEquals( + "No substitution", + insertVariables("No substitution", map::get, this::errorHandler) + ); + assertEquals("A B", insertVariables("${a} ${b}", map::get, this::errorHandler)); + assertEquals("AB", insertVariables("${ab}", map::get, this::errorHandler)); + assertEquals("AB - A - B", insertVariables("${ab2}", map::get, this::errorHandler)); + var ex = assertThrows( + IllegalArgumentException.class, + () -> insertVariables("${c}", map::get, this::errorHandler) + ); + assertEquals("c", ex.getMessage()); + } + + private void errorHandler(String name) { + throw new IllegalArgumentException(name); + } +}