From 1d19a052a8bf2523c3af537edfc92fe9a72e8c62 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Wed, 18 Sep 2024 17:22:19 +0300 Subject: [PATCH 001/106] Add subway station entrances to walk steps --- .../apis/gtfs/datafetchers/stepImpl.java | 5 +++++ .../gtfs/generated/GraphQLDataFetchers.java | 11 +++++++++++ .../apis/gtfs/generated/GraphQLTypes.java | 1 + .../module/osm/VertexGenerator.java | 9 +++++++++ .../opentripplanner/model/plan/WalkStep.java | 10 ++++++++++ .../model/plan/WalkStepBuilder.java | 7 +++++++ .../openstreetmap/model/OSMNode.java | 9 +++++++++ .../mapping/StatesToWalkStepsMapper.java | 18 ++++++++++++++++++ .../model/vertex/StationEntranceVertex.java | 19 +++++++++++++++++++ .../street/model/vertex/VertexFactory.java | 9 +++++++++ .../opentripplanner/apis/gtfs/schema.graphqls | 2 ++ 11 files changed, 100 insertions(+) create mode 100644 src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/stepImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/stepImpl.java index 6bd51ae5f29..d79e224e51e 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/stepImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/stepImpl.java @@ -53,6 +53,11 @@ public DataFetcher exit() { return environment -> getSource(environment).getExit(); } + @Override + public DataFetcher entrance() { + return environment -> getSource(environment).getEntrance(); + } + @Override public DataFetcher lat() { return environment -> getSource(environment).getStartLocation().latitude(); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java index 67944543580..3c162b14112 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java @@ -1,9 +1,11 @@ //THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. package org.opentripplanner.apis.gtfs.generated; +import graphql.relay.Connection; import graphql.relay.Connection; import graphql.relay.DefaultEdge; import graphql.relay.Edge; +import graphql.relay.Edge; import graphql.schema.DataFetcher; import graphql.schema.TypeResolver; import java.util.Currency; @@ -24,8 +26,12 @@ import org.opentripplanner.apis.gtfs.model.FeedPublisher; import org.opentripplanner.apis.gtfs.model.PlanPageInfo; import org.opentripplanner.apis.gtfs.model.RideHailingProvider; +import org.opentripplanner.apis.gtfs.model.RouteTypeModel; +import org.opentripplanner.apis.gtfs.model.StopOnRouteModel; +import org.opentripplanner.apis.gtfs.model.StopOnTripModel; import org.opentripplanner.apis.gtfs.model.StopPosition; import org.opentripplanner.apis.gtfs.model.TripOccupancy; +import org.opentripplanner.apis.gtfs.model.UnknownModel; import org.opentripplanner.ext.fares.model.FareRuleSet; import org.opentripplanner.ext.ridehailing.model.RideEstimate; import org.opentripplanner.model.StopTimesInPattern; @@ -48,6 +54,8 @@ import org.opentripplanner.routing.graphfinder.PatternAtStop; import org.opentripplanner.routing.graphfinder.PlaceAtDistance; import org.opentripplanner.routing.vehicle_parking.VehicleParking; +import org.opentripplanner.routing.vehicle_parking.VehicleParking; +import org.opentripplanner.routing.vehicle_parking.VehicleParking; import org.opentripplanner.routing.vehicle_parking.VehicleParkingSpaces; import org.opentripplanner.routing.vehicle_parking.VehicleParkingState; import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; @@ -58,6 +66,7 @@ import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris; +import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris; import org.opentripplanner.service.vehiclerental.model.VehicleRentalSystem; import org.opentripplanner.service.vehiclerental.model.VehicleRentalVehicle; import org.opentripplanner.transit.model.basic.Money; @@ -1419,6 +1428,8 @@ public interface GraphQLStep { public DataFetcher> elevationProfile(); + public DataFetcher entrance(); + public DataFetcher exit(); public DataFetcher lat(); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java index 67051444cdf..8edc0cce870 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java @@ -1,6 +1,7 @@ // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. package org.opentripplanner.apis.gtfs.generated; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java index 14489777dd4..df9c4376871 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java @@ -95,6 +95,15 @@ IntersectionVertex getVertexForOsmNode(OSMNode node, OSMWithTags way) { iv = bv; } + if (node.isSubwayEntrance()) { + String ref = node.getTag("ref"); + if (ref != null) { + iv = vertexFactory.stationEntrance(nid, coordinate, ref); + } else { + iv = vertexFactory.stationEntrance(nid, coordinate, "MAIN_ENTRANCE"); + } + } + if (iv == null) { iv = vertexFactory.osm( diff --git a/src/main/java/org/opentripplanner/model/plan/WalkStep.java b/src/main/java/org/opentripplanner/model/plan/WalkStep.java index 13249d5da52..9a650ecef3e 100644 --- a/src/main/java/org/opentripplanner/model/plan/WalkStep.java +++ b/src/main/java/org/opentripplanner/model/plan/WalkStep.java @@ -44,6 +44,7 @@ public final class WalkStep { private final boolean walkingBike; private final String exit; + private final String entrance; private final ElevationProfile elevationProfile; private final boolean stayOn; @@ -56,6 +57,7 @@ public final class WalkStep { I18NString directionText, Set streetNotes, String exit, + String entrance, ElevationProfile elevationProfile, boolean bogusName, boolean walkingBike, @@ -76,6 +78,7 @@ public final class WalkStep { this.walkingBike = walkingBike; this.area = area; this.exit = exit; + this.entrance = entrance; this.elevationProfile = elevationProfile; this.stayOn = stayOn; this.edges = List.copyOf(Objects.requireNonNull(edges)); @@ -130,6 +133,13 @@ public String getExit() { return exit; } + /** + * When entering or exiting a public transport station, the entrance name + */ + public String getEntrance() { + return entrance; + } + /** * Indicates whether a street changes direction at an intersection. */ diff --git a/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java b/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java index 25c6ee25b6b..8b5d2cedb11 100644 --- a/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java +++ b/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java @@ -25,6 +25,7 @@ public class WalkStepBuilder { private RelativeDirection relativeDirection; private ElevationProfile elevationProfile; private String exit; + private String entrance; private boolean stayOn = false; /** * Distance used for appending elevation profiles @@ -74,6 +75,11 @@ public WalkStepBuilder withExit(String exit) { return this; } + public WalkStepBuilder withEntrance(String entrance) { + this.entrance = entrance; + return this; + } + public WalkStepBuilder withStayOn(boolean stayOn) { this.stayOn = stayOn; return this; @@ -156,6 +162,7 @@ public WalkStep build() { directionText, streetNotes, exit, + entrance, elevationProfile, bogusName, walkingBike, diff --git a/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java b/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java index d181cde4564..371751d5e4a 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java +++ b/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java @@ -63,6 +63,15 @@ public boolean isBarrier() { ); } + /** + * Checks if this node is an subway station entrance + * + * @return true if it does + */ + 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/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java index 94905bb840a..d49755bb548 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java @@ -26,6 +26,7 @@ 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; @@ -258,6 +259,8 @@ private void processState(State backState, State forwardState) { setMotorwayExit(backState); + setStationEntrance(backState); + if (createdNewStep && !modeTransition) { // check last three steps for zag int lastIndex = steps.size() - 1; @@ -380,6 +383,21 @@ private void setMotorwayExit(State backState) { } } + /** + * Update the walk step with the name of the station entrance if set from OSM + */ + private void setStationEntrance(State backState) { + State entranceState = backState; + Edge entranceEdge = entranceState.getBackEdge(); + while (entranceEdge instanceof FreeEdge) { + entranceState = entranceState.getBackState(); + entranceEdge = entranceState.getBackEdge(); + } + if (entranceState.getVertex() instanceof StationEntranceVertex) { + current.withEntrance(((StationEntranceVertex) entranceState.getVertex()).getEntranceName()); + } + } + /** * Is it possible to turn to another street from this previous state */ diff --git a/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java b/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java new file mode 100644 index 00000000000..af1e824a7bd --- /dev/null +++ b/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java @@ -0,0 +1,19 @@ +package org.opentripplanner.street.model.vertex; + +public class StationEntranceVertex extends OsmVertex { + + private final String entranceName; + + public StationEntranceVertex(double x, double y, long nodeId, String entranceName) { + super(x, y, nodeId); + this.entranceName = entranceName; + } + + public String getEntranceName() { + return entranceName; + } + + public String toString() { + return "StationEntranceVertex(" + super.toString() + ")"; + } +} diff --git a/src/main/java/org/opentripplanner/street/model/vertex/VertexFactory.java b/src/main/java/org/opentripplanner/street/model/vertex/VertexFactory.java index c7e38ca0032..3fe201070d4 100644 --- a/src/main/java/org/opentripplanner/street/model/vertex/VertexFactory.java +++ b/src/main/java/org/opentripplanner/street/model/vertex/VertexFactory.java @@ -103,6 +103,15 @@ public ExitVertex exit(long nid, Coordinate coordinate, String exitName) { return addToGraph(new ExitVertex(coordinate.x, coordinate.y, nid, exitName)); } + @Nonnull + public StationEntranceVertex stationEntrance( + long nid, + Coordinate coordinate, + String entranceName + ) { + return addToGraph(new StationEntranceVertex(coordinate.x, coordinate.y, nid, entranceName)); + } + @Nonnull public OsmVertex osm( Coordinate coordinate, diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 927af19f8b1..12decc1e3d5 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -2628,6 +2628,8 @@ type step { elevationProfile: [elevationProfileComponent] "When exiting a highway or traffic circle, the exit name/number." exit: String + "Name of entrance to a public transport station" + entrance: String "The latitude of the start of the step." lat: Float "The longitude of the start of the step." From 67f4b1b54ef64b40c0853bfd8e5d10a172534e72 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Wed, 2 Oct 2024 16:03:23 +0300 Subject: [PATCH 002/106] Add entity to walk steps --- .../apis/gtfs/GtfsGraphQLIndex.java | 2 ++ .../apis/gtfs/datafetchers/stepImpl.java | 4 ++-- .../gtfs/generated/GraphQLDataFetchers.java | 10 +++++++++- .../apis/gtfs/model/StepEntity.java | 19 +++++++++++++++++++ .../opentripplanner/model/plan/WalkStep.java | 13 +++++++------ .../model/plan/WalkStepBuilder.java | 10 ++++++---- .../mapping/StatesToWalkStepsMapper.java | 5 ++++- .../opentripplanner/apis/gtfs/schema.graphqls | 13 +++++++++++-- 8 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/opentripplanner/apis/gtfs/model/StepEntity.java diff --git a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java index 43a8399e70c..ed3242b5823 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java @@ -81,6 +81,7 @@ import org.opentripplanner.apis.gtfs.datafetchers.serviceTimeRangeImpl; import org.opentripplanner.apis.gtfs.datafetchers.stepImpl; import org.opentripplanner.apis.gtfs.datafetchers.stopAtDistanceImpl; +import org.opentripplanner.apis.gtfs.model.StepEntity; import org.opentripplanner.apis.gtfs.model.StopPosition; import org.opentripplanner.apis.support.graphql.LoggingDataFetcherExceptionHandler; import org.opentripplanner.ext.actuator.MicrometerGraphQLInstrumentation; @@ -124,6 +125,7 @@ protected static GraphQLSchema buildSchema() { .type("Node", type -> type.typeResolver(new NodeTypeResolver())) .type("PlaceInterface", type -> type.typeResolver(new PlaceInterfaceTypeResolver())) .type("StopPosition", type -> type.typeResolver(new StopPosition() {})) + .type("StepEntity", type -> type.typeResolver(new StepEntity() {})) .type("FareProduct", type -> type.typeResolver(new FareProductTypeResolver())) .type("AlertEntity", type -> type.typeResolver(new AlertEntityTypeResolver())) .type(typeWiring.build(AgencyImpl.class)) diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/stepImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/stepImpl.java index d79e224e51e..658f2d321ad 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/stepImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/stepImpl.java @@ -54,8 +54,8 @@ public DataFetcher exit() { } @Override - public DataFetcher entrance() { - return environment -> getSource(environment).getEntrance(); + public DataFetcher entity() { + return environment -> getSource(environment).getEntity(); } @Override diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java index 3c162b14112..efc59f72d42 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java @@ -365,6 +365,11 @@ public interface GraphQLEmissions { public DataFetcher co2(); } + /** Station entrance/exit */ + public interface GraphQLEntrance { + public DataFetcher name(); + } + /** A 'medium' that a fare product applies to, for example cash, 'Oyster Card' or 'DB Navigator App'. */ public interface GraphQLFareMedium { public DataFetcher id(); @@ -977,6 +982,9 @@ public interface GraphQLRoutingError { public DataFetcher inputField(); } + /** Entity to a step */ + public interface GraphQLStepEntity 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. @@ -1428,7 +1436,7 @@ public interface GraphQLStep { public DataFetcher> elevationProfile(); - public DataFetcher entrance(); + public DataFetcher entity(); public DataFetcher exit(); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/model/StepEntity.java b/src/main/java/org/opentripplanner/apis/gtfs/model/StepEntity.java new file mode 100644 index 00000000000..5e10f4e08a6 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/model/StepEntity.java @@ -0,0 +1,19 @@ +package org.opentripplanner.apis.gtfs.model; + +import graphql.TypeResolutionEnvironment; +import graphql.schema.GraphQLObjectType; +import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; + +public interface StepEntity extends GraphQLDataFetchers.GraphQLStepEntity { + record Entrance(String name) implements StepEntity {} + + @Override + default GraphQLObjectType getType(TypeResolutionEnvironment env) { + var schema = env.getSchema(); + Object o = env.getObject(); + if (o instanceof Entrance) { + return schema.getObjectType("Entrance"); + } + return null; + } +} diff --git a/src/main/java/org/opentripplanner/model/plan/WalkStep.java b/src/main/java/org/opentripplanner/model/plan/WalkStep.java index 9a650ecef3e..3e9e9fe14dc 100644 --- a/src/main/java/org/opentripplanner/model/plan/WalkStep.java +++ b/src/main/java/org/opentripplanner/model/plan/WalkStep.java @@ -4,6 +4,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import org.opentripplanner.apis.gtfs.model.StepEntity; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.lang.DoubleUtils; @@ -44,7 +45,7 @@ public final class WalkStep { private final boolean walkingBike; private final String exit; - private final String entrance; + private final StepEntity entity; private final ElevationProfile elevationProfile; private final boolean stayOn; @@ -57,7 +58,7 @@ public final class WalkStep { I18NString directionText, Set streetNotes, String exit, - String entrance, + StepEntity entity, ElevationProfile elevationProfile, boolean bogusName, boolean walkingBike, @@ -78,7 +79,7 @@ public final class WalkStep { this.walkingBike = walkingBike; this.area = area; this.exit = exit; - this.entrance = entrance; + this.entity = entity; this.elevationProfile = elevationProfile; this.stayOn = stayOn; this.edges = List.copyOf(Objects.requireNonNull(edges)); @@ -134,10 +135,10 @@ public String getExit() { } /** - * When entering or exiting a public transport station, the entrance name + * Entity related to a step e.g. building entrance/exit. */ - public String getEntrance() { - return entrance; + public Object getEntity() { + return entity; } /** diff --git a/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java b/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java index 8b5d2cedb11..d020f8d0113 100644 --- a/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java +++ b/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java @@ -5,6 +5,8 @@ import java.util.List; import java.util.Set; import javax.annotation.Nullable; +import org.opentripplanner.apis.gtfs.model.StepEntity; +import org.opentripplanner.apis.gtfs.model.StepEntity.Entrance; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.lang.DoubleUtils; @@ -25,7 +27,7 @@ public class WalkStepBuilder { private RelativeDirection relativeDirection; private ElevationProfile elevationProfile; private String exit; - private String entrance; + private StepEntity entity; private boolean stayOn = false; /** * Distance used for appending elevation profiles @@ -75,8 +77,8 @@ public WalkStepBuilder withExit(String exit) { return this; } - public WalkStepBuilder withEntrance(String entrance) { - this.entrance = entrance; + public WalkStepBuilder withEntrance(Entrance entrance) { + this.entity = entrance; return this; } @@ -162,7 +164,7 @@ public WalkStep build() { directionText, streetNotes, exit, - entrance, + entity, elevationProfile, bogusName, walkingBike, diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java index d49755bb548..7e0ff3696ca 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java @@ -10,6 +10,7 @@ import javax.annotation.Nonnull; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; +import org.opentripplanner.apis.gtfs.model.StepEntity.Entrance; import org.opentripplanner.framework.geometry.DirectionUtils; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; @@ -394,7 +395,9 @@ private void setStationEntrance(State backState) { entranceEdge = entranceState.getBackEdge(); } if (entranceState.getVertex() instanceof StationEntranceVertex) { - current.withEntrance(((StationEntranceVertex) entranceState.getVertex()).getEntranceName()); + current.withEntrance( + new Entrance(((StationEntranceVertex) entranceState.getVertex()).getEntranceName()) + ); } } diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 12decc1e3d5..b9b3fddb6e4 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -2607,6 +2607,15 @@ type serviceTimeRange { start: Long } +"Station entrance/exit" +type Entrance { + "Name of a station entrance/exit" + name: String +} + +"Entity to a step" +union StepEntity = Entrance + type step { "The cardinal (compass) direction (e.g. north, northeast) taken when engaging this step." absoluteDirection: AbsoluteDirection @@ -2628,8 +2637,6 @@ type step { elevationProfile: [elevationProfileComponent] "When exiting a highway or traffic circle, the exit name/number." exit: String - "Name of entrance to a public transport station" - entrance: String "The latitude of the start of the step." lat: Float "The longitude of the start of the step." @@ -2642,6 +2649,8 @@ type step { streetName: String "Is this step walking with a bike?" walkingBike: Boolean + "Step entity e.g. an entrance" + entity: StepEntity } type stopAtDistance implements Node { From 9d18269db5311d5802b79e0410ece8fd24e6583b Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Thu, 3 Oct 2024 14:52:42 +0300 Subject: [PATCH 003/106] Add more parameters to Entrance --- .../apis/gtfs/generated/GraphQLDataFetchers.java | 4 ++++ .../org/opentripplanner/apis/gtfs/model/StepEntity.java | 6 +++++- .../routing/algorithm/mapping/StatesToWalkStepsMapper.java | 6 ++++-- .../resources/org/opentripplanner/apis/gtfs/schema.graphqls | 6 +++++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java index efc59f72d42..dfe2527715f 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java @@ -367,6 +367,10 @@ public interface GraphQLEmissions { /** Station entrance/exit */ public interface GraphQLEntrance { + public DataFetcher code(); + + public DataFetcher gtfsId(); + public DataFetcher name(); } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/model/StepEntity.java b/src/main/java/org/opentripplanner/apis/gtfs/model/StepEntity.java index 5e10f4e08a6..e1f53cf6842 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/model/StepEntity.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/model/StepEntity.java @@ -5,7 +5,11 @@ import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; public interface StepEntity extends GraphQLDataFetchers.GraphQLStepEntity { - record Entrance(String name) implements StepEntity {} + record Entrance(String code, String name, String gtfsId) implements StepEntity { + public static Entrance withCode(String code) { + return new Entrance(code, null, null); + } + } @Override default GraphQLObjectType getType(TypeResolutionEnvironment env) { diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java index 7e0ff3696ca..eeb091125aa 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java @@ -10,7 +10,7 @@ import javax.annotation.Nonnull; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; -import org.opentripplanner.apis.gtfs.model.StepEntity.Entrance; +import org.opentripplanner.apis.gtfs.model.StepEntity; import org.opentripplanner.framework.geometry.DirectionUtils; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; @@ -396,7 +396,9 @@ private void setStationEntrance(State backState) { } if (entranceState.getVertex() instanceof StationEntranceVertex) { current.withEntrance( - new Entrance(((StationEntranceVertex) entranceState.getVertex()).getEntranceName()) + StepEntity.Entrance.withCode( + ((StationEntranceVertex) entranceState.getVertex()).getEntranceName() + ) ); } } diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index b9b3fddb6e4..9fef54c3965 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -2609,8 +2609,12 @@ type serviceTimeRange { "Station entrance/exit" type Entrance { - "Name of a station entrance/exit" + "Code of entrance/exit eg A or B" + code: String + "Name of entrance/exit" name: String + "Gtfs id of entrance/exit" + gtfsId: String } "Entity to a step" From 3b8828831c3e0409fbe3da49a7d238fa0888a884 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Mon, 7 Oct 2024 17:22:54 +0300 Subject: [PATCH 004/106] Move StepEntity classes --- .../apis/gtfs/GtfsGraphQLIndex.java | 4 +-- .../datafetchers/StepEntityTypeResolver.java | 30 +++++++++++++++++++ .../apis/gtfs/model/StepEntity.java | 23 -------------- .../opentripplanner/model/plan/Entrance.java | 18 +++++++++++ .../model/plan/StepEntity.java | 3 ++ .../opentripplanner/model/plan/WalkStep.java | 2 +- .../model/plan/WalkStepBuilder.java | 4 +-- .../mapping/StatesToWalkStepsMapper.java | 6 ++-- 8 files changed, 58 insertions(+), 32 deletions(-) create mode 100644 src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java delete mode 100644 src/main/java/org/opentripplanner/apis/gtfs/model/StepEntity.java create mode 100644 src/main/java/org/opentripplanner/model/plan/Entrance.java create mode 100644 src/main/java/org/opentripplanner/model/plan/StepEntity.java diff --git a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java index ed3242b5823..5b288762262 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java @@ -58,6 +58,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.StepEntityTypeResolver; import org.opentripplanner.apis.gtfs.datafetchers.StopGeometriesImpl; import org.opentripplanner.apis.gtfs.datafetchers.StopImpl; import org.opentripplanner.apis.gtfs.datafetchers.StopOnRouteImpl; @@ -81,7 +82,6 @@ import org.opentripplanner.apis.gtfs.datafetchers.serviceTimeRangeImpl; import org.opentripplanner.apis.gtfs.datafetchers.stepImpl; import org.opentripplanner.apis.gtfs.datafetchers.stopAtDistanceImpl; -import org.opentripplanner.apis.gtfs.model.StepEntity; import org.opentripplanner.apis.gtfs.model.StopPosition; import org.opentripplanner.apis.support.graphql.LoggingDataFetcherExceptionHandler; import org.opentripplanner.ext.actuator.MicrometerGraphQLInstrumentation; @@ -125,7 +125,7 @@ protected static GraphQLSchema buildSchema() { .type("Node", type -> type.typeResolver(new NodeTypeResolver())) .type("PlaceInterface", type -> type.typeResolver(new PlaceInterfaceTypeResolver())) .type("StopPosition", type -> type.typeResolver(new StopPosition() {})) - .type("StepEntity", type -> type.typeResolver(new StepEntity() {})) + .type("StepEntity", type -> type.typeResolver(new StepEntityTypeResolver())) .type("FareProduct", type -> type.typeResolver(new FareProductTypeResolver())) .type("AlertEntity", type -> type.typeResolver(new AlertEntityTypeResolver())) .type(typeWiring.build(AgencyImpl.class)) diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java new file mode 100644 index 00000000000..5fc8a123226 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java @@ -0,0 +1,30 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.TypeResolutionEnvironment; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; +import graphql.schema.TypeResolver; +import org.opentripplanner.apis.gtfs.model.RouteTypeModel; +import org.opentripplanner.apis.gtfs.model.StopOnRouteModel; +import org.opentripplanner.apis.gtfs.model.StopOnTripModel; +import org.opentripplanner.apis.gtfs.model.UnknownModel; +import org.opentripplanner.model.plan.Entrance; +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.RegularStop; +import org.opentripplanner.transit.model.timetable.Trip; + +public class StepEntityTypeResolver 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/src/main/java/org/opentripplanner/apis/gtfs/model/StepEntity.java b/src/main/java/org/opentripplanner/apis/gtfs/model/StepEntity.java deleted file mode 100644 index e1f53cf6842..00000000000 --- a/src/main/java/org/opentripplanner/apis/gtfs/model/StepEntity.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.opentripplanner.apis.gtfs.model; - -import graphql.TypeResolutionEnvironment; -import graphql.schema.GraphQLObjectType; -import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; - -public interface StepEntity extends GraphQLDataFetchers.GraphQLStepEntity { - record Entrance(String code, String name, String gtfsId) implements StepEntity { - public static Entrance withCode(String code) { - return new Entrance(code, null, null); - } - } - - @Override - default GraphQLObjectType getType(TypeResolutionEnvironment env) { - var schema = env.getSchema(); - Object o = env.getObject(); - if (o instanceof Entrance) { - return schema.getObjectType("Entrance"); - } - return null; - } -} diff --git a/src/main/java/org/opentripplanner/model/plan/Entrance.java b/src/main/java/org/opentripplanner/model/plan/Entrance.java new file mode 100644 index 00000000000..7b28ee992a4 --- /dev/null +++ b/src/main/java/org/opentripplanner/model/plan/Entrance.java @@ -0,0 +1,18 @@ +package org.opentripplanner.model.plan; + +public final class Entrance extends StepEntity { + + private final String code; + private final String gtfsId; + private final String name; + + public Entrance(String code, String gtfsId, String name) { + this.code = code; + this.gtfsId = gtfsId; + this.name = name; + } + + public static Entrance withCode(String code) { + return new Entrance(code, null, null); + } +} diff --git a/src/main/java/org/opentripplanner/model/plan/StepEntity.java b/src/main/java/org/opentripplanner/model/plan/StepEntity.java new file mode 100644 index 00000000000..e6bfd587bfc --- /dev/null +++ b/src/main/java/org/opentripplanner/model/plan/StepEntity.java @@ -0,0 +1,3 @@ +package org.opentripplanner.model.plan; + +public abstract class StepEntity {} diff --git a/src/main/java/org/opentripplanner/model/plan/WalkStep.java b/src/main/java/org/opentripplanner/model/plan/WalkStep.java index 3e9e9fe14dc..8114246d7e8 100644 --- a/src/main/java/org/opentripplanner/model/plan/WalkStep.java +++ b/src/main/java/org/opentripplanner/model/plan/WalkStep.java @@ -4,11 +4,11 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import org.opentripplanner.apis.gtfs.model.StepEntity; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.lang.DoubleUtils; import org.opentripplanner.framework.tostring.ToStringBuilder; +import org.opentripplanner.model.plan.StepEntity; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.note.StreetNote; diff --git a/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java b/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java index d020f8d0113..02b73c0ce15 100644 --- a/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java +++ b/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java @@ -5,12 +5,12 @@ import java.util.List; import java.util.Set; import javax.annotation.Nullable; -import org.opentripplanner.apis.gtfs.model.StepEntity; -import org.opentripplanner.apis.gtfs.model.StepEntity.Entrance; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.lang.DoubleUtils; import org.opentripplanner.framework.lang.IntUtils; +import org.opentripplanner.model.plan.Entrance; +import org.opentripplanner.model.plan.StepEntity; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.note.StreetNote; diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java index eeb091125aa..158979f5d91 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java @@ -10,11 +10,11 @@ import javax.annotation.Nonnull; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; -import org.opentripplanner.apis.gtfs.model.StepEntity; import org.opentripplanner.framework.geometry.DirectionUtils; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.model.plan.ElevationProfile; +import org.opentripplanner.model.plan.Entrance; import org.opentripplanner.model.plan.RelativeDirection; import org.opentripplanner.model.plan.WalkStep; import org.opentripplanner.model.plan.WalkStepBuilder; @@ -396,9 +396,7 @@ private void setStationEntrance(State backState) { } if (entranceState.getVertex() instanceof StationEntranceVertex) { current.withEntrance( - StepEntity.Entrance.withCode( - ((StationEntranceVertex) entranceState.getVertex()).getEntranceName() - ) + Entrance.withCode(((StationEntranceVertex) entranceState.getVertex()).getEntranceName()) ); } } From 0a62bf81363030d990ed9e0e5e596a479c3a5644 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Wed, 16 Oct 2024 18:19:55 +0300 Subject: [PATCH 005/106] Remove default name for subway station entrances --- .../graph_builder/module/osm/VertexGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java index df9c4376871..633d4343b83 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java @@ -100,7 +100,7 @@ IntersectionVertex getVertexForOsmNode(OSMNode node, OSMWithTags way) { if (ref != null) { iv = vertexFactory.stationEntrance(nid, coordinate, ref); } else { - iv = vertexFactory.stationEntrance(nid, coordinate, "MAIN_ENTRANCE"); + iv = vertexFactory.stationEntrance(nid, coordinate, null); } } From 528ab55927be6269d8dee5bfec52e1b0dcbf999d Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Fri, 18 Oct 2024 17:39:43 +0300 Subject: [PATCH 006/106] Add option to turn on osm subway entrances in osmDefaults --- doc/user/BuildConfiguration.md | 2 ++ .../module/configure/GraphBuilderModules.java | 2 ++ .../graph_builder/module/osm/OsmModule.java | 8 ++++++- .../module/osm/OsmModuleBuilder.java | 9 +++++++- .../module/osm/VertexGenerator.java | 11 ++++++++-- .../osm/parameters/OsmExtractParameters.java | 21 +++++++++++++++++-- .../OsmExtractParametersBuilder.java | 15 +++++++++++++ .../parameters/OsmProcessingParameters.java | 4 +++- .../openstreetmap/OsmProvider.java | 9 ++++++++ .../config/buildconfig/OsmConfig.java | 8 +++++++ .../module/osm/WalkableAreaBuilderTest.java | 2 +- 11 files changed, 83 insertions(+), 8 deletions(-) diff --git a/doc/user/BuildConfiguration.md b/doc/user/BuildConfiguration.md index b311991120e..7af5bbad1c5 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 in the OSM data. Overrides the value specified in `osmDefaults`. | *Optional* | `false` | 2.2 | |       [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 in the OSM data. | *Optional* | `false` | 2.2 | |    [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/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java b/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java index 10d3a997579..169ac351c87 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java @@ -68,6 +68,7 @@ static OsmModule provideOpenStreetMapModule( osmConfiguredDataSource.dataSource(), osmConfiguredDataSource.config().osmTagMapper(), osmConfiguredDataSource.config().timeZone(), + osmConfiguredDataSource.config().includeOsmSubwayEntrances(), config.osmCacheDataInMem, issueStore ) @@ -83,6 +84,7 @@ static OsmModule provideOpenStreetMapModule( .withStaticBikeParkAndRide(config.staticBikeParkAndRide) .withMaxAreaNodes(config.maxAreaNodes) .withBoardingAreaRefTags(config.boardingLocationTags) + .withIncludeOsmSubwayEntrances(config.osmDefaults.includeOsmSubwayEntrances()) .withIssueStore(issueStore) .withStreetLimitationParameters(streetLimitationParameters) .build(); diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java index 10c215ee448..cbdd3d7a7a2 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java @@ -71,7 +71,13 @@ public class OsmModule implements GraphBuilderModule { this.issueStore = issueStore; this.params = params; this.osmdb = new OsmDatabase(issueStore); - this.vertexGenerator = new VertexGenerator(osmdb, graph, params.boardingAreaRefTags()); + this.vertexGenerator = + new VertexGenerator( + osmdb, + graph, + params.boardingAreaRefTags(), + params.includeOsmSubwayEntrances() + ); this.normalizer = new SafetyValueNormalizer(graph, issueStore); this.streetLimitationParameters = Objects.requireNonNull(streetLimitationParameters); } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModuleBuilder.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModuleBuilder.java index f0a40fa678f..29e8f8a1ae5 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModuleBuilder.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModuleBuilder.java @@ -24,6 +24,7 @@ public class OsmModuleBuilder { private boolean platformEntriesLinking = false; private boolean staticParkAndRide = false; private boolean staticBikeParkAndRide = false; + private boolean includeOsmSubwayEntrances = false; private int maxAreaNodes; private StreetLimitationParameters streetLimitationParameters = new StreetLimitationParameters(); @@ -72,6 +73,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; @@ -90,7 +96,8 @@ public OsmModule build() { areaVisibility, platformEntriesLinking, staticParkAndRide, - staticBikeParkAndRide + staticBikeParkAndRide, + includeOsmSubwayEntrances ) ); } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java index 633d4343b83..87cd88a5227 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java +++ b/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,7 +102,7 @@ IntersectionVertex getVertexForOsmNode(OSMNode node, OSMWithTags way) { iv = bv; } - if (node.isSubwayEntrance()) { + if (includeOsmSubwayEntrances && node.isSubwayEntrance()) { String ref = node.getTag("ref"); if (ref != null) { iv = vertexFactory.stationEntrance(nid, coordinate, ref); diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmExtractParameters.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmExtractParameters.java index 9d2eead5f7e..1cae389d9c4 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmExtractParameters.java +++ b/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.getIncludeOsmSubwayEntrances() + ); } @Override @@ -37,6 +49,11 @@ public ZoneId timeZone() { return timeZone; } + @Nullable + public boolean includeOsmSubwayEntrances() { + return includeOsmSubwayEntrances; + } + public OsmExtractParametersBuilder copyOf() { return new OsmExtractParametersBuilder(this); } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmExtractParametersBuilder.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmExtractParametersBuilder.java index 03fd7eaec4e..0bbc184569d 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmExtractParametersBuilder.java +++ b/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 getIncludeOsmSubwayEntrances() { + return includeOsmSubwayEntrances; + } + public OsmExtractParameters build() { return new OsmExtractParameters(this); } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmProcessingParameters.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmProcessingParameters.java index 52bf8d65314..a3fd14020e8 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmProcessingParameters.java +++ b/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/src/main/java/org/opentripplanner/openstreetmap/OsmProvider.java b/src/main/java/org/opentripplanner/openstreetmap/OsmProvider.java index e35a846cbbd..d66f5db394b 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/OsmProvider.java +++ b/src/main/java/org/opentripplanner/openstreetmap/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/src/main/java/org/opentripplanner/standalone/config/buildconfig/OsmConfig.java b/src/main/java/org/opentripplanner/standalone/config/buildconfig/OsmConfig.java index 1b2ec0ed74d..c1a3963c6a5 100644 --- a/src/main/java/org/opentripplanner/standalone/config/buildconfig/OsmConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/buildconfig/OsmConfig.java @@ -84,6 +84,14 @@ public static OsmExtractParametersBuilder mapOsmGenericParameters( ) .docDefaultValue(docDefaults.timeZone()) .asZoneId(defaults.timeZone()) + ) + .withIncludeOsmSubwayEntrances( + node + .of("includeOsmSubwayEntrances") + .since(V2_2) + .summary("Whether to include subway entrances from the OSM data." + documentationAddition) + .docDefaultValue(docDefaults.includeOsmSubwayEntrances()) + .asBoolean(defaults.includeOsmSubwayEntrances()) ); } } diff --git a/src/test/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilderTest.java b/src/test/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilderTest.java index 4906bce930d..26500299062 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilderTest.java +++ b/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, From 401a405cd9000a5f1ebb92bb9ba130f8a6ff0fcd Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Mon, 21 Oct 2024 18:41:18 +0300 Subject: [PATCH 007/106] Fix walk step generation --- .../mapping/StatesToWalkStepsMapper.java | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) 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 9202c94c76a..6bcef1b0da9 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 @@ -177,6 +177,9 @@ private void processState(State backState, State forwardState) { if (edge instanceof ElevatorAlightEdge) { addStep(createElevatorWalkStep(backState, forwardState, edge)); return; + } else if (backState.getVertex() instanceof StationEntranceVertex) { + addStep(createStationEntranceWalkStep(backState, forwardState, edge)); + return; } else if (edge instanceof PathwayEdge pwe && pwe.signpostedAs().isPresent()) { createAndSaveStep(backState, forwardState, pwe.signpostedAs().get(), FOLLOW_SIGNS, edge); return; @@ -259,8 +262,6 @@ private void processState(State backState, State forwardState) { setMotorwayExit(backState); - setStationEntrance(backState); - if (createdNewStep && !modeTransition) { // check last three steps for zag int lastIndex = steps.size() - 1; @@ -382,23 +383,6 @@ private void setMotorwayExit(State backState) { } } - /** - * Update the walk step with the name of the station entrance if set from OSM - */ - private void setStationEntrance(State backState) { - State entranceState = backState; - Edge entranceEdge = entranceState.getBackEdge(); - while (entranceEdge instanceof FreeEdge) { - entranceState = entranceState.getBackState(); - entranceEdge = entranceState.getBackEdge(); - } - if (entranceState.getVertex() instanceof StationEntranceVertex) { - current.withEntrance( - Entrance.withCode(((StationEntranceVertex) entranceState.getVertex()).getEntranceName()) - ); - } - } - /** * Is it possible to turn to another street from this previous state */ @@ -536,6 +520,22 @@ private WalkStepBuilder createElevatorWalkStep(State backState, State forwardSta return step; } + private WalkStepBuilder createStationEntranceWalkStep( + State backState, + State forwardState, + Edge edge + ) { + // don't care what came before or comes after + var step = createWalkStep(forwardState, backState); + + step.withRelativeDirection(RelativeDirection.CONTINUE); + + step.withEntrance( + Entrance.withCode(((StationEntranceVertex) backState.getVertex()).getEntranceName()) + ); + return step; + } + private void createAndSaveStep( State backState, State forwardState, From 438bc318faa6ef96c1ef78f997005b631910c6fd Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Wed, 23 Oct 2024 20:14:01 +0300 Subject: [PATCH 008/106] Add step entity to graphql tests --- .../apis/gtfs/GraphQLIntegrationTest.java | 8 +++++++- .../apis/gtfs/expectations/walk-steps.json | 17 +++++++++++++++-- .../apis/gtfs/queries/walk-steps.graphql | 6 ++++++ 3 files changed, 28 insertions(+), 3 deletions(-) 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 663ce2d5f3f..020a1ec6113 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -49,6 +49,7 @@ import org.opentripplanner.model.fare.ItineraryFares; import org.opentripplanner.model.fare.RiderCategory; import org.opentripplanner.model.plan.Emissions; +import org.opentripplanner.model.plan.Entrance; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.model.plan.Place; import org.opentripplanner.model.plan.RelativeDirection; @@ -232,8 +233,13 @@ public Set getRoutesForStop(StopLocation stop) { .build(); var step2 = walkStep("elevator").withRelativeDirection(RelativeDirection.ELEVATOR).build(); + var step3 = walkStep("entrance") + .withRelativeDirection(RelativeDirection.CONTINUE) + .withEntrance(Entrance.withCode("A")) + .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/resources/org/opentripplanner/apis/gtfs/expectations/walk-steps.json b/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/walk-steps.json index be584a875be..b4172c0f70f 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,26 @@ "streetName" : "street", "area" : false, "relativeDirection" : "DEPART", - "absoluteDirection" : "NORTHEAST" + "absoluteDirection" : "NORTHEAST", + "entity" : null }, { "streetName" : "elevator", "area" : false, "relativeDirection" : "ELEVATOR", - "absoluteDirection" : null + "absoluteDirection" : null, + "entity" : null + + }, + { + "streetName" : "entrance", + "area" : false, + "relativeDirection" : "CONTINUE", + "absoluteDirection" : null, + "entity" : { + "__typename" : "Entrance", + "code": "A" + } } ] }, 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..74cbe0c599e 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,12 @@ area relativeDirection absoluteDirection + entity { + __typename + ... on Entrance { + code + } + } } } } From 97c2de6ac34d7315ffb07555f011b4aeacd52cfa Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Fri, 25 Oct 2024 15:15:02 +0300 Subject: [PATCH 009/106] Rename variables to match with graphql --- .../algorithm/mapping/StatesToWalkStepsMapper.java | 6 ++---- .../street/model/vertex/StationEntranceVertex.java | 12 ++++++------ 2 files changed, 8 insertions(+), 10 deletions(-) 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 6bcef1b0da9..8a289713532 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 @@ -529,10 +529,8 @@ private WalkStepBuilder createStationEntranceWalkStep( var step = createWalkStep(forwardState, backState); step.withRelativeDirection(RelativeDirection.CONTINUE); - - step.withEntrance( - Entrance.withCode(((StationEntranceVertex) backState.getVertex()).getEntranceName()) - ); + System.out.println(backState.getVertex().toString()); + step.withEntrance(Entrance.withCode(((StationEntranceVertex) backState.getVertex()).getCode())); return step; } diff --git a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java index af1e824a7bd..083c0d52a0f 100644 --- a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java @@ -2,18 +2,18 @@ public class StationEntranceVertex extends OsmVertex { - private final String entranceName; + private final String code; - public StationEntranceVertex(double x, double y, long nodeId, String entranceName) { + public StationEntranceVertex(double x, double y, long nodeId, String code) { super(x, y, nodeId); - this.entranceName = entranceName; + this.code = code; } - public String getEntranceName() { - return entranceName; + public String getCode() { + return code; } public String toString() { - return "StationEntranceVertex(" + super.toString() + ")"; + return "StationEntranceVertex(" + super.toString() + ", code=" + code + ")"; } } From d844561b9d4a5f98d11a4f8eaabe63fe1b461a42 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Fri, 25 Oct 2024 16:10:14 +0300 Subject: [PATCH 010/106] Rename variables --- .../street/model/vertex/VertexFactory.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 007d3119a55..61f74841245 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 @@ -94,12 +94,8 @@ 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 entranceName - ) { - return addToGraph(new StationEntranceVertex(coordinate.x, coordinate.y, nid, entranceName)); + public StationEntranceVertex stationEntrance(long nid, Coordinate coordinate, String code) { + return addToGraph(new StationEntranceVertex(coordinate.x, coordinate.y, nid, code)); } public OsmVertex osm( From 02a07ea2ed91ade71e5d4e5384a5e93fcf31ea67 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Sun, 27 Oct 2024 12:39:45 +0200 Subject: [PATCH 011/106] Rename function --- .../module/osm/parameters/OsmExtractParameters.java | 2 +- .../module/osm/parameters/OsmExtractParametersBuilder.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 6e8ff0f793c..37edaf687ab 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 @@ -31,7 +31,7 @@ public record OsmExtractParameters( builder.getSource(), builder.getOsmTagMapper(), builder.getTimeZone(), - builder.getIncludeOsmSubwayEntrances() + builder.includeOsmSubwayEntrances() ); } 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 9abc6e6bcc7..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 @@ -72,7 +72,7 @@ public ZoneId getTimeZone() { return timeZone; } - public boolean getIncludeOsmSubwayEntrances() { + public boolean includeOsmSubwayEntrances() { return includeOsmSubwayEntrances; } From 5dc74dd48555935dcd1deddcf39de0d4b73eaac0 Mon Sep 17 00:00:00 2001 From: Henrik Sundell <47221103+HenrikSundell@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:05:39 +0200 Subject: [PATCH 012/106] Fix comments Co-authored-by: Joel Lappalainen --- .../java/org/opentripplanner/osm/model/OsmNode.java | 4 ++-- .../org/opentripplanner/apis/gtfs/schema.graphqls | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) 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 2c343401adf..cb9fcd679f0 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java @@ -64,9 +64,9 @@ public boolean isBarrier() { } /** - * Checks if this node is an subway station entrance + * Checks if this node is a subway station entrance. * - * @return true if it does + * @return true if it is */ public boolean isSubwayEntrance() { return hasTag("railway") && "subway_entrance".equals(getTag("railway")); 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 9c4d0e4dab1..b2d48f7e0b1 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -447,13 +447,13 @@ type Emissions { co2: Grams } -"Station entrance/exit" +"Station entrance or exit." type Entrance { - "Code of entrance/exit eg A or B" + "Short text or a number that identifies the entrance or exit for passengers. For example, `A` or `B`." code: String - "Gtfs id of entrance/exit" + "ID of the entrance in the format of `FeedId:EntranceId`." gtfsId: String - "Name of entrance/exit" + "Name of the entrance or exit." name: String } @@ -2658,7 +2658,7 @@ type step { distance: Float "The elevation profile as a list of { distance, elevation } values." elevationProfile: [elevationProfileComponent] - "Step entity e.g. an entrance" + "Step entity, e.g. an entrance." entity: StepEntity "When exiting a highway or traffic circle, the exit name/number." exit: String From 5d55646ab1861c7553d7a7b3b9f91a5d328bbd59 Mon Sep 17 00:00:00 2001 From: Henrik Sundell <47221103+HenrikSundell@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:06:35 +0200 Subject: [PATCH 013/106] Remove println Co-authored-by: Joel Lappalainen --- .../routing/algorithm/mapping/StatesToWalkStepsMapper.java | 1 - 1 file changed, 1 deletion(-) 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 8a289713532..f04a6a5a2d9 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 @@ -529,7 +529,6 @@ private WalkStepBuilder createStationEntranceWalkStep( var step = createWalkStep(forwardState, backState); step.withRelativeDirection(RelativeDirection.CONTINUE); - System.out.println(backState.getVertex().toString()); step.withEntrance(Entrance.withCode(((StationEntranceVertex) backState.getVertex()).getCode())); return step; } From d1067c6679a68ee252f8bda96300d28dee8cfe75 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Mon, 28 Oct 2024 16:06:52 +0200 Subject: [PATCH 014/106] Remove unnecessary imports --- .../apis/gtfs/datafetchers/StepEntityTypeResolver.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java index 5fc8a123226..5e7d6098344 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java @@ -4,16 +4,7 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.TypeResolver; -import org.opentripplanner.apis.gtfs.model.RouteTypeModel; -import org.opentripplanner.apis.gtfs.model.StopOnRouteModel; -import org.opentripplanner.apis.gtfs.model.StopOnTripModel; -import org.opentripplanner.apis.gtfs.model.UnknownModel; import org.opentripplanner.model.plan.Entrance; -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.RegularStop; -import org.opentripplanner.transit.model.timetable.Trip; public class StepEntityTypeResolver implements TypeResolver { From 5c97ad17f610d7458d6ff5e1a79c1b03ac643f18 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Mon, 28 Oct 2024 18:40:41 +0200 Subject: [PATCH 015/106] Add accessibilty information to entrances --- .../apis/gtfs/generated/GraphQLDataFetchers.java | 13 +++---------- .../graph_builder/module/osm/VertexGenerator.java | 8 +++----- .../org/opentripplanner/model/plan/Entrance.java | 8 +++++--- .../algorithm/mapping/StatesToWalkStepsMapper.java | 4 +++- .../street/model/vertex/StationEntranceVertex.java | 8 +++++++- .../street/model/vertex/VertexFactory.java | 9 +++++++-- .../org/opentripplanner/apis/gtfs/schema.graphqls | 2 ++ .../apis/gtfs/GraphQLIntegrationTest.java | 2 +- .../apis/gtfs/expectations/walk-steps.json | 3 ++- .../apis/gtfs/queries/walk-steps.graphql | 1 + 10 files changed, 34 insertions(+), 24 deletions(-) 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 179758bd78d..ec0e573d705 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 @@ -1,11 +1,9 @@ //THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. package org.opentripplanner.apis.gtfs.generated; -import graphql.relay.Connection; import graphql.relay.Connection; import graphql.relay.DefaultEdge; import graphql.relay.Edge; -import graphql.relay.Edge; import graphql.schema.DataFetcher; import graphql.schema.TypeResolver; import java.util.Currency; @@ -27,12 +25,8 @@ import org.opentripplanner.apis.gtfs.model.FeedPublisher; import org.opentripplanner.apis.gtfs.model.PlanPageInfo; import org.opentripplanner.apis.gtfs.model.RideHailingProvider; -import org.opentripplanner.apis.gtfs.model.RouteTypeModel; -import org.opentripplanner.apis.gtfs.model.StopOnRouteModel; -import org.opentripplanner.apis.gtfs.model.StopOnTripModel; import org.opentripplanner.apis.gtfs.model.StopPosition; import org.opentripplanner.apis.gtfs.model.TripOccupancy; -import org.opentripplanner.apis.gtfs.model.UnknownModel; import org.opentripplanner.ext.fares.model.FareRuleSet; import org.opentripplanner.ext.ridehailing.model.RideEstimate; import org.opentripplanner.model.StopTimesInPattern; @@ -55,8 +49,6 @@ import org.opentripplanner.routing.graphfinder.PatternAtStop; import org.opentripplanner.routing.graphfinder.PlaceAtDistance; import org.opentripplanner.routing.vehicle_parking.VehicleParking; -import org.opentripplanner.routing.vehicle_parking.VehicleParking; -import org.opentripplanner.routing.vehicle_parking.VehicleParking; import org.opentripplanner.routing.vehicle_parking.VehicleParkingSpaces; import org.opentripplanner.routing.vehicle_parking.VehicleParkingState; import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; @@ -67,7 +59,6 @@ import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris; -import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris; import org.opentripplanner.service.vehiclerental.model.VehicleRentalSystem; import org.opentripplanner.service.vehiclerental.model.VehicleRentalVehicle; import org.opentripplanner.transit.model.basic.Money; @@ -366,8 +357,10 @@ public interface GraphQLEmissions { public DataFetcher co2(); } - /** Station entrance/exit */ + /** Station entrance or exit. */ public interface GraphQLEntrance { + public DataFetcher accessible(); + public DataFetcher code(); public DataFetcher gtfsId(); 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 42bbaf659c1..b4c3c08aa8b 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 @@ -104,11 +104,9 @@ IntersectionVertex getVertexForOsmNode(OsmNode node, OsmWithTags way) { if (includeOsmSubwayEntrances && node.isSubwayEntrance()) { String ref = node.getTag("ref"); - if (ref != null) { - iv = vertexFactory.stationEntrance(nid, coordinate, ref); - } else { - iv = vertexFactory.stationEntrance(nid, coordinate, null); - } + + boolean accessible = node.isTag("wheelchair", "yes"); + iv = vertexFactory.stationEntrance(nid, coordinate, ref, accessible); } if (iv == null) { diff --git a/application/src/main/java/org/opentripplanner/model/plan/Entrance.java b/application/src/main/java/org/opentripplanner/model/plan/Entrance.java index 7b28ee992a4..fa1e0fe5e57 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/Entrance.java +++ b/application/src/main/java/org/opentripplanner/model/plan/Entrance.java @@ -5,14 +5,16 @@ public final class Entrance extends StepEntity { private final String code; private final String gtfsId; private final String name; + private final boolean accessible; - public Entrance(String code, String gtfsId, String name) { + public Entrance(String code, String gtfsId, String name, boolean accessible) { this.code = code; this.gtfsId = gtfsId; this.name = name; + this.accessible = accessible; } - public static Entrance withCode(String code) { - return new Entrance(code, null, null); + public static Entrance withCodeAndAccessible(String code, boolean accessible) { + return new Entrance(code, null, null, accessible); } } 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 f04a6a5a2d9..14ed2a553e4 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 @@ -529,7 +529,9 @@ private WalkStepBuilder createStationEntranceWalkStep( var step = createWalkStep(forwardState, backState); step.withRelativeDirection(RelativeDirection.CONTINUE); - step.withEntrance(Entrance.withCode(((StationEntranceVertex) backState.getVertex()).getCode())); + + StationEntranceVertex vertex = (StationEntranceVertex) backState.getVertex(); + step.withEntrance(Entrance.withCodeAndAccessible(vertex.getCode(), vertex.isAccessible())); return step; } diff --git a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java index 083c0d52a0f..3f83a356dea 100644 --- a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java @@ -3,16 +3,22 @@ public class StationEntranceVertex extends OsmVertex { private final String code; + private final boolean accessible; - public StationEntranceVertex(double x, double y, long nodeId, String code) { + public StationEntranceVertex(double x, double y, long nodeId, String code, boolean accessible) { super(x, y, nodeId); this.code = code; + this.accessible = accessible; } public String getCode() { return code; } + public boolean isAccessible() { + return accessible; + } + public String toString() { return "StationEntranceVertex(" + super.toString() + ", code=" + code + ")"; } 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 61f74841245..1c37d39adb6 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 @@ -94,8 +94,13 @@ 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) { - return addToGraph(new StationEntranceVertex(coordinate.x, coordinate.y, nid, code)); + public StationEntranceVertex stationEntrance( + long nid, + Coordinate coordinate, + String code, + boolean accessible + ) { + return addToGraph(new StationEntranceVertex(coordinate.x, coordinate.y, nid, code, accessible)); } public OsmVertex osm( 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 b2d48f7e0b1..83c32e2c1ca 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -449,6 +449,8 @@ type Emissions { "Station entrance or exit." type Entrance { + "True if the entrance is wheelchair accessible." + accessible: Boolean "Short text or a number that identifies the entrance or exit for passengers. For example, `A` or `B`." code: String "ID of the entrance in the format of `FeedId:EntranceId`." 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 020a1ec6113..d2448a92c59 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -235,7 +235,7 @@ public Set getRoutesForStop(StopLocation stop) { var step3 = walkStep("entrance") .withRelativeDirection(RelativeDirection.CONTINUE) - .withEntrance(Entrance.withCode("A")) + .withEntrance(Entrance.withCodeAndAccessible("A", true)) .build(); Itinerary i1 = newItinerary(A, T11_00) 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 b4172c0f70f..3a8e952880f 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 @@ -29,7 +29,8 @@ "absoluteDirection" : null, "entity" : { "__typename" : "Entrance", - "code": "A" + "code": "A", + "accessible": true } } ] 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 74cbe0c599e..5e7c0493c35 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 @@ -24,6 +24,7 @@ __typename ... on Entrance { code + accessible } } } From e10e0a2fae17e16cfebb1e888652a531e7fef493 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Thu, 7 Nov 2024 13:22:19 +0200 Subject: [PATCH 016/106] Use existing entrance class for walk steps --- .../mapping/RelativeDirectionMapper.java | 1 + .../restapi/model/ApiRelativeDirection.java | 1 + .../apis/gtfs/GtfsGraphQLIndex.java | 4 +- .../apis/gtfs/datafetchers/EntranceImpl.java | 53 +++++++++++++++++++ .../datafetchers/StepEntityTypeResolver.java | 2 +- .../apis/gtfs/datafetchers/stepImpl.java | 4 +- .../gtfs/generated/GraphQLDataFetchers.java | 9 ++-- .../apis/gtfs/generated/GraphQLTypes.java | 1 + .../apis/gtfs/mapping/DirectionMapper.java | 1 + .../opentripplanner/model/plan/Entrance.java | 20 ------- .../model/plan/RelativeDirection.java | 1 + .../model/plan/StepEntity.java | 3 -- .../opentripplanner/model/plan/WalkStep.java | 14 ++--- .../model/plan/WalkStepBuilder.java | 9 ++-- .../mapping/StatesToWalkStepsMapper.java | 17 ++++-- .../framework/AbstractTransitEntity.java | 2 +- .../opentripplanner/apis/gtfs/schema.graphqls | 12 ++--- .../apis/gtfs/GraphQLIntegrationTest.java | 14 +++-- .../apis/gtfs/expectations/walk-steps.json | 11 ++-- .../apis/gtfs/queries/walk-steps.graphql | 9 ++-- 20 files changed, 115 insertions(+), 73 deletions(-) create mode 100644 application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java delete mode 100644 application/src/main/java/org/opentripplanner/model/plan/Entrance.java delete mode 100644 application/src/main/java/org/opentripplanner/model/plan/StepEntity.java 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..708da1fd6c3 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 @@ -25,6 +25,7 @@ public static ApiRelativeDirection mapRelativeDirection(RelativeDirection domain case UTURN_RIGHT -> ApiRelativeDirection.UTURN_RIGHT; case ENTER_STATION -> ApiRelativeDirection.ENTER_STATION; case EXIT_STATION -> ApiRelativeDirection.EXIT_STATION; + case ENTER_OR_EXIT_STATION -> ApiRelativeDirection.ENTER_OR_EXIT_STATION; case FOLLOW_SIGNS -> ApiRelativeDirection.FOLLOW_SIGNS; }; } diff --git a/application/src/ext/java/org/opentripplanner/ext/restapi/model/ApiRelativeDirection.java b/application/src/ext/java/org/opentripplanner/ext/restapi/model/ApiRelativeDirection.java index 02a530f06de..eb624df5ea6 100644 --- a/application/src/ext/java/org/opentripplanner/ext/restapi/model/ApiRelativeDirection.java +++ b/application/src/ext/java/org/opentripplanner/ext/restapi/model/ApiRelativeDirection.java @@ -21,5 +21,6 @@ public enum ApiRelativeDirection { UTURN_RIGHT, ENTER_STATION, EXIT_STATION, + ENTER_OR_EXIT_STATION, FOLLOW_SIGNS, } 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 5b288762262..a5eedb4c71c 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java @@ -37,6 +37,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.FareProductTypeResolver; import org.opentripplanner.apis.gtfs.datafetchers.FareProductUseImpl; import org.opentripplanner.apis.gtfs.datafetchers.FeedImpl; @@ -58,7 +59,6 @@ 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.StepEntityTypeResolver; import org.opentripplanner.apis.gtfs.datafetchers.StopGeometriesImpl; import org.opentripplanner.apis.gtfs.datafetchers.StopImpl; import org.opentripplanner.apis.gtfs.datafetchers.StopOnRouteImpl; @@ -125,7 +125,6 @@ protected static GraphQLSchema buildSchema() { .type("Node", type -> type.typeResolver(new NodeTypeResolver())) .type("PlaceInterface", type -> type.typeResolver(new PlaceInterfaceTypeResolver())) .type("StopPosition", type -> type.typeResolver(new StopPosition() {})) - .type("StepEntity", type -> type.typeResolver(new StepEntityTypeResolver())) .type("FareProduct", type -> type.typeResolver(new FareProductTypeResolver())) .type("AlertEntity", type -> type.typeResolver(new AlertEntityTypeResolver())) .type(typeWiring.build(AgencyImpl.class)) @@ -181,6 +180,7 @@ protected static GraphQLSchema buildSchema() { .type(typeWiring.build(FareProductUseImpl.class)) .type(typeWiring.build(DefaultFareProductImpl.class)) .type(typeWiring.build(TripOccupancyImpl.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..89f80286eed --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java @@ -0,0 +1,53 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +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 code() { + return environment -> { + Entrance entrance = getEntrance(environment); + return entrance != null && entrance.getCode() != null ? entrance.getCode() : null; + }; + } + + @Override + public DataFetcher gtfsId() { + return environment -> { + Entrance entrance = getEntrance(environment); + return entrance != null && entrance.getId() != null ? entrance.getId().toString() : null; + }; + } + + @Override + public DataFetcher name() { + return environment -> { + Entrance entrance = getEntrance(environment); + return entrance != null && entrance.getName() != null ? entrance.getName().toString() : null; + }; + } + + @Override + public DataFetcher wheelchairAccessible() { + return environment -> { + Entrance entrance = getEntrance(environment); + return entrance != null + ? GraphQLUtils.toGraphQL(entrance.getWheelchairAccessibility()) + : null; + }; + } + + /** + * Helper method to retrieve the Entrance object from the DataFetchingEnvironment. + */ + private Entrance getEntrance(DataFetchingEnvironment environment) { + Object source = environment.getSource(); + return source instanceof Entrance ? (Entrance) source : null; + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java index 5e7d6098344..43762c97a7d 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java @@ -4,7 +4,7 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.TypeResolver; -import org.opentripplanner.model.plan.Entrance; +import org.opentripplanner.transit.model.site.Entrance; public class StepEntityTypeResolver implements TypeResolver { 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 658f2d321ad..994434b4e4c 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 @@ -54,8 +54,8 @@ public DataFetcher exit() { } @Override - public DataFetcher entity() { - return environment -> getSource(environment).getEntity(); + public DataFetcher entrance() { + return environment -> getSource(environment).getEntrance(); } @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 ec0e573d705..f99fceb410c 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 @@ -359,13 +359,13 @@ public interface GraphQLEmissions { /** Station entrance or exit. */ public interface GraphQLEntrance { - public DataFetcher accessible(); - public DataFetcher code(); public DataFetcher gtfsId(); public DataFetcher name(); + + public DataFetcher wheelchairAccessible(); } /** A 'medium' that a fare product applies to, for example cash, 'Oyster Card' or 'DB Navigator App'. */ @@ -984,9 +984,6 @@ public interface GraphQLRoutingError { public DataFetcher inputField(); } - /** Entity to a step */ - public interface GraphQLStepEntity 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. @@ -1438,7 +1435,7 @@ public interface GraphQLStep { public DataFetcher> elevationProfile(); - public DataFetcher entity(); + public DataFetcher entrance(); public DataFetcher exit(); 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 bf413f5fca4..9e4119100c0 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 @@ -3942,6 +3942,7 @@ public enum GraphQLRelativeDirection { CONTINUE, DEPART, ELEVATOR, + ENTER_OR_EXIT_STATION, ENTER_STATION, EXIT_STATION, FOLLOW_SIGNS, 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..1439cdd34c3 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 @@ -38,6 +38,7 @@ public static GraphQLRelativeDirection map(RelativeDirection relativeDirection) case UTURN_RIGHT -> GraphQLRelativeDirection.UTURN_RIGHT; case ENTER_STATION -> GraphQLRelativeDirection.ENTER_STATION; case EXIT_STATION -> GraphQLRelativeDirection.EXIT_STATION; + case ENTER_OR_EXIT_STATION -> GraphQLRelativeDirection.ENTER_OR_EXIT_STATION; case FOLLOW_SIGNS -> GraphQLRelativeDirection.FOLLOW_SIGNS; }; } diff --git a/application/src/main/java/org/opentripplanner/model/plan/Entrance.java b/application/src/main/java/org/opentripplanner/model/plan/Entrance.java deleted file mode 100644 index fa1e0fe5e57..00000000000 --- a/application/src/main/java/org/opentripplanner/model/plan/Entrance.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.opentripplanner.model.plan; - -public final class Entrance extends StepEntity { - - private final String code; - private final String gtfsId; - private final String name; - private final boolean accessible; - - public Entrance(String code, String gtfsId, String name, boolean accessible) { - this.code = code; - this.gtfsId = gtfsId; - this.name = name; - this.accessible = accessible; - } - - public static Entrance withCodeAndAccessible(String code, boolean accessible) { - return new Entrance(code, null, null, accessible); - } -} 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..fbdb836ab6a 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,7 @@ public enum RelativeDirection { UTURN_RIGHT, ENTER_STATION, EXIT_STATION, + ENTER_OR_EXIT_STATION, FOLLOW_SIGNS; public static RelativeDirection calculate( diff --git a/application/src/main/java/org/opentripplanner/model/plan/StepEntity.java b/application/src/main/java/org/opentripplanner/model/plan/StepEntity.java deleted file mode 100644 index e6bfd587bfc..00000000000 --- a/application/src/main/java/org/opentripplanner/model/plan/StepEntity.java +++ /dev/null @@ -1,3 +0,0 @@ -package org.opentripplanner.model.plan; - -public abstract class StepEntity {} 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 8114246d7e8..1f72a2960df 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java +++ b/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java @@ -8,9 +8,9 @@ import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.lang.DoubleUtils; import org.opentripplanner.framework.tostring.ToStringBuilder; -import org.opentripplanner.model.plan.StepEntity; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.note.StreetNote; +import org.opentripplanner.transit.model.site.Entrance; /** * Represents one instruction in walking directions. Three examples from New York City: @@ -45,7 +45,7 @@ public final class WalkStep { private final boolean walkingBike; private final String exit; - private final StepEntity entity; + private final Entrance entrance; private final ElevationProfile elevationProfile; private final boolean stayOn; @@ -58,7 +58,7 @@ public final class WalkStep { I18NString directionText, Set streetNotes, String exit, - StepEntity entity, + Entrance entrance, ElevationProfile elevationProfile, boolean bogusName, boolean walkingBike, @@ -79,7 +79,7 @@ public final class WalkStep { this.walkingBike = walkingBike; this.area = area; this.exit = exit; - this.entity = entity; + this.entrance = entrance; this.elevationProfile = elevationProfile; this.stayOn = stayOn; this.edges = List.copyOf(Objects.requireNonNull(edges)); @@ -135,10 +135,10 @@ public String getExit() { } /** - * Entity related to a step e.g. building entrance/exit. + * Get information about building entrance or exit. */ - public Object getEntity() { - return entity; + public Entrance getEntrance() { + return 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 02b73c0ce15..b8055125051 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java +++ b/application/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java @@ -9,10 +9,9 @@ import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.lang.DoubleUtils; import org.opentripplanner.framework.lang.IntUtils; -import org.opentripplanner.model.plan.Entrance; -import org.opentripplanner.model.plan.StepEntity; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.note.StreetNote; +import org.opentripplanner.transit.model.site.Entrance; public class WalkStepBuilder { @@ -27,7 +26,7 @@ public class WalkStepBuilder { private RelativeDirection relativeDirection; private ElevationProfile elevationProfile; private String exit; - private StepEntity entity; + private Entrance entrance; private boolean stayOn = false; /** * Distance used for appending elevation profiles @@ -78,7 +77,7 @@ public WalkStepBuilder withExit(String exit) { } public WalkStepBuilder withEntrance(Entrance entrance) { - this.entity = entrance; + this.entrance = entrance; return this; } @@ -164,7 +163,7 @@ public WalkStep build() { directionText, streetNotes, exit, - entity, + entrance, elevationProfile, bogusName, walkingBike, 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 14ed2a553e4..67b76578d9b 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 @@ -13,7 +13,6 @@ import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.model.plan.ElevationProfile; -import org.opentripplanner.model.plan.Entrance; import org.opentripplanner.model.plan.RelativeDirection; import org.opentripplanner.model.plan.WalkStep; import org.opentripplanner.model.plan.WalkStepBuilder; @@ -30,6 +29,8 @@ 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.basic.Accessibility; +import org.opentripplanner.transit.model.site.Entrance; /** * Process a list of states into a list of walking/driving instructions for a street leg. @@ -528,10 +529,20 @@ private WalkStepBuilder createStationEntranceWalkStep( // don't care what came before or comes after var step = createWalkStep(forwardState, backState); - step.withRelativeDirection(RelativeDirection.CONTINUE); + step.withRelativeDirection(RelativeDirection.ENTER_OR_EXIT_STATION); StationEntranceVertex vertex = (StationEntranceVertex) backState.getVertex(); - step.withEntrance(Entrance.withCodeAndAccessible(vertex.getCode(), vertex.isAccessible())); + + Entrance entrance = Entrance + .of(null) + .withCode(vertex.getCode()) + .withCoordinate(new WgsCoordinate(vertex.getCoordinate())) + .withWheelchairAccessibility( + vertex.isAccessible() ? Accessibility.POSSIBLE : Accessibility.NOT_POSSIBLE + ) + .build(); + + step.withEntrance(entrance); return step; } diff --git a/application/src/main/java/org/opentripplanner/transit/model/framework/AbstractTransitEntity.java b/application/src/main/java/org/opentripplanner/transit/model/framework/AbstractTransitEntity.java index 258d505b53c..6f233aa9b31 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/framework/AbstractTransitEntity.java +++ b/application/src/main/java/org/opentripplanner/transit/model/framework/AbstractTransitEntity.java @@ -30,7 +30,7 @@ public abstract class AbstractTransitEntity< private final FeedScopedId id; public AbstractTransitEntity(FeedScopedId id) { - this.id = Objects.requireNonNull(id); + this.id = id; // TODO IS THIS WORNG } public final FeedScopedId getId() { 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 83c32e2c1ca..de1a748c1e9 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -73,9 +73,6 @@ interface PlaceInterface { "Entity related to an alert" union AlertEntity = Agency | Pattern | Route | RouteType | Stop | StopOnRoute | StopOnTrip | Trip | Unknown -"Entity to a step" -union StepEntity = Entrance - union StopPosition = PositionAtStop | PositionBetweenStops "A public transport agency" @@ -449,14 +446,14 @@ type Emissions { "Station entrance or exit." type Entrance { - "True if the entrance is wheelchair accessible." - accessible: Boolean "Short text or a number that identifies the entrance or exit for passengers. For example, `A` or `B`." code: String "ID of the entrance in the format of `FeedId:EntranceId`." gtfsId: String "Name of the entrance or exit." name: String + "Whether the entrance or exit is accessible by wheelchair" + wheelchairAccessible: WheelchairBoarding } "A 'medium' that a fare product applies to, for example cash, 'Oyster Card' or 'DB Navigator App'." @@ -2660,8 +2657,8 @@ type step { distance: Float "The elevation profile as a list of { distance, elevation } values." elevationProfile: [elevationProfileComponent] - "Step entity, e.g. an entrance." - entity: StepEntity + "Information about an station entrance or exit" + entrance: Entrance "When exiting a highway or traffic circle, the exit name/number." exit: String "The latitude of the start of the step." @@ -3329,6 +3326,7 @@ enum RelativeDirection { CONTINUE DEPART ELEVATOR + ENTER_OR_EXIT_STATION ENTER_STATION EXIT_STATION FOLLOW_SIGNS 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 d2448a92c59..a672ff3db3f 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -49,7 +49,6 @@ import org.opentripplanner.model.fare.ItineraryFares; import org.opentripplanner.model.fare.RiderCategory; import org.opentripplanner.model.plan.Emissions; -import org.opentripplanner.model.plan.Entrance; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.model.plan.Place; import org.opentripplanner.model.plan.RelativeDirection; @@ -81,6 +80,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; @@ -90,6 +90,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; @@ -232,10 +233,15 @@ public Set getRoutesForStop(StopLocation stop) { .withAbsoluteDirection(20) .build(); var step2 = walkStep("elevator").withRelativeDirection(RelativeDirection.ELEVATOR).build(); - + Entrance entrance = Entrance + .of(null) + .withCoordinate(new WgsCoordinate(60, 80)) + .withCode("A") + .withWheelchairAccessibility(Accessibility.POSSIBLE) + .build(); var step3 = walkStep("entrance") - .withRelativeDirection(RelativeDirection.CONTINUE) - .withEntrance(Entrance.withCodeAndAccessible("A", true)) + .withRelativeDirection(RelativeDirection.ENTER_OR_EXIT_STATION) + .withEntrance(entrance) .build(); Itinerary i1 = newItinerary(A, T11_00) 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 3a8e952880f..7cb304d6bb1 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 @@ -12,25 +12,24 @@ "area" : false, "relativeDirection" : "DEPART", "absoluteDirection" : "NORTHEAST", - "entity" : null + "entrance" : null }, { "streetName" : "elevator", "area" : false, "relativeDirection" : "ELEVATOR", "absoluteDirection" : null, - "entity" : null + "entrance" : null }, { "streetName" : "entrance", "area" : false, - "relativeDirection" : "CONTINUE", + "relativeDirection" : "ENTER_OR_EXIT_STATION", "absoluteDirection" : null, - "entity" : { - "__typename" : "Entrance", + "entrance": { "code": "A", - "accessible": true + "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 5e7c0493c35..c7de3f22ee1 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,12 +20,9 @@ area relativeDirection absoluteDirection - entity { - __typename - ... on Entrance { - code - accessible - } + entrance { + code + wheelchairAccessible } } } From 34761fe1b48d7ec93f735785a58f13e3b993e100 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Thu, 7 Nov 2024 14:41:35 +0200 Subject: [PATCH 017/106] Fix EntranceImpl --- .../apis/gtfs/datafetchers/EntranceImpl.java | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) 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 index 89f80286eed..16cb3f5f6e7 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java @@ -1,7 +1,6 @@ package org.opentripplanner.apis.gtfs.datafetchers; import graphql.schema.DataFetcher; -import graphql.schema.DataFetchingEnvironment; import org.opentripplanner.apis.gtfs.GraphQLUtils; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; @@ -12,42 +11,37 @@ public class EntranceImpl implements GraphQLDataFetchers.GraphQLEntrance { @Override public DataFetcher code() { return environment -> { - Entrance entrance = getEntrance(environment); - return entrance != null && entrance.getCode() != null ? entrance.getCode() : null; + Entrance entrance = environment.getSource(); + return entrance.getCode(); }; } @Override public DataFetcher gtfsId() { return environment -> { - Entrance entrance = getEntrance(environment); - return entrance != null && entrance.getId() != null ? entrance.getId().toString() : null; + Entrance entrance = environment.getSource(); + return entrance.getId() != null ? entrance.getId().toString() : null; }; } @Override public DataFetcher name() { return environment -> { - Entrance entrance = getEntrance(environment); - return entrance != null && entrance.getName() != null ? entrance.getName().toString() : null; + Entrance entrance = environment.getSource(); + return entrance.getName() != null + ? org.opentripplanner.framework.graphql.GraphQLUtils.getTranslation( + entrance.getName(), + environment + ) + : null; }; } @Override public DataFetcher wheelchairAccessible() { return environment -> { - Entrance entrance = getEntrance(environment); - return entrance != null - ? GraphQLUtils.toGraphQL(entrance.getWheelchairAccessibility()) - : null; + Entrance entrance = environment.getSource(); + return GraphQLUtils.toGraphQL(entrance.getWheelchairAccessibility()); }; } - - /** - * Helper method to retrieve the Entrance object from the DataFetchingEnvironment. - */ - private Entrance getEntrance(DataFetchingEnvironment environment) { - Object source = environment.getSource(); - return source instanceof Entrance ? (Entrance) source : null; - } } From c84b7cf878974075ccd0049d813b33f94c9bf2e3 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Fri, 8 Nov 2024 10:09:53 +0200 Subject: [PATCH 018/106] Add id to walk step entrances --- .../opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java | 2 +- .../apis/gtfs/generated/GraphQLDataFetchers.java | 2 +- .../routing/algorithm/mapping/StatesToWalkStepsMapper.java | 5 ++++- .../street/model/vertex/StationEntranceVertex.java | 4 ++++ .../transit/model/framework/AbstractTransitEntity.java | 2 +- .../resources/org/opentripplanner/apis/gtfs/schema.graphqls | 2 +- .../opentripplanner/apis/gtfs/GraphQLIntegrationTest.java | 3 ++- .../opentripplanner/apis/gtfs/expectations/walk-steps.json | 3 ++- .../org/opentripplanner/apis/gtfs/queries/walk-steps.graphql | 1 + 9 files changed, 17 insertions(+), 7 deletions(-) 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 index 16cb3f5f6e7..c062f62d07a 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java @@ -17,7 +17,7 @@ public DataFetcher code() { } @Override - public DataFetcher gtfsId() { + public DataFetcher entranceId() { return environment -> { Entrance entrance = environment.getSource(); return entrance.getId() != null ? entrance.getId().toString() : null; 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 f99fceb410c..0d547b4cf10 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 @@ -361,7 +361,7 @@ public interface GraphQLEmissions { public interface GraphQLEntrance { public DataFetcher code(); - public DataFetcher gtfsId(); + public DataFetcher entranceId(); public DataFetcher name(); 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 67b76578d9b..81f6ffcb461 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 @@ -30,6 +30,7 @@ import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.street.search.state.State; import org.opentripplanner.transit.model.basic.Accessibility; +import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.site.Entrance; /** @@ -533,8 +534,10 @@ private WalkStepBuilder createStationEntranceWalkStep( StationEntranceVertex vertex = (StationEntranceVertex) backState.getVertex(); + FeedScopedId entranceId = new FeedScopedId("osm", vertex.getId()); + Entrance entrance = Entrance - .of(null) + .of(entranceId) .withCode(vertex.getCode()) .withCoordinate(new WgsCoordinate(vertex.getCoordinate())) .withWheelchairAccessibility( diff --git a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java index 3f83a356dea..e55ac7078db 100644 --- a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java @@ -19,6 +19,10 @@ public boolean isAccessible() { return accessible; } + public String getId() { + return Long.toString(nodeId); + } + public String toString() { return "StationEntranceVertex(" + super.toString() + ", code=" + code + ")"; } diff --git a/application/src/main/java/org/opentripplanner/transit/model/framework/AbstractTransitEntity.java b/application/src/main/java/org/opentripplanner/transit/model/framework/AbstractTransitEntity.java index 6f233aa9b31..258d505b53c 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/framework/AbstractTransitEntity.java +++ b/application/src/main/java/org/opentripplanner/transit/model/framework/AbstractTransitEntity.java @@ -30,7 +30,7 @@ public abstract class AbstractTransitEntity< private final FeedScopedId id; public AbstractTransitEntity(FeedScopedId id) { - this.id = id; // TODO IS THIS WORNG + this.id = Objects.requireNonNull(id); } public final FeedScopedId getId() { 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 de1a748c1e9..b4a5ce6da15 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -449,7 +449,7 @@ type Entrance { "Short text or a number that identifies the entrance or exit for passengers. For example, `A` or `B`." code: String "ID of the entrance in the format of `FeedId:EntranceId`." - gtfsId: String + entranceId: String "Name of the entrance or exit." name: String "Whether the entrance or exit is accessible by wheelchair" 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 a672ff3db3f..d67038ec3f6 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -233,8 +233,9 @@ public Set getRoutesForStop(StopLocation stop) { .withAbsoluteDirection(20) .build(); var step2 = walkStep("elevator").withRelativeDirection(RelativeDirection.ELEVATOR).build(); + FeedScopedId entranceId = new FeedScopedId("osm", "123"); Entrance entrance = Entrance - .of(null) + .of(entranceId) .withCoordinate(new WgsCoordinate(60, 80)) .withCode("A") .withWheelchairAccessibility(Accessibility.POSSIBLE) 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 7cb304d6bb1..a0a781153f1 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 @@ -29,7 +29,8 @@ "absoluteDirection" : null, "entrance": { "code": "A", - "wheelchairAccessible": "POSSIBLE" + "wheelchairAccessible": "POSSIBLE", + "entranceId": "osm:123" } } ] 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 c7de3f22ee1..45e2eed904a 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 @@ -23,6 +23,7 @@ entrance { code wheelchairAccessible + entranceId } } } From 2ea8a522e6616f2832b7410ed896c6434758c238 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Fri, 8 Nov 2024 10:37:19 +0200 Subject: [PATCH 019/106] Remove old file --- .../datafetchers/StepEntityTypeResolver.java | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java deleted file mode 100644 index 43762c97a7d..00000000000 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepEntityTypeResolver.java +++ /dev/null @@ -1,21 +0,0 @@ -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 StepEntityTypeResolver 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; - } -} From 39b0db37f86b28fe5f03f7da2716f47bdbfed3cc Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Fri, 8 Nov 2024 10:38:59 +0200 Subject: [PATCH 020/106] Fix otp version --- .../standalone/config/buildconfig/OsmConfig.java | 3 ++- doc/user/BuildConfiguration.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) 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 c1a3963c6a5..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; @@ -88,7 +89,7 @@ public static OsmExtractParametersBuilder mapOsmGenericParameters( .withIncludeOsmSubwayEntrances( node .of("includeOsmSubwayEntrances") - .since(V2_2) + .since(V2_7) .summary("Whether to include subway entrances from the OSM data." + documentationAddition) .docDefaultValue(docDefaults.includeOsmSubwayEntrances()) .asBoolean(defaults.includeOsmSubwayEntrances()) diff --git a/doc/user/BuildConfiguration.md b/doc/user/BuildConfiguration.md index 8b38da6fdbb..c5fdfa8095b 100644 --- a/doc/user/BuildConfiguration.md +++ b/doc/user/BuildConfiguration.md @@ -84,12 +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.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.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 | From 3b6bf3fbab24a97c67957effb7838b8383b652ff Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Fri, 8 Nov 2024 10:39:31 +0200 Subject: [PATCH 021/106] Remove unused import --- .../org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java | 1 - 1 file changed, 1 deletion(-) 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 9e4119100c0..32e1fc69870 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 @@ -1,7 +1,6 @@ // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. package org.opentripplanner.apis.gtfs.generated; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; From c9df52d9e8e3a2359ef39c64e78295230090ff2e Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Sun, 10 Nov 2024 17:02:44 +0200 Subject: [PATCH 022/106] Require entranceId --- .../opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java | 2 +- .../resources/org/opentripplanner/apis/gtfs/schema.graphqls | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index c062f62d07a..d7e2e6aa3ac 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java @@ -20,7 +20,7 @@ public DataFetcher code() { public DataFetcher entranceId() { return environment -> { Entrance entrance = environment.getSource(); - return entrance.getId() != null ? entrance.getId().toString() : null; + return entrance.getId().toString(); }; } diff --git a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 80094ecfd3f..c7cb1a6e346 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -449,7 +449,7 @@ type Entrance { "Short text or a number that identifies the entrance or exit for passengers. For example, `A` or `B`." code: String "ID of the entrance in the format of `FeedId:EntranceId`." - entranceId: String + entranceId: String! "Name of the entrance or exit." name: String "Whether the entrance or exit is accessible by wheelchair" From 7a9a8f694ad29a782827605bdc70e7cd8adea370 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Sun, 10 Nov 2024 17:59:58 +0200 Subject: [PATCH 023/106] Rename methods --- .../opentripplanner/ext/restapi/mapping/WalkStepMapper.java | 2 +- .../opentripplanner/apis/gtfs/datafetchers/stepImpl.java | 4 ++-- .../apis/transmodel/model/plan/PathGuidanceType.java | 2 +- .../main/java/org/opentripplanner/model/plan/WalkStep.java | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) 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 c5c59e4dc37..1df3d7e6fa7 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.getHighwayExit(); api.stayOn = domain.isStayOn(); api.area = domain.getArea(); api.bogusName = domain.getBogusName(); 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 994434b4e4c..4414fc7b4cd 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,12 +50,12 @@ public DataFetcher> elevationProfile() { @Override public DataFetcher exit() { - return environment -> getSource(environment).getExit(); + return environment -> getSource(environment).getHighwayExit(); } @Override public DataFetcher entrance() { - return environment -> getSource(environment).getEntrance(); + return environment -> getSource(environment).getStationEntrance(); } @Override 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 c52baa0be4c..12eaa089a50 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 @@ -65,7 +65,7 @@ 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()).getHighwayExit()) .build() ) .field( 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 9e055e33559..96bde6d1ef3 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java +++ b/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java @@ -130,14 +130,14 @@ public Optional getAbsoluteDirection() { /** * When exiting a highway or traffic circle, the exit name/number. */ - public String getExit() { + public String getHighwayExit() { return exit; } /** - * Get information about building entrance or exit. + * Get information about a subway station entrance or exit. */ - public Entrance getEntrance() { + public Entrance getStationEntrance() { return entrance; } From c9139e31f60662e7cae7cf35ace311ec9adc1d37 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Mon, 11 Nov 2024 15:04:33 +0200 Subject: [PATCH 024/106] Update dosumentation --- .../routing/algorithm/mapping/StatesToWalkStepsMapper.java | 3 ++- .../resources/org/opentripplanner/apis/gtfs/schema.graphqls | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) 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 81f6ffcb461..b0df9658301 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 @@ -529,7 +529,8 @@ private WalkStepBuilder createStationEntranceWalkStep( ) { // don't care what came before or comes after var step = 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. step.withRelativeDirection(RelativeDirection.ENTER_OR_EXIT_STATION); StationEntranceVertex vertex = (StationEntranceVertex) backState.getVertex(); 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 c7cb1a6e346..13b576a46c7 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -444,11 +444,11 @@ type Emissions { co2: Grams } -"Station entrance or exit." +"Station entrance or exit, originating from OSM or GTFS data." type Entrance { "Short text or a number that identifies the entrance or exit for passengers. For example, `A` or `B`." code: String - "ID of the entrance in the format of `FeedId:EntranceId`." + "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 From 7b210246ee0c5be3b54b4eff66722ffdd7e39e90 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Mon, 11 Nov 2024 15:18:55 +0200 Subject: [PATCH 025/106] Update documentation --- .../routing/algorithm/mapping/StatesToWalkStepsMapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b0df9658301..39941b72ecd 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 @@ -529,7 +529,7 @@ private WalkStepBuilder createStationEntranceWalkStep( ) { // don't care what came before or comes after var step = createWalkStep(forwardState, backState); - // There is not a way to definitively determine if a user is entering or exiting the station, + // 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. step.withRelativeDirection(RelativeDirection.ENTER_OR_EXIT_STATION); From ce7719cf50c103b24a4a25530ec9be29aaa67d5a Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Mon, 11 Nov 2024 15:20:23 +0200 Subject: [PATCH 026/106] Remove redundant null check --- .../apis/gtfs/datafetchers/EntranceImpl.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 index d7e2e6aa3ac..9891d107479 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java @@ -28,12 +28,10 @@ public DataFetcher entranceId() { public DataFetcher name() { return environment -> { Entrance entrance = environment.getSource(); - return entrance.getName() != null - ? org.opentripplanner.framework.graphql.GraphQLUtils.getTranslation( - entrance.getName(), - environment - ) - : null; + return org.opentripplanner.framework.graphql.GraphQLUtils.getTranslation( + entrance.getName(), + environment + ); }; } From f7ed65d74e8a3e26b0091b5b56444c3a1fdb2055 Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Mon, 11 Nov 2024 15:55:04 +0000 Subject: [PATCH 027/106] add Platform and PlatformEdge into the graph --- .../graph_builder/module/osm/OsmModule.java | 53 ++++++++++++++++--- .../street/model/edge/LinearPlatform.java | 9 ++++ .../street/model/edge/LinearPlatformEdge.java | 11 ++++ .../model/edge/LinearPlatformEdgeBuilder.java | 20 +++++++ 4 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatform.java create mode 100644 application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdge.java create mode 100644 application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdgeBuilder.java 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 08d23087a45..11c8428b0de 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; @@ -28,6 +29,8 @@ import org.opentripplanner.routing.vehicle_parking.VehicleParking; import org.opentripplanner.street.model.StreetLimitationParameters; import org.opentripplanner.street.model.StreetTraversalPermission; +import org.opentripplanner.street.model.edge.LinearPlatform; +import org.opentripplanner.street.model.edge.LinearPlatformEdgeBuilder; import org.opentripplanner.street.model.edge.StreetEdge; import org.opentripplanner.street.model.edge.StreetEdgeBuilder; import org.opentripplanner.street.model.vertex.BarrierVertex; @@ -304,6 +307,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)); @@ -384,7 +389,8 @@ private void buildBasicGraph() { way, i, permissions, - geometry + geometry, + platform ); params.edgeNamer().recordEdges(way, streets); @@ -407,6 +413,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 LinearPlatform( + 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()); @@ -464,7 +497,8 @@ private StreetEdgePair getEdgesForStreet( OsmWay way, int index, StreetTraversalPermission permissions, - LineString geometry + LineString geometry, + Optional platform ) { // No point in returning edges that can't be traversed by anyone. if (permissions.allowsNothing()) { @@ -490,7 +524,8 @@ private StreetEdgePair getEdgesForStreet( length, permissionsFront, geometry, - false + false, + platform ); } if (permissionsBack.allowsAnything()) { @@ -503,7 +538,8 @@ private StreetEdgePair getEdgesForStreet( length, permissionsBack, backGeometry, - true + true, + platform ); } if (street != null && backStreet != null) { @@ -520,14 +556,19 @@ private StreetEdge getEdgeForStreet( double length, StreetTraversalPermission permissions, LineString geometry, - boolean back + boolean back, + Optional platform ) { String label = "way " + way.getId() + " from " + index; label = label.intern(); I18NString name = params.edgeNamer().getNameForWay(way, label); float carSpeed = way.getOsmProvider().getOsmTagMapper().getCarSpeedForWay(way, back); - StreetEdgeBuilder seb = new StreetEdgeBuilder<>() + var seb = platform + .>map(p -> new LinearPlatformEdgeBuilder().withPlatform(p)) + .orElse(new StreetEdgeBuilder<>()); + + seb .withFromVertex(startEndpoint) .withToVertex(endEndpoint) .withGeometry(geometry) diff --git a/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatform.java b/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatform.java new file mode 100644 index 00000000000..688d4418530 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatform.java @@ -0,0 +1,9 @@ +package org.opentripplanner.street.model.edge; + +import java.io.Serializable; +import java.util.Set; +import org.locationtech.jts.geom.LineString; +import org.opentripplanner.framework.i18n.I18NString; + +public record LinearPlatform(I18NString name, LineString geometry, Set references) + implements Serializable {} diff --git a/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdge.java b/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdge.java new file mode 100644 index 00000000000..849266166a2 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdge.java @@ -0,0 +1,11 @@ +package org.opentripplanner.street.model.edge; + +public class LinearPlatformEdge extends StreetEdge { + + public final LinearPlatform platform; + + protected LinearPlatformEdge(LinearPlatformEdgeBuilder builder) { + super(builder); + platform = builder.platform(); + } +} diff --git a/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdgeBuilder.java b/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdgeBuilder.java new file mode 100644 index 00000000000..7057f9a7e42 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdgeBuilder.java @@ -0,0 +1,20 @@ +package org.opentripplanner.street.model.edge; + +public class LinearPlatformEdgeBuilder extends StreetEdgeBuilder { + + private LinearPlatform platform; + + @Override + public LinearPlatformEdge buildAndConnect() { + return Edge.connectToGraph(new LinearPlatformEdge(this)); + } + + public LinearPlatform platform() { + return platform; + } + + public LinearPlatformEdgeBuilder withPlatform(LinearPlatform platform) { + this.platform = platform; + return this; + } +} From 2d423a896f599c4acc77fcfc19766732fc374ff9 Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Mon, 11 Nov 2024 17:12:28 +0000 Subject: [PATCH 028/106] link linear platforms to stops --- .../module/OsmBoardingLocationsModule.java | 114 +++++++++++++----- .../routing/linking/VertexLinker.java | 49 +++++++- .../edge/BoardingLocationToStopLink.java | 10 +- 3 files changed, 137 insertions(+), 36 deletions(-) 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..eb91d0b1d30 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,8 +1,12 @@ package org.opentripplanner.graph_builder.module; import jakarta.inject.Inject; +import java.util.ArrayList; +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.opentripplanner.framework.geometry.GeometryUtils; @@ -17,6 +21,8 @@ import org.opentripplanner.street.model.edge.AreaEdge; import org.opentripplanner.street.model.edge.BoardingLocationToStopLink; import org.opentripplanner.street.model.edge.Edge; +import org.opentripplanner.street.model.edge.LinearPlatform; +import org.opentripplanner.street.model.edge.LinearPlatformEdge; import org.opentripplanner.street.model.edge.NamedArea; import org.opentripplanner.street.model.edge.StreetEdge; import org.opentripplanner.street.model.edge.StreetEdgeBuilder; @@ -24,9 +30,11 @@ 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.StationElement; import org.opentripplanner.transit.service.TimetableRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -99,14 +107,15 @@ public void buildGraph() { } private boolean connectVertexToStop(TransitStopVertex ts, StreetIndex index) { - var stopCode = ts.getStop().getCode(); - var stopId = ts.getStop().getId().getId(); + var stop = ts.getStop(); + var stopCode = stop.getCode(); + var stopId = stop.getId().getId(); Envelope envelope = new Envelope(ts.getCoordinate()); double xscale = Math.cos(ts.getCoordinate().y * Math.PI / 180); envelope.expandBy(searchRadiusDegrees / xscale, searchRadiusDegrees); - // if the boarding location is an OSM node it's generated in the OSM processing step but we need + // if the boarding location is a node it's generated in the OSM processing step but we need // link it here var nearbyBoardingLocations = index .getVerticesForEnvelope(envelope) @@ -116,26 +125,14 @@ private boolean connectVertexToStop(TransitStopVertex ts, StreetIndex index) { .collect(Collectors.toSet()); for (var boardingLocation : nearbyBoardingLocations) { - if ( - (stopCode != null && boardingLocation.references.contains(stopCode)) || - boardingLocation.references.contains(stopId) - ) { + if (matchesReference(stop, boardingLocation.references)) { 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) - ); - } + (osmBoardingLocationVertex, splitVertex) -> + getConnectingEdges(boardingLocation, osmBoardingLocationVertex, splitVertex) ); } linkBoardingLocationToStop(ts, stopCode, boardingLocation); @@ -143,9 +140,50 @@ private boolean connectVertexToStop(TransitStopVertex ts, StreetIndex index) { } } - // if the boarding location is an OSM way (an area) then we are generating the vertex here and + // if the boarding location is a non-area way we are finding the vertex representing the + // center of the way, splitting if needed + var nearbyLinearPlatformEdges = new HashMap>(); + + for (var edge : index.getEdgesForEnvelope(envelope)) { + if (edge instanceof LinearPlatformEdge platformEdge) { + var platform = platformEdge.platform; + if (matchesReference(stop, platform.references())) { + if (!nearbyLinearPlatformEdges.containsKey(platform)) { + var list = new ArrayList(); + list.add(platformEdge); + nearbyLinearPlatformEdges.put(platform, list); + } else { + nearbyLinearPlatformEdges.get(platform).add(platformEdge); + } + } + } + } + + for (var platformEdgeList : nearbyLinearPlatformEdges.entrySet()) { + LinearPlatform platform = platformEdgeList.getKey(); + var name = platform.name(); + var label = "platform-centroid/%s".formatted(stop.getId().toString()); + var centroid = platform.geometry().getCentroid(); + var boardingLocation = vertexFactory.osmBoardingLocation( + new Coordinate(centroid.getX(), centroid.getY()), + label, + 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, stopCode, vertex); + } + return true; + } + + // if the boarding location is 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 + var nearbyAreaEdgeList = index .getEdgesForEnvelope(envelope) .stream() .filter(AreaEdge.class::isInstance) @@ -155,18 +193,15 @@ 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 label = "platform-centroid/%s".formatted(stop.getId().toString()); var centroid = edgeList.getGeometry().getCentroid(); var boardingLocation = vertexFactory.osmBoardingLocation( new Coordinate(centroid.getX(), centroid.getY()), @@ -182,6 +217,22 @@ private boolean connectVertexToStop(TransitStopVertex ts, StreetIndex index) { return false; } + 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 +248,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 +261,11 @@ private void linkBoardingLocationToStop( boardingLocation.getCoordinate() ); } + + private boolean matchesReference(StationElement stop, Set 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/routing/linking/VertexLinker.java b/application/src/main/java/org/opentripplanner/routing/linking/VertexLinker.java index 48f5ff997c8..a433f3882e1 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,38 @@ private DisposableEdgeCollection link( return tempEdges; } + 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 +276,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 +312,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/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)); } From 89b3ac7e1c193c8e9bfa274a4c5142b84f9eba45 Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Mon, 11 Nov 2024 17:32:03 +0000 Subject: [PATCH 029/106] render PlatformEdge on debug layer --- .../org/opentripplanner/apis/vectortiles/DebugStyleSpec.java | 2 ++ .../resources/org/opentripplanner/apis/vectortiles/style.json | 3 +++ 2 files changed, 5 insertions(+) diff --git a/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java b/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java index 3c8423c5270..079012e7404 100644 --- a/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java +++ b/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java @@ -18,6 +18,7 @@ import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.edge.ElevatorHopEdge; import org.opentripplanner.street.model.edge.EscalatorEdge; +import org.opentripplanner.street.model.edge.LinearPlatformEdge; import org.opentripplanner.street.model.edge.PathwayEdge; import org.opentripplanner.street.model.edge.StreetEdge; import org.opentripplanner.street.model.edge.StreetStationCentroidLink; @@ -61,6 +62,7 @@ public class DebugStyleSpec { private static final Class[] EDGES_TO_DISPLAY = new Class[] { StreetEdge.class, AreaEdge.class, + LinearPlatformEdge.class, EscalatorEdge.class, PathwayEdge.class, ElevatorHopEdge.class, diff --git a/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json b/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json index 8a0e457396e..e349a886ec4 100644 --- a/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json +++ b/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json @@ -437,6 +437,7 @@ "class", "StreetEdge", "AreaEdge", + "LinearPlatformEdge", "EscalatorEdge", "PathwayEdge", "ElevatorHopEdge", @@ -518,6 +519,7 @@ "class", "StreetEdge", "AreaEdge", + "LinearPlatformEdge", "EscalatorEdge", "PathwayEdge", "ElevatorHopEdge", @@ -550,6 +552,7 @@ "class", "StreetEdge", "AreaEdge", + "LinearPlatformEdge", "EscalatorEdge", "PathwayEdge", "ElevatorHopEdge", From b73741100d9cb5a6a3691ded6976691880ed7682 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Thu, 14 Nov 2024 09:49:28 +0200 Subject: [PATCH 030/106] Add feature union to steps --- .../apis/gtfs/GtfsGraphQLIndex.java | 2 ++ .../apis/gtfs/datafetchers/EntranceImpl.java | 13 +++++++--- .../datafetchers/StepFeatureTypeResolver.java | 25 +++++++++++++++++++ .../apis/gtfs/datafetchers/stepImpl.java | 4 +-- .../gtfs/generated/GraphQLDataFetchers.java | 8 +++--- .../apis/gtfs/model/StepFeature.java | 20 +++++++++++++++ .../opentripplanner/model/plan/WalkStep.java | 14 +++++------ .../model/plan/WalkStepBuilder.java | 7 +++--- .../opentripplanner/apis/gtfs/schema.graphqls | 7 ++++-- .../apis/gtfs/expectations/walk-steps.json | 11 ++++---- .../apis/gtfs/queries/walk-steps.graphql | 11 +++++--- 11 files changed, 92 insertions(+), 30 deletions(-) create mode 100644 application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepFeatureTypeResolver.java create mode 100644 application/src/main/java/org/opentripplanner/apis/gtfs/model/StepFeature.java 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 a5eedb4c71c..22e8f2e6fa7 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java @@ -59,6 +59,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.StopGeometriesImpl; import org.opentripplanner.apis.gtfs.datafetchers.StopImpl; import org.opentripplanner.apis.gtfs.datafetchers.StopOnRouteImpl; @@ -127,6 +128,7 @@ protected static GraphQLSchema buildSchema() { .type("StopPosition", type -> type.typeResolver(new StopPosition() {})) .type("FareProduct", type -> type.typeResolver(new FareProductTypeResolver())) .type("AlertEntity", type -> type.typeResolver(new AlertEntityTypeResolver())) + .type("StepFeature", type -> type.typeResolver(new StepFeatureTypeResolver())) .type(typeWiring.build(AgencyImpl.class)) .type(typeWiring.build(AlertImpl.class)) .type(typeWiring.build(BikeParkImpl.class)) 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 index 9891d107479..bcf37d42cc7 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java @@ -4,6 +4,7 @@ import org.opentripplanner.apis.gtfs.GraphQLUtils; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.apis.gtfs.model.StepFeature; import org.opentripplanner.transit.model.site.Entrance; public class EntranceImpl implements GraphQLDataFetchers.GraphQLEntrance { @@ -11,7 +12,8 @@ public class EntranceImpl implements GraphQLDataFetchers.GraphQLEntrance { @Override public DataFetcher code() { return environment -> { - Entrance entrance = environment.getSource(); + StepFeature feature = environment.getSource(); + Entrance entrance = (Entrance) feature.getFeature(); return entrance.getCode(); }; } @@ -19,7 +21,8 @@ public DataFetcher code() { @Override public DataFetcher entranceId() { return environment -> { - Entrance entrance = environment.getSource(); + StepFeature feature = environment.getSource(); + Entrance entrance = (Entrance) feature.getFeature(); return entrance.getId().toString(); }; } @@ -27,7 +30,8 @@ public DataFetcher entranceId() { @Override public DataFetcher name() { return environment -> { - Entrance entrance = environment.getSource(); + StepFeature feature = environment.getSource(); + Entrance entrance = (Entrance) feature.getFeature(); return org.opentripplanner.framework.graphql.GraphQLUtils.getTranslation( entrance.getName(), environment @@ -38,7 +42,8 @@ public DataFetcher name() { @Override public DataFetcher wheelchairAccessible() { return environment -> { - Entrance entrance = environment.getSource(); + StepFeature feature = environment.getSource(); + Entrance entrance = (Entrance) feature.getFeature(); 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..8748d87700d --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepFeatureTypeResolver.java @@ -0,0 +1,25 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.TypeResolutionEnvironment; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; +import graphql.schema.TypeResolver; +import org.opentripplanner.apis.gtfs.model.StepFeature; +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 StepFeature) { + Object feature = ((StepFeature) o).getFeature(); + if (feature 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 4414fc7b4cd..74453d6e7c4 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 @@ -54,8 +54,8 @@ public DataFetcher exit() { } @Override - public DataFetcher entrance() { - return environment -> getSource(environment).getStationEntrance(); + public DataFetcher feature() { + return environment -> getSource(environment).getStepFeature(); } @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 0d547b4cf10..b7889c8be3a 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 @@ -357,7 +357,7 @@ public interface GraphQLEmissions { public DataFetcher co2(); } - /** Station entrance or exit. */ + /** Station entrance or exit, originating from OSM or GTFS data. */ public interface GraphQLEntrance { public DataFetcher code(); @@ -984,6 +984,8 @@ public interface GraphQLRoutingError { public DataFetcher inputField(); } + 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. @@ -1435,10 +1437,10 @@ public interface GraphQLStep { public DataFetcher> elevationProfile(); - public DataFetcher entrance(); - public DataFetcher exit(); + public DataFetcher feature(); + public DataFetcher lat(); public DataFetcher lon(); diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/model/StepFeature.java b/application/src/main/java/org/opentripplanner/apis/gtfs/model/StepFeature.java new file mode 100644 index 00000000000..c4842e25476 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/model/StepFeature.java @@ -0,0 +1,20 @@ +package org.opentripplanner.apis.gtfs.model; + +import org.opentripplanner.transit.model.site.Entrance; + +/** + * A generic wrapper class for features in Walk steps. + * At the moment only subway station entrances. + **/ +public class StepFeature { + + private final Object feature; + + public StepFeature(Entrance entrance) { + this.feature = entrance; + } + + public Object getFeature() { + return feature; + } +} 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 96bde6d1ef3..660b973a8a8 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java +++ b/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java @@ -4,11 +4,11 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import org.opentripplanner.apis.gtfs.model.StepFeature; import org.opentripplanner.framework.geometry.WgsCoordinate; 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; @@ -45,7 +45,7 @@ public final class WalkStep { private final boolean walkingBike; private final String exit; - private final Entrance entrance; + private final StepFeature feature; private final ElevationProfile elevationProfile; private final boolean stayOn; @@ -58,7 +58,7 @@ public final class WalkStep { I18NString directionText, Set streetNotes, String exit, - Entrance entrance, + StepFeature feature, ElevationProfile elevationProfile, boolean bogusName, boolean walkingBike, @@ -79,7 +79,7 @@ public final class WalkStep { this.walkingBike = walkingBike; this.area = area; this.exit = exit; - this.entrance = entrance; + this.feature = feature; this.elevationProfile = elevationProfile; this.stayOn = stayOn; this.edges = List.copyOf(Objects.requireNonNull(edges)); @@ -135,10 +135,10 @@ public String getHighwayExit() { } /** - * Get information about a subway station entrance or exit. + * Get information about feature e.g. a subway station entrance or exit. */ - public Entrance getStationEntrance() { - return entrance; + public StepFeature getStepFeature() { + return feature; } /** 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 7ef5012e145..6752eba95e9 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java +++ b/application/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Set; import javax.annotation.Nullable; +import org.opentripplanner.apis.gtfs.model.StepFeature; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.street.model.edge.Edge; @@ -26,7 +27,7 @@ public class WalkStepBuilder { private RelativeDirection relativeDirection; private ElevationProfile elevationProfile; private String exit; - private Entrance entrance; + private StepFeature feature; private boolean stayOn = false; /** * Distance used for appending elevation profiles @@ -77,7 +78,7 @@ public WalkStepBuilder withExit(String exit) { } public WalkStepBuilder withEntrance(Entrance entrance) { - this.entrance = entrance; + this.feature = new StepFeature(entrance); return this; } @@ -163,7 +164,7 @@ public WalkStep build() { directionText, streetNotes, exit, - entrance, + feature, elevationProfile, bogusName, walkingBike, 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 13b576a46c7..83fe952ce7e 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -73,6 +73,9 @@ interface PlaceInterface { "Entity related to an alert" union AlertEntity = Agency | Pattern | Route | RouteType | Stop | StopOnRoute | StopOnTrip | Trip | Unknown +"A feature for a step" +union StepFeature = Entrance + union StopPosition = PositionAtStop | PositionBetweenStops "A public transport agency" @@ -2657,10 +2660,10 @@ type step { distance: Float "The elevation profile as a list of { distance, elevation } values." elevationProfile: [elevationProfileComponent] - "Information about an station entrance or exit" - entrance: Entrance "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." 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 a0a781153f1..8d79102fc59 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 @@ -12,14 +12,14 @@ "area" : false, "relativeDirection" : "DEPART", "absoluteDirection" : "NORTHEAST", - "entrance" : null + "feature" : null }, { "streetName" : "elevator", "area" : false, "relativeDirection" : "ELEVATOR", "absoluteDirection" : null, - "entrance" : null + "feature" : null }, { @@ -27,10 +27,11 @@ "area" : false, "relativeDirection" : "ENTER_OR_EXIT_STATION", "absoluteDirection" : null, - "entrance": { + "feature": { + "__typename": "Entrance", "code": "A", - "wheelchairAccessible": "POSSIBLE", - "entranceId": "osm:123" + "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 45e2eed904a..565e620fed3 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,10 +20,13 @@ area relativeDirection absoluteDirection - entrance { - code - wheelchairAccessible - entranceId + feature { + __typename + ... on Entrance { + code + entranceId + wheelchairAccessible + } } } } From f547e074f2879c9e4f6bec37759a7d64fc927258 Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Thu, 14 Nov 2024 12:42:47 +0200 Subject: [PATCH 031/106] Return feature based on relativeDirection --- .../apis/gtfs/datafetchers/EntranceImpl.java | 13 ++++--------- .../gtfs/datafetchers/StepFeatureTypeResolver.java | 8 ++------ .../apis/gtfs/datafetchers/stepImpl.java | 9 ++++++++- .../apis/gtfs/model/StepFeature.java | 8 ++++---- 4 files changed, 18 insertions(+), 20 deletions(-) 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 index bcf37d42cc7..9891d107479 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java @@ -4,7 +4,6 @@ import org.opentripplanner.apis.gtfs.GraphQLUtils; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; -import org.opentripplanner.apis.gtfs.model.StepFeature; import org.opentripplanner.transit.model.site.Entrance; public class EntranceImpl implements GraphQLDataFetchers.GraphQLEntrance { @@ -12,8 +11,7 @@ public class EntranceImpl implements GraphQLDataFetchers.GraphQLEntrance { @Override public DataFetcher code() { return environment -> { - StepFeature feature = environment.getSource(); - Entrance entrance = (Entrance) feature.getFeature(); + Entrance entrance = environment.getSource(); return entrance.getCode(); }; } @@ -21,8 +19,7 @@ public DataFetcher code() { @Override public DataFetcher entranceId() { return environment -> { - StepFeature feature = environment.getSource(); - Entrance entrance = (Entrance) feature.getFeature(); + Entrance entrance = environment.getSource(); return entrance.getId().toString(); }; } @@ -30,8 +27,7 @@ public DataFetcher entranceId() { @Override public DataFetcher name() { return environment -> { - StepFeature feature = environment.getSource(); - Entrance entrance = (Entrance) feature.getFeature(); + Entrance entrance = environment.getSource(); return org.opentripplanner.framework.graphql.GraphQLUtils.getTranslation( entrance.getName(), environment @@ -42,8 +38,7 @@ public DataFetcher name() { @Override public DataFetcher wheelchairAccessible() { return environment -> { - StepFeature feature = environment.getSource(); - Entrance entrance = (Entrance) feature.getFeature(); + 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 index 8748d87700d..714518cb9ea 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepFeatureTypeResolver.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StepFeatureTypeResolver.java @@ -4,7 +4,6 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.TypeResolver; -import org.opentripplanner.apis.gtfs.model.StepFeature; import org.opentripplanner.transit.model.site.Entrance; public class StepFeatureTypeResolver implements TypeResolver { @@ -14,11 +13,8 @@ public GraphQLObjectType getType(TypeResolutionEnvironment environment) { Object o = environment.getObject(); GraphQLSchema schema = environment.getSchema(); - if (o instanceof StepFeature) { - Object feature = ((StepFeature) o).getFeature(); - if (feature instanceof Entrance) { - return schema.getObjectType("Entrance"); - } + 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 74453d6e7c4..6a1c180fad6 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 @@ -7,6 +7,7 @@ import org.opentripplanner.apis.gtfs.mapping.DirectionMapper; import org.opentripplanner.apis.gtfs.mapping.StreetNoteMapper; import org.opentripplanner.model.plan.ElevationProfile.Step; +import org.opentripplanner.model.plan.RelativeDirection; import org.opentripplanner.model.plan.WalkStep; import org.opentripplanner.routing.alertpatch.TransitAlert; @@ -55,7 +56,13 @@ public DataFetcher exit() { @Override public DataFetcher feature() { - return environment -> getSource(environment).getStepFeature(); + return environment -> { + WalkStep source = getSource(environment); + if (source.getRelativeDirection() == RelativeDirection.ENTER_OR_EXIT_STATION) { + return source.getStepFeature().getEntrance(); + } + return null; + }; } @Override diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/model/StepFeature.java b/application/src/main/java/org/opentripplanner/apis/gtfs/model/StepFeature.java index c4842e25476..bf5fd8cc104 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/model/StepFeature.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/model/StepFeature.java @@ -8,13 +8,13 @@ **/ public class StepFeature { - private final Object feature; + private final Entrance entranceFeature; public StepFeature(Entrance entrance) { - this.feature = entrance; + this.entranceFeature = entrance; } - public Object getFeature() { - return feature; + public Entrance getEntrance() { + return entranceFeature; } } From 18b84f00a2f83841ef5a993cd5d958899a3805fd Mon Sep 17 00:00:00 2001 From: Henrik Sundell Date: Thu, 14 Nov 2024 14:20:00 +0200 Subject: [PATCH 032/106] Remove StepFeature class --- .../apis/gtfs/datafetchers/stepImpl.java | 2 +- .../apis/gtfs/model/StepFeature.java | 20 ------------------- .../opentripplanner/model/plan/WalkStep.java | 14 ++++++------- .../model/plan/WalkStepBuilder.java | 7 +++---- 4 files changed, 11 insertions(+), 32 deletions(-) delete mode 100644 application/src/main/java/org/opentripplanner/apis/gtfs/model/StepFeature.java 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 6a1c180fad6..c98237f78c5 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 @@ -59,7 +59,7 @@ public DataFetcher feature() { return environment -> { WalkStep source = getSource(environment); if (source.getRelativeDirection() == RelativeDirection.ENTER_OR_EXIT_STATION) { - return source.getStepFeature().getEntrance(); + return source.getEntrance(); } return null; }; diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/model/StepFeature.java b/application/src/main/java/org/opentripplanner/apis/gtfs/model/StepFeature.java deleted file mode 100644 index bf5fd8cc104..00000000000 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/model/StepFeature.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.opentripplanner.apis.gtfs.model; - -import org.opentripplanner.transit.model.site.Entrance; - -/** - * A generic wrapper class for features in Walk steps. - * At the moment only subway station entrances. - **/ -public class StepFeature { - - private final Entrance entranceFeature; - - public StepFeature(Entrance entrance) { - this.entranceFeature = entrance; - } - - public Entrance getEntrance() { - return entranceFeature; - } -} 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 660b973a8a8..2efe6e36aff 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java +++ b/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java @@ -4,11 +4,11 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import org.opentripplanner.apis.gtfs.model.StepFeature; import org.opentripplanner.framework.geometry.WgsCoordinate; 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; @@ -45,7 +45,7 @@ public final class WalkStep { private final boolean walkingBike; private final String exit; - private final StepFeature feature; + private final Entrance entrance; private final ElevationProfile elevationProfile; private final boolean stayOn; @@ -58,7 +58,7 @@ public final class WalkStep { I18NString directionText, Set streetNotes, String exit, - StepFeature feature, + Entrance entrance, ElevationProfile elevationProfile, boolean bogusName, boolean walkingBike, @@ -79,7 +79,7 @@ public final class WalkStep { this.walkingBike = walkingBike; this.area = area; this.exit = exit; - this.feature = feature; + this.entrance = entrance; this.elevationProfile = elevationProfile; this.stayOn = stayOn; this.edges = List.copyOf(Objects.requireNonNull(edges)); @@ -135,10 +135,10 @@ public String getHighwayExit() { } /** - * Get information about feature e.g. a subway station entrance or exit. + * Get information about a subway station entrance or exit. */ - public StepFeature getStepFeature() { - return feature; + public Entrance getEntrance() { + return 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 6752eba95e9..7ef5012e145 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java +++ b/application/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java @@ -5,7 +5,6 @@ import java.util.List; import java.util.Set; import javax.annotation.Nullable; -import org.opentripplanner.apis.gtfs.model.StepFeature; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.street.model.edge.Edge; @@ -27,7 +26,7 @@ public class WalkStepBuilder { private RelativeDirection relativeDirection; private ElevationProfile elevationProfile; private String exit; - private StepFeature feature; + private Entrance entrance; private boolean stayOn = false; /** * Distance used for appending elevation profiles @@ -78,7 +77,7 @@ public WalkStepBuilder withExit(String exit) { } public WalkStepBuilder withEntrance(Entrance entrance) { - this.feature = new StepFeature(entrance); + this.entrance = entrance; return this; } @@ -164,7 +163,7 @@ public WalkStep build() { directionText, streetNotes, exit, - feature, + entrance, elevationProfile, bogusName, walkingBike, From 12b49957ece1db3f3295c251c93186ac0527d964 Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Tue, 19 Nov 2024 11:37:22 +0000 Subject: [PATCH 033/106] make all polling updater wait for graph update finish --- .../alert/GtfsRealtimeAlertsUpdater.java | 45 +++++++++---------- .../updater/trip/PollingTripUpdater.java | 5 ++- .../VehicleParkingAvailabilityUpdater.java | 5 ++- .../VehicleParkingUpdater.java | 5 ++- .../PollingVehiclePositionUpdater.java | 5 ++- .../vehicle_rental/VehicleRentalUpdater.java | 5 ++- 6 files changed, 36 insertions(+), 34 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java b/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java index b71dad6a656..0e7ab35cb13 100644 --- a/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java @@ -2,6 +2,7 @@ import com.google.transit.realtime.GtfsRealtime.FeedMessage; import java.net.URI; +import java.util.concurrent.ExecutionException; import org.opentripplanner.framework.io.OtpHttpClient; import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.routing.impl.TransitAlertServiceImpl; @@ -63,32 +64,28 @@ public String toString() { } @Override - protected void runPolling() { - try { - final FeedMessage feed = otpHttpClient.getAndMap( - URI.create(url), - this.headers.asMap(), - FeedMessage.PARSER::parseFrom - ); + protected void runPolling() throws InterruptedException, ExecutionException { + final FeedMessage feed = otpHttpClient.getAndMap( + URI.create(url), + this.headers.asMap(), + FeedMessage.PARSER::parseFrom + ); - long feedTimestamp = feed.getHeader().getTimestamp(); - if (feedTimestamp == lastTimestamp) { - LOG.debug("Ignoring feed with a timestamp that has not been updated from {}", url); - return; - } - if (feedTimestamp < lastTimestamp) { - LOG.info("Ignoring feed with older than previous timestamp from {}", url); - return; - } + long feedTimestamp = feed.getHeader().getTimestamp(); + if (feedTimestamp == lastTimestamp) { + LOG.debug("Ignoring feed with a timestamp that has not been updated from {}", url); + return; + } + if (feedTimestamp < lastTimestamp) { + LOG.info("Ignoring feed with older than previous timestamp from {}", url); + return; + } - // Handle update in graph writer runnable - saveResultOnGraph.execute(context -> - updateHandler.update(feed, context.gtfsRealtimeFuzzyTripMatcher()) - ); + // Handle update in graph writer runnable + saveResultOnGraph + .execute(context -> updateHandler.update(feed, context.gtfsRealtimeFuzzyTripMatcher())) + .get(); - lastTimestamp = feedTimestamp; - } catch (Exception e) { - LOG.error("Error reading gtfs-realtime feed from " + url, e); - } + lastTimestamp = feedTimestamp; } } diff --git a/application/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java b/application/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java index c725c8b1088..42f24031839 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java @@ -2,6 +2,7 @@ import com.google.transit.realtime.GtfsRealtime.TripUpdate; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.function.Consumer; import org.opentripplanner.updater.spi.PollingGraphUpdater; import org.opentripplanner.updater.spi.UpdateResult; @@ -73,7 +74,7 @@ public void setup(WriteToGraphCallback writeToGraphCallback) { * applies those updates to the graph. */ @Override - public void runPolling() { + public void runPolling() throws InterruptedException, ExecutionException { // Get update lists from update source List updates = updateSource.getUpdates(); var incrementality = updateSource.incrementalityOfLastUpdates(); @@ -89,7 +90,7 @@ public void runPolling() { feedId, recordMetrics ); - saveResultOnGraph.execute(runnable); + saveResultOnGraph.execute(runnable).get(); } } diff --git a/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java b/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java index b23f46522c3..bc5217f7534 100644 --- a/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; import java.util.function.Function; import java.util.stream.Collectors; import org.opentripplanner.routing.vehicle_parking.VehicleParking; @@ -49,12 +50,12 @@ public void setup(WriteToGraphCallback writeToGraphCallback) { } @Override - protected void runPolling() { + protected void runPolling() throws InterruptedException, ExecutionException { if (source.update()) { var updates = source.getUpdates(); var graphWriterRunnable = new AvailabilityUpdater(updates); - saveResultOnGraph.execute(graphWriterRunnable); + saveResultOnGraph.execute(graphWriterRunnable).get(); } } diff --git a/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java b/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java index 830429151b4..60a7b306052 100644 --- a/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.function.Function; import java.util.stream.Collectors; import org.opentripplanner.routing.graph.Graph; @@ -69,7 +70,7 @@ public void setup(WriteToGraphCallback writeToGraphCallback) { } @Override - protected void runPolling() { + protected void runPolling() throws InterruptedException, ExecutionException { LOG.debug("Updating vehicle parkings from {}", source); if (!source.update()) { LOG.debug("No updates"); @@ -81,7 +82,7 @@ protected void runPolling() { VehicleParkingGraphWriterRunnable graphWriterRunnable = new VehicleParkingGraphWriterRunnable( vehicleParkings ); - saveResultOnGraph.execute(graphWriterRunnable); + saveResultOnGraph.execute(graphWriterRunnable).get(); } private class VehicleParkingGraphWriterRunnable implements GraphWriterRunnable { diff --git a/application/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java b/application/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java index 4c487ac997b..a4d36f1d1c3 100644 --- a/application/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java @@ -3,6 +3,7 @@ import com.google.transit.realtime.GtfsRealtime.VehiclePosition; import java.util.List; import java.util.Set; +import java.util.concurrent.ExecutionException; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; import org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig; @@ -64,7 +65,7 @@ public void setup(WriteToGraphCallback writeToGraphCallback) { * applies those updates to the graph. */ @Override - public void runPolling() { + public void runPolling() throws InterruptedException, ExecutionException { // Get update lists from update source List updates = vehiclePositionSource.getPositions(); @@ -77,7 +78,7 @@ public void runPolling() { fuzzyTripMatching, updates ); - saveResultOnGraph.execute(runnable); + saveResultOnGraph.execute(runnable).get(); } } diff --git a/application/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java b/application/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java index 24686edce6c..c8030e5492a 100644 --- a/application/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java @@ -8,6 +8,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import java.util.stream.Stream; import org.opentripplanner.routing.linking.DisposableEdgeCollection; @@ -124,7 +125,7 @@ public String getConfigRef() { } @Override - protected void runPolling() { + protected void runPolling() throws InterruptedException, ExecutionException { LOG.debug("Updating vehicle rental stations from {}", nameForLogging); if (!source.update()) { LOG.debug("No updates from {}", nameForLogging); @@ -138,7 +139,7 @@ protected void runPolling() { stations, geofencingZones ); - saveResultOnGraph.execute(graphWriterRunnable); + saveResultOnGraph.execute(graphWriterRunnable).get(); } private class VehicleRentalGraphWriterRunnable implements GraphWriterRunnable { From 364c40112f9241b76732af9077f3e7042a48fc6f Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Wed, 27 Nov 2024 12:27:49 +0000 Subject: [PATCH 034/106] fix test after merge --- .../resources/org/opentripplanner/apis/vectortiles/style.json | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json b/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json index e3c15aee0e4..98765ed757f 100644 --- a/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json +++ b/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json @@ -419,6 +419,7 @@ "class", "StreetEdge", "AreaEdge", + "LinearPlatformEdge", "EscalatorEdge", "PathwayEdge", "ElevatorHopEdge", From 0944921250cfc2a36d422a24c9af341865162ad0 Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Thu, 12 Dec 2024 10:41:40 +0000 Subject: [PATCH 035/106] extract future handling logic to a new method --- .../updater/alert/GtfsRealtimeAlertsUpdater.java | 8 +++++--- .../opentripplanner/updater/spi/PollingGraphUpdater.java | 7 +++++++ .../opentripplanner/updater/trip/PollingTripUpdater.java | 2 +- .../VehicleParkingAvailabilityUpdater.java | 2 +- .../updater/vehicle_parking/VehicleParkingUpdater.java | 2 +- .../vehicle_position/PollingVehiclePositionUpdater.java | 2 +- .../updater/vehicle_rental/VehicleRentalUpdater.java | 2 +- 7 files changed, 17 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java b/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java index 0e7ab35cb13..bd20d8ddcc3 100644 --- a/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java @@ -82,9 +82,11 @@ protected void runPolling() throws InterruptedException, ExecutionException { } // Handle update in graph writer runnable - saveResultOnGraph - .execute(context -> updateHandler.update(feed, context.gtfsRealtimeFuzzyTripMatcher())) - .get(); + processGraphUpdaterResult( + saveResultOnGraph.execute(context -> + updateHandler.update(feed, context.gtfsRealtimeFuzzyTripMatcher()) + ) + ); lastTimestamp = feedTimestamp; } diff --git a/application/src/main/java/org/opentripplanner/updater/spi/PollingGraphUpdater.java b/application/src/main/java/org/opentripplanner/updater/spi/PollingGraphUpdater.java index e0859371de8..21eb60c2737 100644 --- a/application/src/main/java/org/opentripplanner/updater/spi/PollingGraphUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/spi/PollingGraphUpdater.java @@ -2,6 +2,8 @@ import java.time.Duration; import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -100,4 +102,9 @@ public String getConfigRef() { * with pauses in between. The length of the pause is defined in the preference frequency. */ protected abstract void runPolling() throws Exception; + + protected void processGraphUpdaterResult(Future result) + throws ExecutionException, InterruptedException { + result.get(); + } } diff --git a/application/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java b/application/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java index 42f24031839..1f88044d51a 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java @@ -90,7 +90,7 @@ public void runPolling() throws InterruptedException, ExecutionException { feedId, recordMetrics ); - saveResultOnGraph.execute(runnable).get(); + processGraphUpdaterResult(saveResultOnGraph.execute(runnable)); } } diff --git a/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java b/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java index 64f8ca81763..c65ff7bc4c1 100644 --- a/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java @@ -55,7 +55,7 @@ protected void runPolling() throws InterruptedException, ExecutionException { var updates = source.getUpdates(); var graphWriterRunnable = new AvailabilityUpdater(updates); - saveResultOnGraph.execute(graphWriterRunnable).get(); + processGraphUpdaterResult(saveResultOnGraph.execute(graphWriterRunnable)); } } diff --git a/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java b/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java index 4116486ee2d..ea356200f3b 100644 --- a/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java @@ -82,7 +82,7 @@ protected void runPolling() throws InterruptedException, ExecutionException { VehicleParkingGraphWriterRunnable graphWriterRunnable = new VehicleParkingGraphWriterRunnable( vehicleParkings ); - saveResultOnGraph.execute(graphWriterRunnable).get(); + processGraphUpdaterResult(saveResultOnGraph.execute(graphWriterRunnable)); } private class VehicleParkingGraphWriterRunnable implements GraphWriterRunnable { diff --git a/application/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java b/application/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java index a4d36f1d1c3..c787989c5d2 100644 --- a/application/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java @@ -78,7 +78,7 @@ public void runPolling() throws InterruptedException, ExecutionException { fuzzyTripMatching, updates ); - saveResultOnGraph.execute(runnable).get(); + processGraphUpdaterResult(saveResultOnGraph.execute(runnable)); } } diff --git a/application/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java b/application/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java index c8030e5492a..efcf9812feb 100644 --- a/application/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java @@ -139,7 +139,7 @@ protected void runPolling() throws InterruptedException, ExecutionException { stations, geofencingZones ); - saveResultOnGraph.execute(graphWriterRunnable).get(); + processGraphUpdaterResult(saveResultOnGraph.execute(graphWriterRunnable)); } private class VehicleRentalGraphWriterRunnable implements GraphWriterRunnable { From 4df65a19cc68d495e5aedfecea0b134b798c39eb Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Thu, 12 Dec 2024 10:55:00 +0000 Subject: [PATCH 036/106] add feature flag --- .../opentripplanner/framework/application/OTPFeature.java | 6 ++++++ doc/user/Configuration.md | 1 + 2 files changed, 7 insertions(+) diff --git a/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java b/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java index 6631287613b..add5d690e2e 100644 --- a/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java +++ b/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java @@ -97,6 +97,12 @@ public enum OTPFeature { false, "Whether the @async annotation in the GraphQL schema should lead to the fetch being executed asynchronously. This allows batch or alias queries to run in parallel at the cost of consuming extra threads." ), + WaitForGraphUpdateInPollingUpdaters( + true, + false, + "Make all polling updaters wait for graph updates to complete before finishing. " + + "If this is not enabled, the updaters will finish after submitting the task to update the graph." + ), Co2Emissions(false, true, "Enable the emissions sandbox module."), DataOverlay( false, diff --git a/doc/user/Configuration.md b/doc/user/Configuration.md index f80966b8cb3..1d65c2af939 100644 --- a/doc/user/Configuration.md +++ b/doc/user/Configuration.md @@ -238,6 +238,7 @@ Here is a list of all features which can be toggled on/off and their default val | `TransmodelGraphQlApi` | Enable the [Transmodel (NeTEx) GraphQL API](apis/TransmodelApi.md). | ✓️ | ✓️ | | `ActuatorAPI` | Endpoint for actuators (service health status). | | ✓️ | | `AsyncGraphQLFetchers` | Whether the @async annotation in the GraphQL schema should lead to the fetch being executed asynchronously. This allows batch or alias queries to run in parallel at the cost of consuming extra threads. | | | +| `WaitForGraphUpdateInPollingUpdaters` | Make all polling updaters wait for graph updates to complete before finishing. If this is not enabled, the updaters will finish after submitting the task to update the graph. | ✓️ | | | `Co2Emissions` | Enable the emissions sandbox module. | | ✓️ | | `DataOverlay` | Enable usage of data overlay when calculating costs for the street network. | | ✓️ | | `FaresV2` | Enable import of GTFS-Fares v2 data. | | ✓️ | From d8a34ddcfe80dabfba1c08a47f95adaa900d643d Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Thu, 12 Dec 2024 12:50:06 +0000 Subject: [PATCH 037/106] refactor update graph logic to PollingGraphUpdater --- .../updater/alert/GtfsRealtimeAlertsUpdater.java | 12 +----------- .../updater/siri/updater/SiriETUpdater.java | 9 --------- .../updater/siri/updater/SiriSXUpdater.java | 9 +-------- .../updater/spi/PollingGraphUpdater.java | 15 ++++++++++++--- .../updater/trip/PollingTripUpdater.java | 12 +----------- .../VehicleParkingAvailabilityUpdater.java | 10 +--------- .../vehicle_parking/VehicleParkingUpdater.java | 9 +-------- .../PollingVehiclePositionUpdater.java | 12 +----------- .../vehicle_rental/VehicleRentalUpdater.java | 10 +--------- 9 files changed, 19 insertions(+), 79 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java b/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java index bd20d8ddcc3..b217c60a05b 100644 --- a/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java @@ -27,7 +27,6 @@ public class GtfsRealtimeAlertsUpdater extends PollingGraphUpdater implements Tr private final TransitAlertService transitAlertService; private final HttpHeaders headers; private final OtpHttpClient otpHttpClient; - private WriteToGraphCallback saveResultOnGraph; private Long lastTimestamp = Long.MIN_VALUE; public GtfsRealtimeAlertsUpdater( @@ -49,11 +48,6 @@ public GtfsRealtimeAlertsUpdater( LOG.info("Creating real-time alert updater running every {}: {}", pollingPeriod(), url); } - @Override - public void setup(WriteToGraphCallback writeToGraphCallback) { - this.saveResultOnGraph = writeToGraphCallback; - } - public TransitAlertService getTransitAlertService() { return transitAlertService; } @@ -82,11 +76,7 @@ protected void runPolling() throws InterruptedException, ExecutionException { } // Handle update in graph writer runnable - processGraphUpdaterResult( - saveResultOnGraph.execute(context -> - updateHandler.update(feed, context.gtfsRealtimeFuzzyTripMatcher()) - ) - ); + updateGraph(context -> updateHandler.update(feed, context.gtfsRealtimeFuzzyTripMatcher())); lastTimestamp = feedTimestamp; } diff --git a/application/src/main/java/org/opentripplanner/updater/siri/updater/SiriETUpdater.java b/application/src/main/java/org/opentripplanner/updater/siri/updater/SiriETUpdater.java index 087bf28e875..38efa18c177 100644 --- a/application/src/main/java/org/opentripplanner/updater/siri/updater/SiriETUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/siri/updater/SiriETUpdater.java @@ -28,10 +28,6 @@ public class SiriETUpdater extends PollingGraphUpdater { * Feed id that is used for the trip ids in the TripUpdates */ private final String feedId; - /** - * Parent update manager. Is used to execute graph writer runnables. - */ - protected WriteToGraphCallback saveResultOnGraph; private final EstimatedTimetableHandler estimatedTimetableHandler; @@ -61,11 +57,6 @@ public SiriETUpdater( recordMetrics = TripUpdateMetrics.streaming(config); } - @Override - public void setup(WriteToGraphCallback writeToGraphCallback) { - this.saveResultOnGraph = writeToGraphCallback; - } - /** * Repeatedly makes blocking calls to an UpdateStreamer to retrieve new stop time updates, and * applies those updates to the graph. diff --git a/application/src/main/java/org/opentripplanner/updater/siri/updater/SiriSXUpdater.java b/application/src/main/java/org/opentripplanner/updater/siri/updater/SiriSXUpdater.java index 83200db30d3..8311a385e9c 100644 --- a/application/src/main/java/org/opentripplanner/updater/siri/updater/SiriSXUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/siri/updater/SiriSXUpdater.java @@ -13,7 +13,6 @@ import org.opentripplanner.updater.alert.TransitAlertProvider; import org.opentripplanner.updater.siri.SiriAlertsUpdateHandler; import org.opentripplanner.updater.spi.PollingGraphUpdater; -import org.opentripplanner.updater.spi.WriteToGraphCallback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.org.siri.siri20.ServiceDelivery; @@ -33,7 +32,6 @@ public class SiriSXUpdater extends PollingGraphUpdater implements TransitAlertPr // TODO RT_AB: Document why SiriAlertsUpdateHandler is a separate instance that persists across // many graph update operations. private final SiriAlertsUpdateHandler updateHandler; - private WriteToGraphCallback writeToGraphCallback; private ZonedDateTime lastTimestamp = ZonedDateTime.now().minusWeeks(1); private String requestorRef; /** @@ -78,11 +76,6 @@ public SiriSXUpdater(SiriSXUpdaterParameters config, TimetableRepository timetab ); } - @Override - public void setup(WriteToGraphCallback writeToGraphCallback) { - this.writeToGraphCallback = writeToGraphCallback; - } - public TransitAlertService getTransitAlertService() { return transitAlertService; } @@ -141,7 +134,7 @@ private void updateSiri() { // All that said, out of all the update types, Alerts (and SIRI SX) are probably the ones // that would be most tolerant of non-versioned application-wide storage since they don't // participate in routing and are tacked on to already-completed routing responses. - writeToGraphCallback.execute(context -> { + saveResultOnGraph.execute(context -> { updateHandler.update(serviceDelivery, context); if (markPrimed) { primed = true; diff --git a/application/src/main/java/org/opentripplanner/updater/spi/PollingGraphUpdater.java b/application/src/main/java/org/opentripplanner/updater/spi/PollingGraphUpdater.java index 21eb60c2737..0525406505e 100644 --- a/application/src/main/java/org/opentripplanner/updater/spi/PollingGraphUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/spi/PollingGraphUpdater.java @@ -3,7 +3,7 @@ import java.time.Duration; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; +import org.opentripplanner.updater.GraphWriterRunnable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,6 +36,10 @@ public abstract class PollingGraphUpdater implements GraphUpdater { * removed that. */ protected volatile boolean primed; + /** + * Parent update manager. Is used to execute graph writer runnables. + */ + protected WriteToGraphCallback saveResultOnGraph; /** Shared configuration code for all polling graph updaters. */ protected PollingGraphUpdater(PollingGraphUpdaterParameters config) { @@ -97,14 +101,19 @@ public String getConfigRef() { return configRef; } + @Override + public final void setup(WriteToGraphCallback writeToGraphCallback) { + this.saveResultOnGraph = writeToGraphCallback; + } + /** * Mirrors GraphUpdater.run method. Only difference is that runPolling will be run multiple times * with pauses in between. The length of the pause is defined in the preference frequency. */ protected abstract void runPolling() throws Exception; - protected void processGraphUpdaterResult(Future result) + protected final void updateGraph(GraphWriterRunnable task) throws ExecutionException, InterruptedException { - result.get(); + saveResultOnGraph.execute(task).get(); } } diff --git a/application/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java b/application/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java index 1f88044d51a..8331bf19d3b 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java @@ -6,7 +6,6 @@ import java.util.function.Consumer; import org.opentripplanner.updater.spi.PollingGraphUpdater; import org.opentripplanner.updater.spi.UpdateResult; -import org.opentripplanner.updater.spi.WriteToGraphCallback; import org.opentripplanner.updater.trip.metrics.BatchTripUpdateMetrics; import org.opentripplanner.utils.tostring.ToStringBuilder; import org.slf4j.Logger; @@ -34,10 +33,6 @@ public class PollingTripUpdater extends PollingGraphUpdater { private final BackwardsDelayPropagationType backwardsDelayPropagationType; private final Consumer recordMetrics; - /** - * Parent update manager. Is used to execute graph writer runnables. - */ - private WriteToGraphCallback saveResultOnGraph; /** * Set only if we should attempt to match the trip_id from other data in TripDescriptor */ @@ -64,11 +59,6 @@ public PollingTripUpdater( ); } - @Override - public void setup(WriteToGraphCallback writeToGraphCallback) { - this.saveResultOnGraph = writeToGraphCallback; - } - /** * Repeatedly makes blocking calls to an UpdateStreamer to retrieve new stop time updates, and * applies those updates to the graph. @@ -90,7 +80,7 @@ public void runPolling() throws InterruptedException, ExecutionException { feedId, recordMetrics ); - processGraphUpdaterResult(saveResultOnGraph.execute(runnable)); + updateGraph(runnable); } } diff --git a/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java b/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java index c65ff7bc4c1..29e820ff952 100644 --- a/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java @@ -13,7 +13,6 @@ import org.opentripplanner.updater.RealTimeUpdateContext; import org.opentripplanner.updater.spi.DataSource; import org.opentripplanner.updater.spi.PollingGraphUpdater; -import org.opentripplanner.updater.spi.WriteToGraphCallback; import org.opentripplanner.utils.tostring.ToStringBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,8 +27,6 @@ public class VehicleParkingAvailabilityUpdater extends PollingGraphUpdater { VehicleParkingAvailabilityUpdater.class ); private final DataSource source; - private WriteToGraphCallback saveResultOnGraph; - private final VehicleParkingRepository repository; public VehicleParkingAvailabilityUpdater( @@ -44,18 +41,13 @@ public VehicleParkingAvailabilityUpdater( LOG.info("Creating vehicle-parking updater running every {}: {}", pollingPeriod(), source); } - @Override - public void setup(WriteToGraphCallback writeToGraphCallback) { - this.saveResultOnGraph = writeToGraphCallback; - } - @Override protected void runPolling() throws InterruptedException, ExecutionException { if (source.update()) { var updates = source.getUpdates(); var graphWriterRunnable = new AvailabilityUpdater(updates); - processGraphUpdaterResult(saveResultOnGraph.execute(graphWriterRunnable)); + updateGraph(graphWriterRunnable); } } diff --git a/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java b/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java index ea356200f3b..5513e224f57 100644 --- a/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java @@ -27,7 +27,6 @@ import org.opentripplanner.updater.RealTimeUpdateContext; import org.opentripplanner.updater.spi.DataSource; import org.opentripplanner.updater.spi.PollingGraphUpdater; -import org.opentripplanner.updater.spi.WriteToGraphCallback; import org.opentripplanner.utils.tostring.ToStringBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,7 +42,6 @@ public class VehicleParkingUpdater extends PollingGraphUpdater { private final Map> tempEdgesByPark = new HashMap<>(); private final DataSource source; private final List oldVehicleParkings = new ArrayList<>(); - private WriteToGraphCallback saveResultOnGraph; private final VertexLinker linker; private final VehicleParkingRepository parkingRepository; @@ -64,11 +62,6 @@ public VehicleParkingUpdater( LOG.info("Creating vehicle-parking updater running every {}: {}", pollingPeriod(), source); } - @Override - public void setup(WriteToGraphCallback writeToGraphCallback) { - this.saveResultOnGraph = writeToGraphCallback; - } - @Override protected void runPolling() throws InterruptedException, ExecutionException { LOG.debug("Updating vehicle parkings from {}", source); @@ -82,7 +75,7 @@ protected void runPolling() throws InterruptedException, ExecutionException { VehicleParkingGraphWriterRunnable graphWriterRunnable = new VehicleParkingGraphWriterRunnable( vehicleParkings ); - processGraphUpdaterResult(saveResultOnGraph.execute(graphWriterRunnable)); + updateGraph(graphWriterRunnable); } private class VehicleParkingGraphWriterRunnable implements GraphWriterRunnable { diff --git a/application/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java b/application/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java index c787989c5d2..f53d3010e59 100644 --- a/application/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java @@ -8,7 +8,6 @@ import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; import org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig; import org.opentripplanner.updater.spi.PollingGraphUpdater; -import org.opentripplanner.updater.spi.WriteToGraphCallback; import org.opentripplanner.utils.tostring.ToStringBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,10 +27,6 @@ public class PollingVehiclePositionUpdater extends PollingGraphUpdater { private final GtfsRealtimeHttpVehiclePositionSource vehiclePositionSource; private final Set vehiclePositionFeatures; - /** - * Parent update manager. Is used to execute graph writer runnables. - */ - private WriteToGraphCallback saveResultOnGraph; private final String feedId; private final RealtimeVehicleRepository realtimeVehicleRepository; private final boolean fuzzyTripMatching; @@ -55,11 +50,6 @@ public PollingVehiclePositionUpdater( ); } - @Override - public void setup(WriteToGraphCallback writeToGraphCallback) { - this.saveResultOnGraph = writeToGraphCallback; - } - /** * Repeatedly makes blocking calls to an UpdateStreamer to retrieve new stop time updates, and * applies those updates to the graph. @@ -78,7 +68,7 @@ public void runPolling() throws InterruptedException, ExecutionException { fuzzyTripMatching, updates ); - processGraphUpdaterResult(saveResultOnGraph.execute(runnable)); + updateGraph(runnable); } } diff --git a/application/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java b/application/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java index efcf9812feb..8419d004138 100644 --- a/application/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java @@ -31,7 +31,6 @@ import org.opentripplanner.updater.RealTimeUpdateContext; import org.opentripplanner.updater.spi.PollingGraphUpdater; import org.opentripplanner.updater.spi.UpdaterConstructionException; -import org.opentripplanner.updater.spi.WriteToGraphCallback; import org.opentripplanner.updater.vehicle_rental.datasources.VehicleRentalDatasource; import org.opentripplanner.utils.lang.ObjectUtils; import org.opentripplanner.utils.logging.Throttle; @@ -54,8 +53,6 @@ public class VehicleRentalUpdater extends PollingGraphUpdater { private final VehicleRentalDatasource source; private final String nameForLogging; - private WriteToGraphCallback saveResultOnGraph; - private Map latestModifiedEdges = Map.of(); private Set latestAppliedGeofencingZones = Set.of(); private final Map verticesByStation = new HashMap<>(); @@ -109,11 +106,6 @@ public VehicleRentalUpdater( } } - @Override - public void setup(WriteToGraphCallback writeToGraphCallback) { - this.saveResultOnGraph = writeToGraphCallback; - } - @Override public String toString() { return ToStringBuilder.of(VehicleRentalUpdater.class).addObj("source", source).toString(); @@ -139,7 +131,7 @@ protected void runPolling() throws InterruptedException, ExecutionException { stations, geofencingZones ); - processGraphUpdaterResult(saveResultOnGraph.execute(graphWriterRunnable)); + updateGraph(graphWriterRunnable); } private class VehicleRentalGraphWriterRunnable implements GraphWriterRunnable { From 69ac4854678bb964715d4b7d143e457cc6b77dda Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Thu, 12 Dec 2024 12:50:17 +0000 Subject: [PATCH 038/106] add test for desired feature flag --- .../updater/spi/PollingGraphUpdaterTest.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 application/src/test/java/org/opentripplanner/updater/spi/PollingGraphUpdaterTest.java diff --git a/application/src/test/java/org/opentripplanner/updater/spi/PollingGraphUpdaterTest.java b/application/src/test/java/org/opentripplanner/updater/spi/PollingGraphUpdaterTest.java new file mode 100644 index 00000000000..6132988d92b --- /dev/null +++ b/application/src/test/java/org/opentripplanner/updater/spi/PollingGraphUpdaterTest.java @@ -0,0 +1,78 @@ +package org.opentripplanner.updater.spi; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.updater.GraphWriterRunnable; + +public class PollingGraphUpdaterTest { + + private static final PollingGraphUpdaterParameters config = new PollingGraphUpdaterParameters() { + @Override + public Duration frequency() { + return Duration.ZERO; + } + + @Override + public String configRef() { + return ""; + } + }; + + private static final PollingGraphUpdater subject = new PollingGraphUpdater(config) { + @Override + protected void runPolling() {} + }; + + private boolean updateCompleted; + + @BeforeAll + static void beforeAll() { + subject.setup(runnable -> CompletableFuture.runAsync(() -> runnable.run(null))); + } + + @BeforeEach + void setUp() { + updateCompleted = false; + } + + private final GraphWriterRunnable graphWriterRunnable = context -> { + try { + Thread.sleep(100); + updateCompleted = true; + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }; + + @Test + void testUpdateGraphWithWaitFeatureOn() { + OTPFeature.WaitForGraphUpdateInPollingUpdaters.testOn(() -> { + callUpdater(); + assertTrue(updateCompleted); + }); + } + + @Test + void testProcessGraphUpdaterResultWithWaitFeatureOff() { + OTPFeature.WaitForGraphUpdateInPollingUpdaters.testOff(() -> { + callUpdater(); + assertFalse(updateCompleted); + }); + } + + private void callUpdater() { + try { + subject.updateGraph(graphWriterRunnable); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + } +} From 0037b5aab805b8eb719593387881fd242556744c Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Thu, 12 Dec 2024 12:55:27 +0000 Subject: [PATCH 039/106] implement feature flag --- .../opentripplanner/updater/spi/PollingGraphUpdater.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/opentripplanner/updater/spi/PollingGraphUpdater.java b/application/src/main/java/org/opentripplanner/updater/spi/PollingGraphUpdater.java index 0525406505e..e8faa4b6aba 100644 --- a/application/src/main/java/org/opentripplanner/updater/spi/PollingGraphUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/spi/PollingGraphUpdater.java @@ -3,6 +3,7 @@ import java.time.Duration; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; +import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.updater.GraphWriterRunnable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -114,6 +115,9 @@ public final void setup(WriteToGraphCallback writeToGraphCallback) { protected final void updateGraph(GraphWriterRunnable task) throws ExecutionException, InterruptedException { - saveResultOnGraph.execute(task).get(); + var result = saveResultOnGraph.execute(task); + if (OTPFeature.WaitForGraphUpdateInPollingUpdaters.isOn()) { + result.get(); + } } } From 3825a024000a7bc576ab2f5a19b44bb1dd333822 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 17 Dec 2024 01:32:02 +0100 Subject: [PATCH 040/106] feature: Create a repository to store OSM info for other graph build modules The Repository and Service is not available during routing, only during the graph build. --- .../graph_builder/GraphBuilder.java | 8 +++- .../module/OsmBoardingLocationsModule.java | 9 +++- .../module/configure/GraphBuilderFactory.java | 7 ++- .../module/configure/GraphBuilderModules.java | 8 ++-- .../graph_builder/module/osm/OsmModule.java | 23 +++++++--- .../module/osm/OsmModuleBuilder.java | 5 +++ .../routing/graph/SerializedGraphObject.java | 7 +++ .../osminfo/OsmInfoGraphBuildRepository.java | 23 ++++++++++ .../osminfo/OsmInfoGraphBuildService.java | 22 +++++++++ .../OsmInfoGraphBuildRepositoryModule.java | 12 +++++ .../OsmInfoGraphBuildServiceModule.java | 12 +++++ .../DefaultOsmInfoGraphBuildRepository.java | 37 +++++++++++++++ .../DefaultOsmInfoGraphBuildService.java | 28 ++++++++++++ .../osminfo/model/OsmWayReferences.java | 26 +++++++++++ .../opentripplanner/standalone/OTPMain.java | 1 + .../configure/ConstructApplication.java | 15 ++++++- .../ConstructApplicationFactory.java | 18 ++++---- .../standalone/configure/LoadApplication.java | 7 ++- .../configure/LoadApplicationFactory.java | 6 +++ .../opentripplanner/ConstantsForTests.java | 22 +++++---- .../OsmBoardingLocationsModuleTest.java | 9 +++- .../islandpruning/IslandPruningUtils.java | 9 ++-- .../module/linking/LinkingTest.java | 13 ++++-- .../module/osm/OsmModuleTest.java | 45 +++++++++++++------ .../module/osm/PlatformLinkerTest.java | 12 +++-- .../module/osm/TriangleInequalityTest.java | 8 +++- .../module/osm/UnconnectedAreasTest.java | 8 +++- .../module/osm/UnroutableTest.java | 8 +++- .../routing/graph/GraphSerializationTest.java | 8 ++++ 29 files changed, 355 insertions(+), 61 deletions(-) create mode 100644 application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildRepository.java create mode 100644 application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildService.java create mode 100644 application/src/main/java/org/opentripplanner/service/osminfo/configure/OsmInfoGraphBuildRepositoryModule.java create mode 100644 application/src/main/java/org/opentripplanner/service/osminfo/configure/OsmInfoGraphBuildServiceModule.java create mode 100644 application/src/main/java/org/opentripplanner/service/osminfo/internal/DefaultOsmInfoGraphBuildRepository.java create mode 100644 application/src/main/java/org/opentripplanner/service/osminfo/internal/DefaultOsmInfoGraphBuildService.java create mode 100644 application/src/main/java/org/opentripplanner/service/osminfo/model/OsmWayReferences.java 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..8344b3a3273 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 @@ -13,6 +13,7 @@ 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.street.model.StreetTraversalPermission; import org.opentripplanner.street.model.edge.AreaEdge; import org.opentripplanner.street.model.edge.BoardingLocationToStopLink; @@ -55,14 +56,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); } 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..46c74b52a2d 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 ) { @@ -78,7 +80,7 @@ static OsmModule provideOsmModule( } return OsmModule - .of(providers, graph, parkingService) + .of(providers, graph, osmInfoGraphBuildRepository, vehicleParkingRepository) .withEdgeNamer(config.edgeNamer) .withAreaVisibility(config.areaVisibility) .withPlatformEntriesLinking(config.platformEntriesLinking) 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..00626d1e221 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 @@ -25,6 +25,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.OsmWayReferences; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.vehicleparking.model.VehicleParking; import org.opentripplanner.street.model.StreetLimitationParameters; @@ -52,7 +54,10 @@ public class OsmModule implements GraphBuilderModule { */ private final List providers; private final Graph graph; + // TODO: Use this to store edge stop references + private final OsmInfoGraphBuildRepository osmInfoGraphBuildRepository; private final VehicleParkingRepository parkingRepository; + private final DataImportIssueStore issueStore; private final OsmProcessingParameters params; private final SafetyValueNormalizer normalizer; @@ -63,36 +68,40 @@ 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.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 @@ -408,6 +417,8 @@ private void buildBasicGraph() { StreetEdge backStreet = streets.back(); normalizer.applyWayProperties(street, backStreet, wayData, way); + osmInfoGraphBuildRepository.addReferences(street, new OsmWayReferences(List.of(street.toString()))); + applyEdgesToTurnRestrictions(way, startNode, endNode, street, backStreet); startNode = endNode; osmStartNode = osmdb.getNode(startNode); 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..f7038a40c74 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(); @@ -32,10 +34,12 @@ public class OsmModuleBuilder { OsmModuleBuilder( Collection providers, Graph graph, + OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, VehicleParkingRepository parkingRepository ) { this.providers = providers; this.graph = graph; + this.osmInfoGraphBuildRepository = osmInfoGraphBuildRepository; this.parkingRepository = parkingRepository; } @@ -88,6 +92,7 @@ public OsmModule build() { return new OsmModule( providers, graph, + osmInfoGraphBuildRepository, parkingRepository, issueStore, streetLimitationParameters, 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/service/osminfo/OsmInfoGraphBuildRepository.java b/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildRepository.java new file mode 100644 index 00000000000..c8a2b908893 --- /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.OsmWayReferences; +import org.opentripplanner.street.model.edge.Edge; + +/** + * Store OSM data used during graph build, but after the OSM Graph Builder is done. + *

+ * This is a repository to support the {@link OsmInfoGraphBuildService}. + */ +public interface OsmInfoGraphBuildRepository extends Serializable { + /** + * TODO Add doc + */ + void addReferences(Edge edge, OsmWayReferences info); + + /** + * TODO Add doc + */ + Optional findReferences(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..784867ada30 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildService.java @@ -0,0 +1,22 @@ +package org.opentripplanner.service.osminfo; + +import java.util.Optional; +import org.opentripplanner.service.osminfo.model.OsmWayReferences; +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 { + /** + * TODO Add doc + */ + Optional findReferences(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..4ba575dc8b0 --- /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.OsmWayReferences; +import org.opentripplanner.street.model.edge.Edge; + +public class DefaultOsmInfoGraphBuildRepository + implements OsmInfoGraphBuildRepository, Serializable { + + private final Map references = new HashMap<>(); + + @Inject + public DefaultOsmInfoGraphBuildRepository() {} + + @Override + public void addReferences(Edge edge, OsmWayReferences info) { + Objects.requireNonNull(edge); + Objects.requireNonNull(info); + this.references.put(edge, info); + } + + @Override + public Optional findReferences(Edge edge) { + return Optional.ofNullable(references.get(edge)); + } + + @Override + public String toString() { + return "DefaultOsmInfoGraphBuildRepository{references size = " + references.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..29271e17679 --- /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.OsmWayReferences; +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 findReferences(Edge edge) { + return repository.findReferences(edge); + } + + @Override + public String toString() { + return "DefaultOsmInfoGraphBuildService{ repository=" + repository + '}'; + } +} diff --git a/application/src/main/java/org/opentripplanner/service/osminfo/model/OsmWayReferences.java b/application/src/main/java/org/opentripplanner/service/osminfo/model/OsmWayReferences.java new file mode 100644 index 00000000000..6208758f153 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/osminfo/model/OsmWayReferences.java @@ -0,0 +1,26 @@ +package org.opentripplanner.service.osminfo.model; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; + +/** + * TODO : Add java doc + */ +public class OsmWayReferences implements Serializable { + + private final List references; + + public OsmWayReferences(Collection references) { + this.references = references.stream().sorted().distinct().toList(); + } + + /** + * TODO : Add java doc + * + * returns a sorted distinct list of references + */ + public List references() { + return references; + } +} \ No newline at end of file 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/configure/ConstructApplication.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java index b4edbb36299..ebb83a045d0 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(), @@ -261,6 +270,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/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/graph_builder/module/OsmBoardingLocationsModuleTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/OsmBoardingLocationsModuleTest.java index c55e482e533..18c40bac898 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 @@ -14,6 +14,8 @@ 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; @@ -83,8 +85,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 +111,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 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/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, From fd0b1fe8ae564e70852fb75d487e67f5303a4c56 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Tue, 17 Dec 2024 22:47:28 +0100 Subject: [PATCH 041/106] Update protobuf, OSM parser and Google cloud tools --- application/pom.xml | 6 +++--- .../src/main/java/org/opentripplanner/osm/OsmParser.java | 4 ++-- .../src/main/java/org/opentripplanner/osm/OsmProvider.java | 2 +- .../updater/alert/GtfsRealtimeAlertsUpdater.java | 2 +- .../updater/trip/MqttGtfsRealtimeUpdater.java | 2 +- .../src/test/java/org/opentripplanner/GtfsTest.java | 2 +- gtfs-realtime-protobuf/pom.xml | 7 ++++++- pom.xml | 2 +- 8 files changed, 16 insertions(+), 11 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index c3b4a6ee582..2a6e3043ba2 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -312,9 +312,9 @@ - org.openstreetmap.osmosis - osmosis-osm-binary - 0.48.3 + org.openstreetmap.pbf + osmpbf + 1.6.0 diff --git a/application/src/main/java/org/opentripplanner/osm/OsmParser.java b/application/src/main/java/org/opentripplanner/osm/OsmParser.java index 8a5f8e32448..4b443e6a505 100644 --- a/application/src/main/java/org/opentripplanner/osm/OsmParser.java +++ b/application/src/main/java/org/opentripplanner/osm/OsmParser.java @@ -1,11 +1,11 @@ package org.opentripplanner.osm; +import crosby.binary.BinaryParser; +import crosby.binary.Osmformat; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import org.openstreetmap.osmosis.osmbinary.BinaryParser; -import org.openstreetmap.osmosis.osmbinary.Osmformat; import org.opentripplanner.graph_builder.module.osm.OsmDatabase; import org.opentripplanner.osm.model.OsmMemberType; import org.opentripplanner.osm.model.OsmNode; diff --git a/application/src/main/java/org/opentripplanner/osm/OsmProvider.java b/application/src/main/java/org/opentripplanner/osm/OsmProvider.java index 597fd516b0e..91944a95b86 100644 --- a/application/src/main/java/org/opentripplanner/osm/OsmProvider.java +++ b/application/src/main/java/org/opentripplanner/osm/OsmProvider.java @@ -1,11 +1,11 @@ package org.opentripplanner.osm; +import crosby.binary.file.BlockInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.time.ZoneId; -import org.openstreetmap.osmosis.osmbinary.file.BlockInputStream; import org.opentripplanner.datastore.api.DataSource; import org.opentripplanner.datastore.api.FileType; import org.opentripplanner.datastore.file.FileDataSource; diff --git a/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java b/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java index a5be5ef4185..de6383c6016 100644 --- a/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java @@ -68,7 +68,7 @@ protected void runPolling() { final FeedMessage feed = otpHttpClient.getAndMap( URI.create(url), this.headers.asMap(), - FeedMessage.PARSER::parseFrom + FeedMessage::parseFrom ); long feedTimestamp = feed.getHeader().getTimestamp(); diff --git a/application/src/main/java/org/opentripplanner/updater/trip/MqttGtfsRealtimeUpdater.java b/application/src/main/java/org/opentripplanner/updater/trip/MqttGtfsRealtimeUpdater.java index 20b49ed022f..0580cd4ea63 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/MqttGtfsRealtimeUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/MqttGtfsRealtimeUpdater.java @@ -138,7 +138,7 @@ public void messageArrived(String topic, MqttMessage message) { UpdateIncrementality updateIncrementality = FULL_DATASET; try { // Decode message - GtfsRealtime.FeedMessage feedMessage = GtfsRealtime.FeedMessage.PARSER.parseFrom( + GtfsRealtime.FeedMessage feedMessage = GtfsRealtime.FeedMessage.parseFrom( message.getPayload() ); List feedEntityList = feedMessage.getEntityList(); diff --git a/application/src/test/java/org/opentripplanner/GtfsTest.java b/application/src/test/java/org/opentripplanner/GtfsTest.java index 5d548e4012c..05b7bfbf4f6 100644 --- a/application/src/test/java/org/opentripplanner/GtfsTest.java +++ b/application/src/test/java/org/opentripplanner/GtfsTest.java @@ -221,7 +221,7 @@ protected void setUp() throws Exception { try { InputStream inputStream = new FileInputStream(gtfsRealTime); - FeedMessage feedMessage = FeedMessage.PARSER.parseFrom(inputStream); + FeedMessage feedMessage = FeedMessage.parseFrom(inputStream); List feedEntityList = feedMessage.getEntityList(); List updates = new ArrayList<>(feedEntityList.size()); for (FeedEntity feedEntity : feedEntityList) { diff --git a/gtfs-realtime-protobuf/pom.xml b/gtfs-realtime-protobuf/pom.xml index e4465a4d366..d3c3305b9b2 100644 --- a/gtfs-realtime-protobuf/pom.xml +++ b/gtfs-realtime-protobuf/pom.xml @@ -11,10 +11,15 @@ gtfs-realtime-protobuf OpenTripPlanner - GTFS Realtime (protobuf) + + 4.28.3 + + com.google.protobuf protobuf-java + ${protobuf.version} @@ -46,7 +51,7 @@ - com.google.protobuf:protoc:3.22.0:exe:${os.detected.classifier} + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} diff --git a/pom.xml b/pom.xml index 27eada4007b..4108973c7ff 100644 --- a/pom.xml +++ b/pom.xml @@ -392,7 +392,7 @@ com.google.cloud libraries-bom - 26.48.0 + 26.51.0 pom import From 04d35b7c34c5ab646ee044c7bd80caa50b99355b Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 18 Dec 2024 00:08:13 +0100 Subject: [PATCH 042/106] Clean up code a little --- .../apis/gtfs/GtfsGraphQLIndex.java | 2 +- .../mapping/StatesToWalkStepsMapper.java | 31 +++++++------------ .../model/vertex/StationEntranceVertex.java | 26 +++++++++++----- .../model/site/StationElementBuilder.java | 3 +- .../opentripplanner/apis/gtfs/schema.graphqls | 1 - .../apis/gtfs/expectations/walk-steps.json | 17 +++++----- 6 files changed, 42 insertions(+), 38 deletions(-) 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 cd72633a886..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,8 +39,8 @@ 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.EstimatedTimeImpl; 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; import org.opentripplanner.apis.gtfs.datafetchers.FeedImpl; 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 956ec6d5701..97310a47453 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 @@ -30,7 +30,6 @@ import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.street.search.state.State; import org.opentripplanner.transit.model.basic.Accessibility; -import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.site.Entrance; /** @@ -179,8 +178,8 @@ private void processState(State backState, State forwardState) { if (edge instanceof ElevatorAlightEdge) { addStep(createElevatorWalkStep(backState, forwardState, edge)); return; - } else if (backState.getVertex() instanceof StationEntranceVertex) { - addStep(createStationEntranceWalkStep(backState, forwardState, edge)); + } 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); @@ -525,29 +524,23 @@ private WalkStepBuilder createElevatorWalkStep(State backState, State forwardSta private WalkStepBuilder createStationEntranceWalkStep( State backState, State forwardState, - Edge edge + StationEntranceVertex vertex ) { - // don't care what came before or comes after - var step = 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. - step.withRelativeDirection(RelativeDirection.ENTER_OR_EXIT_STATION); - - StationEntranceVertex vertex = (StationEntranceVertex) backState.getVertex(); - - FeedScopedId entranceId = new FeedScopedId("osm", vertex.getId()); - Entrance entrance = Entrance - .of(entranceId) - .withCode(vertex.getCode()) + .of(vertex.id()) + .withCode(vertex.code()) .withCoordinate(new WgsCoordinate(vertex.getCoordinate())) .withWheelchairAccessibility( - vertex.isAccessible() ? Accessibility.POSSIBLE : Accessibility.NOT_POSSIBLE + vertex.wheelchairAccessibility() ) .build(); - step.withEntrance(entrance); - return step; + // don't care what came before or comes after + return createWalkStep(forwardState, backState) + // There is not a way to definitively determine if a user is entering or exiting the station, + // since the doors might be between or inside stations. + .withRelativeDirection(RelativeDirection.ENTER_OR_EXIT_STATION) + .withEntrance(entrance); } private void createAndSaveStep( diff --git a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java index e55ac7078db..6dd528204b2 100644 --- a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java @@ -1,5 +1,10 @@ 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; + public class StationEntranceVertex extends OsmVertex { private final String code; @@ -11,19 +16,26 @@ public StationEntranceVertex(double x, double y, long nodeId, String code, boole this.accessible = accessible; } - public String getCode() { - return code; + public FeedScopedId id() { + return new FeedScopedId("osm", String.valueOf(nodeId)); } - public boolean isAccessible() { - return accessible; + @Nullable + public String code() { + return code; } - public String getId() { - return Long.toString(nodeId); + public Accessibility wheelchairAccessibility() { + return accessible ? Accessibility.POSSIBLE : Accessibility.NOT_POSSIBLE; } + @Override public String toString() { - return "StationEntranceVertex(" + super.toString() + ", code=" + code + ")"; + return ToStringBuilder + .of(StationEntranceVertex.class) + .addNum("nodeId", nodeId) + .addStr("code", code) + .toString(); } + } 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 1c4951b782c..3eac957f7bb 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -3542,7 +3542,6 @@ enum RelativeDirection { CONTINUE DEPART ELEVATOR - ENTER_OR_EXIT_STATION ENTER_STATION EXIT_STATION FOLLOW_SIGNS 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 19f7f5cc758..be952f72303 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,22 +11,21 @@ "streetName": "street", "area": false, "relativeDirection": "DEPART", - "absoluteDirection" : "NORTHEAST", - "feature" : null + "absoluteDirection": "NORTHEAST", + "feature": null }, { "streetName": "elevator", "area": false, "relativeDirection": "ELEVATOR", - "absoluteDirection" : null, - "feature" : null - + "absoluteDirection": null, + "feature": null }, { - "streetName" : "entrance", - "area" : false, - "relativeDirection" : "ENTER_OR_EXIT_STATION", - "absoluteDirection" : null, + "streetName": "entrance", + "area": false, + "relativeDirection": "ENTER_OR_EXIT_STATION", + "absoluteDirection": null, "feature": { "__typename": "Entrance", "code": "A", From c4d665d21bb995678bc3e3b56a526b1cd3ce0c5f Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 18 Dec 2024 00:20:12 +0100 Subject: [PATCH 043/106] Reformat code and schema --- .../gtfs/generated/GraphQLDataFetchers.java | 15 +++++----- .../apis/gtfs/generated/GraphQLTypes.java | 1 - .../apis/gtfs/mapping/DirectionMapper.java | 3 +- .../mapping/StatesToWalkStepsMapper.java | 4 +-- .../model/vertex/StationEntranceVertex.java | 1 - .../opentripplanner/apis/gtfs/schema.graphqls | 28 +++++++++---------- 6 files changed, 24 insertions(+), 28 deletions(-) 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 760e890045b..d9c9ceb67e8 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,13 +392,6 @@ public interface GraphQLEmissions { public DataFetcher co2(); } - /** Real-time estimates for an arrival or departure at a certain place. */ - public interface GraphQLEstimatedTime { - public DataFetcher delay(); - - public DataFetcher time(); - } - /** Station entrance or exit, originating from OSM or GTFS data. */ public interface GraphQLEntrance { public DataFetcher code(); @@ -410,6 +403,13 @@ public interface GraphQLEntrance { public DataFetcher wheelchairAccessible(); } + /** Real-time estimates for an arrival or departure at a certain place. */ + public interface GraphQLEstimatedTime { + public DataFetcher delay(); + + public DataFetcher time(); + } + /** A 'medium' that a fare product applies to, for example cash, 'Oyster Card' or 'DB Navigator App'. */ public interface GraphQLFareMedium { public DataFetcher id(); @@ -1035,6 +1035,7 @@ public interface GraphQLRoutingError { public DataFetcher inputField(); } + /** A feature for a step */ public interface GraphQLStepFeature extends TypeResolver {} /** 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 fe598f88e40..a969b5223b1 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 @@ -4334,7 +4334,6 @@ public enum GraphQLRelativeDirection { CONTINUE, DEPART, ELEVATOR, - ENTER_OR_EXIT_STATION, ENTER_STATION, EXIT_STATION, FOLLOW_SIGNS, 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 1439cdd34c3..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; @@ -38,7 +38,6 @@ public static GraphQLRelativeDirection map(RelativeDirection relativeDirection) case UTURN_RIGHT -> GraphQLRelativeDirection.UTURN_RIGHT; case ENTER_STATION -> GraphQLRelativeDirection.ENTER_STATION; case EXIT_STATION -> GraphQLRelativeDirection.EXIT_STATION; - case ENTER_OR_EXIT_STATION -> GraphQLRelativeDirection.ENTER_OR_EXIT_STATION; case FOLLOW_SIGNS -> GraphQLRelativeDirection.FOLLOW_SIGNS; }; } 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 97310a47453..9365c50509c 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 @@ -530,9 +530,7 @@ private WalkStepBuilder createStationEntranceWalkStep( .of(vertex.id()) .withCode(vertex.code()) .withCoordinate(new WgsCoordinate(vertex.getCoordinate())) - .withWheelchairAccessibility( - vertex.wheelchairAccessibility() - ) + .withWheelchairAccessibility(vertex.wheelchairAccessibility()) .build(); // don't care what came before or comes after diff --git a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java index 6dd528204b2..ef94bbb64ef 100644 --- a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java @@ -37,5 +37,4 @@ public String toString() { .addStr("code", code) .toString(); } - } diff --git a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 3eac957f7bb..748b69607e0 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -491,18 +491,6 @@ type Emissions { co2: Grams } -"Real-time estimates for an arrival or departure at a certain place." -type EstimatedTime { - """ - The delay or "earliness" of the vehicle at a certain place. This estimate can change quite often. - - If the vehicle is early then this is a negative duration. - """ - delay: Duration! - "The estimate for a call event (such as arrival or departure) at a certain place. This estimate can change quite often." - time: OffsetDateTime! -} - "Station entrance or exit, originating from OSM or GTFS data." type Entrance { "Short text or a number that identifies the entrance or exit for passengers. For example, `A` or `B`." @@ -515,6 +503,18 @@ type Entrance { wheelchairAccessible: WheelchairBoarding } +"Real-time estimates for an arrival or departure at a certain place." +type EstimatedTime { + """ + The delay or "earliness" of the vehicle at a certain place. This estimate can change quite often. + + If the vehicle is early then this is a negative duration. + """ + delay: Duration! + "The estimate for a call event (such as arrival or departure) at a certain place. This estimate can change quite often." + time: OffsetDateTime! +} + "A 'medium' that a fare product applies to, for example cash, 'Oyster Card' or 'DB Navigator App'." type FareMedium { "ID of the medium" @@ -2359,7 +2359,7 @@ type Stoptime { """ The position of the stop in the pattern. This is required to start from 0 and be consecutive along the pattern, up to n-1 for a pattern with n stops. - + The purpose of this field is to identify the position of the stop within the pattern so it can be cross-referenced between different trips on the same pattern, as stopPosition can be different between trips even within the same pattern. @@ -2518,7 +2518,7 @@ type TripOnServiceDate { end: StopCall! """ The service date when the trip occurs. - + **Note**: A service date is a technical term useful for transit planning purposes and might not correspond to a how a passenger thinks of a calendar date. For example, a night bus running on Sunday morning at 1am to 3am, might have the previous Saturday's service date. From bf89f4969fd39cc2b1265f35b3c79836ac6ca496 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 18 Dec 2024 00:25:08 +0100 Subject: [PATCH 044/106] Fix tests --- .../resources/org/opentripplanner/apis/gtfs/schema.graphqls | 6 +++--- .../apis/gtfs/mapping/DirectionMapperTest.java | 1 + .../opentripplanner/apis/gtfs/expectations/walk-steps.json | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) 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 748b69607e0..6ac56be3d1d 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -507,7 +507,7 @@ type Entrance { type EstimatedTime { """ The delay or "earliness" of the vehicle at a certain place. This estimate can change quite often. - + If the vehicle is early then this is a negative duration. """ delay: Duration! @@ -2359,7 +2359,7 @@ type Stoptime { """ The position of the stop in the pattern. This is required to start from 0 and be consecutive along the pattern, up to n-1 for a pattern with n stops. - + The purpose of this field is to identify the position of the stop within the pattern so it can be cross-referenced between different trips on the same pattern, as stopPosition can be different between trips even within the same pattern. @@ -2518,7 +2518,7 @@ type TripOnServiceDate { end: StopCall! """ The service date when the trip occurs. - + **Note**: A service date is a technical term useful for transit planning purposes and might not correspond to a how a passenger thinks of a calendar date. For example, a night bus running on Sunday morning at 1am to 3am, might have the previous Saturday's service date. 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/resources/org/opentripplanner/apis/gtfs/expectations/walk-steps.json b/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/walk-steps.json index be952f72303..0e089aac428 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 @@ -24,7 +24,7 @@ { "streetName": "entrance", "area": false, - "relativeDirection": "ENTER_OR_EXIT_STATION", + "relativeDirection": "CONTINUE", "absoluteDirection": null, "feature": { "__typename": "Entrance", From 2eb0e7b45c87941ea5fb928c4929f748fa089d5b Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 18 Dec 2024 11:33:51 +0100 Subject: [PATCH 045/106] Add documentation --- .../model/vertex/StationEntranceVertex.java | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java index ef94bbb64ef..254c71c527d 100644 --- a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java @@ -5,28 +5,46 @@ 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 boolean accessible; - - public StationEntranceVertex(double x, double y, long nodeId, String code, boolean accessible) { - super(x, y, nodeId); + private final boolean wheelchairAccessible; + + public StationEntranceVertex( + double lat, + double lon, + long nodeId, + String code, + boolean wheelchairAccessible + ) { + super(lat, lon, nodeId); this.code = code; - this.accessible = accessible; + this.wheelchairAccessible = wheelchairAccessible; } + /** + * The id of the entrance which may or may not be human-readable. + */ public FeedScopedId id() { - return new FeedScopedId("osm", String.valueOf(nodeId)); + 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 accessible ? Accessibility.POSSIBLE : Accessibility.NOT_POSSIBLE; + return wheelchairAccessible ? Accessibility.POSSIBLE : Accessibility.NOT_POSSIBLE; } @Override From 977d8ebc943d0476f3e1723ed56f1e9ee48be39d Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 18 Dec 2024 12:07:09 +0100 Subject: [PATCH 046/106] Clean up --- .../ext/restapi/mapping/RelativeDirectionMapper.java | 3 +-- .../opentripplanner/ext/restapi/mapping/WalkStepMapper.java | 2 +- .../ext/restapi/model/ApiRelativeDirection.java | 1 - .../opentripplanner/apis/gtfs/datafetchers/stepImpl.java | 2 +- .../apis/transmodel/model/plan/PathGuidanceType.java | 2 +- .../module/osm/parameters/OsmExtractParameters.java | 1 - .../org/opentripplanner/model/plan/RelativeDirection.java | 6 ++++++ .../main/java/org/opentripplanner/model/plan/WalkStep.java | 2 +- 8 files changed, 11 insertions(+), 8 deletions(-) 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 708da1fd6c3..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; @@ -25,7 +25,6 @@ public static ApiRelativeDirection mapRelativeDirection(RelativeDirection domain case UTURN_RIGHT -> ApiRelativeDirection.UTURN_RIGHT; case ENTER_STATION -> ApiRelativeDirection.ENTER_STATION; case EXIT_STATION -> ApiRelativeDirection.EXIT_STATION; - case ENTER_OR_EXIT_STATION -> ApiRelativeDirection.ENTER_OR_EXIT_STATION; case FOLLOW_SIGNS -> ApiRelativeDirection.FOLLOW_SIGNS; }; } 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 9360620d40b..49e467a89db 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.getHighwayExit(); + api.exit = domain.isHighwayExit(); api.stayOn = domain.isStayOn(); api.area = domain.getArea(); api.bogusName = domain.nameIsDerived(); diff --git a/application/src/ext/java/org/opentripplanner/ext/restapi/model/ApiRelativeDirection.java b/application/src/ext/java/org/opentripplanner/ext/restapi/model/ApiRelativeDirection.java index eb624df5ea6..02a530f06de 100644 --- a/application/src/ext/java/org/opentripplanner/ext/restapi/model/ApiRelativeDirection.java +++ b/application/src/ext/java/org/opentripplanner/ext/restapi/model/ApiRelativeDirection.java @@ -21,6 +21,5 @@ public enum ApiRelativeDirection { UTURN_RIGHT, ENTER_STATION, EXIT_STATION, - ENTER_OR_EXIT_STATION, FOLLOW_SIGNS, } 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 4953d887713..7b1df1693d3 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 @@ -51,7 +51,7 @@ public DataFetcher> elevationProfile() { @Override public DataFetcher exit() { - return environment -> getSource(environment).getHighwayExit(); + return environment -> getSource(environment).isHighwayExit(); } @Override 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 5c2fa6f3a5e..74b30c83f44 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 @@ -65,7 +65,7 @@ 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()).getHighwayExit()) + .dataFetcher(environment -> ((WalkStep) environment.getSource()).isHighwayExit()) .build() ) .field( 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 37edaf687ab..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 @@ -49,7 +49,6 @@ public ZoneId timeZone() { return timeZone; } - @Nullable public boolean includeOsmSubwayEntrances() { return includeOsmSubwayEntrances; } 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 fbdb836ab6a..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,12 @@ 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; 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 eb59196b971..7edae8d7174 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java +++ b/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java @@ -130,7 +130,7 @@ public Optional getAbsoluteDirection() { /** * When exiting a highway or traffic circle, the exit name/number. */ - public String getHighwayExit() { + public String isHighwayExit() { return exit; } From b7cc6fddb042f6f69a098208925c8d71cba7ee29 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 18 Dec 2024 12:36:16 +0100 Subject: [PATCH 047/106] Remove enum mapper test for REST API --- .../ext/restapi/mapping/EnumMapperTest.java | 41 ------------------- .../ext/restapi/mapping/WalkStepMapper.java | 2 +- .../apis/gtfs/datafetchers/stepImpl.java | 11 +---- .../model/plan/PathGuidanceType.java | 4 +- .../opentripplanner/model/plan/WalkStep.java | 8 ++-- 5 files changed, 10 insertions(+), 56 deletions(-) diff --git a/application/src/ext-test/java/org/opentripplanner/ext/restapi/mapping/EnumMapperTest.java b/application/src/ext-test/java/org/opentripplanner/ext/restapi/mapping/EnumMapperTest.java index 35cc368fec4..5b03ada1c1f 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/restapi/mapping/EnumMapperTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/restapi/mapping/EnumMapperTest.java @@ -8,39 +8,11 @@ import java.util.Map; import java.util.function.Function; import org.junit.jupiter.api.Test; -import org.opentripplanner.ext.restapi.model.ApiAbsoluteDirection; -import org.opentripplanner.ext.restapi.model.ApiRelativeDirection; import org.opentripplanner.ext.restapi.model.ApiVertexType; -import org.opentripplanner.model.plan.AbsoluteDirection; -import org.opentripplanner.model.plan.RelativeDirection; import org.opentripplanner.model.plan.VertexType; public class EnumMapperTest { - private static final String MSG = - "Assert that the API enums have the exact same values that " + - "the domain enums of the same type, and that the specialized mapper is mapping all " + - "values. If this assumtion does not hold, create a new test."; - - @Test - public void map() { - try { - verifyExactMatch( - AbsoluteDirection.class, - ApiAbsoluteDirection.class, - AbsoluteDirectionMapper::mapAbsoluteDirection - ); - verifyExactMatch( - RelativeDirection.class, - ApiRelativeDirection.class, - RelativeDirectionMapper::mapRelativeDirection - ); - } catch (RuntimeException ex) { - System.out.println(MSG); - throw ex; - } - } - @Test public void testVertexTypeMapping() { verifyExplicitMatch( @@ -75,17 +47,4 @@ private , A extends Enum> void verifyExplicitMatch( assertTrue(rest.isEmpty()); } - private , A extends Enum> void verifyExactMatch( - Class domainClass, - Class apiClass, - Function mapper - ) { - List rest = new ArrayList<>(List.of(apiClass.getEnumConstants())); - for (D it : domainClass.getEnumConstants()) { - A result = mapper.apply(it); - assertEquals(result.name(), it.name()); - rest.remove(result); - } - assertTrue(rest.isEmpty()); - } } diff --git a/application/src/ext/java/org/opentripplanner/ext/restapi/mapping/WalkStepMapper.java b/application/src/ext/java/org/opentripplanner/ext/restapi/mapping/WalkStepMapper.java index 49e467a89db..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.isHighwayExit(); + api.exit = domain.highwayExit().orElse(null); api.stayOn = domain.isStayOn(); api.area = domain.getArea(); api.bogusName = domain.nameIsDerived(); diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/stepImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/stepImpl.java index 7b1df1693d3..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 @@ -7,7 +7,6 @@ import org.opentripplanner.apis.gtfs.mapping.DirectionMapper; import org.opentripplanner.apis.gtfs.mapping.StreetNoteMapper; import org.opentripplanner.model.plan.ElevationProfile.Step; -import org.opentripplanner.model.plan.RelativeDirection; import org.opentripplanner.model.plan.WalkStep; import org.opentripplanner.routing.alertpatch.TransitAlert; @@ -51,18 +50,12 @@ public DataFetcher> elevationProfile() { @Override public DataFetcher exit() { - return environment -> getSource(environment).isHighwayExit(); + return environment -> getSource(environment).highwayExit().orElse(null); } @Override public DataFetcher feature() { - return environment -> { - WalkStep source = getSource(environment); - if (source.getRelativeDirection() == RelativeDirection.ENTER_OR_EXIT_STATION) { - return source.getEntrance(); - } - return null; - }; + return environment -> getSource(environment).entrance().orElse(null); } @Override 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 74b30c83f44..86c8359c026 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 @@ -65,7 +65,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()).isHighwayExit()) + .dataFetcher(environment -> + ((WalkStep) environment.getSource()).highwayExit().orElse(null) + ) .build() ) .field( 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 7edae8d7174..fea605fe910 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java +++ b/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java @@ -130,15 +130,15 @@ public Optional getAbsoluteDirection() { /** * When exiting a highway or traffic circle, the exit name/number. */ - public String isHighwayExit() { - return exit; + public Optional highwayExit() { + return exit.describeConstable(); } /** * Get information about a subway station entrance or exit. */ - public Entrance getEntrance() { - return entrance; + public Optional entrance() { + return Optional.ofNullable(entrance); } /** From e473061d17441245d415771ee7f1c25c404a02a0 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 18 Dec 2024 12:44:25 +0100 Subject: [PATCH 048/106] Fix highway exits --- .../java/org/opentripplanner/model/plan/WalkStep.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 fea605fe910..7ade16de39a 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java +++ b/application/src/main/java/org/opentripplanner/model/plan/WalkStep.java @@ -44,7 +44,7 @@ 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; @@ -57,7 +57,7 @@ public final class WalkStep { AbsoluteDirection absoluteDirection, I18NString directionText, Set streetNotes, - String exit, + String highwayExit, Entrance entrance, ElevationProfile elevationProfile, boolean nameIsDerived, @@ -78,7 +78,7 @@ 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; @@ -131,7 +131,7 @@ public Optional getAbsoluteDirection() { * When exiting a highway or traffic circle, the exit name/number. */ public Optional highwayExit() { - return exit.describeConstable(); + return Optional.ofNullable(highwayExit); } /** From d40d0b3acceaa3bd117cb8dc10d3f4518f04dce2 Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Thu, 19 Dec 2024 10:55:34 +0000 Subject: [PATCH 049/106] use a service to store platform data --- .../apis/vectortiles/DebugStyleSpec.java | 2 - .../module/OsmBoardingLocationsModule.java | 28 +++++++------- .../graph_builder/module/osm/OsmModule.java | 37 +++++++------------ .../osminfo/OsmInfoGraphBuildRepository.java | 10 ++--- .../osminfo/OsmInfoGraphBuildService.java | 9 +++-- .../DefaultOsmInfoGraphBuildRepository.java | 16 ++++---- .../DefaultOsmInfoGraphBuildService.java | 6 +-- .../osminfo/model/OsmWayReferences.java | 26 ------------- .../service/osminfo/model/Platform.java | 8 ++++ .../street/model/edge/LinearPlatform.java | 9 ----- .../street/model/edge/LinearPlatformEdge.java | 11 ------ .../model/edge/LinearPlatformEdgeBuilder.java | 20 ---------- .../apis/vectortiles/style.json | 4 -- 13 files changed, 57 insertions(+), 129 deletions(-) delete mode 100644 application/src/main/java/org/opentripplanner/service/osminfo/model/OsmWayReferences.java create mode 100644 application/src/main/java/org/opentripplanner/service/osminfo/model/Platform.java delete mode 100644 application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatform.java delete mode 100644 application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdge.java delete mode 100644 application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdgeBuilder.java diff --git a/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java b/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java index 9d55d36f301..7070f8b486e 100644 --- a/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java +++ b/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java @@ -20,7 +20,6 @@ import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.edge.ElevatorHopEdge; import org.opentripplanner.street.model.edge.EscalatorEdge; -import org.opentripplanner.street.model.edge.LinearPlatformEdge; import org.opentripplanner.street.model.edge.PathwayEdge; import org.opentripplanner.street.model.edge.StreetEdge; import org.opentripplanner.street.model.edge.StreetStationCentroidLink; @@ -81,7 +80,6 @@ public class DebugStyleSpec { private static final Class[] EDGES_TO_DISPLAY = new Class[] { StreetEdge.class, AreaEdge.class, - LinearPlatformEdge.class, EscalatorEdge.class, PathwayEdge.class, ElevatorHopEdge.class, 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 0664b442f8a..7e2ba334f2f 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 @@ -2,9 +2,9 @@ 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; @@ -18,12 +18,11 @@ 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; import org.opentripplanner.street.model.edge.Edge; -import org.opentripplanner.street.model.edge.LinearPlatform; -import org.opentripplanner.street.model.edge.LinearPlatformEdge; import org.opentripplanner.street.model.edge.NamedArea; import org.opentripplanner.street.model.edge.StreetEdge; import org.opentripplanner.street.model.edge.StreetEdgeBuilder; @@ -149,25 +148,24 @@ private boolean connectVertexToStop(TransitStopVertex ts, StreetIndex index) { // if the boarding location is a non-area way we are finding the vertex representing the // center of the way, splitting if needed - var nearbyLinearPlatformEdges = new HashMap>(); + var nearbyEdges = new HashMap>(); for (var edge : index.getEdgesForEnvelope(envelope)) { - if (edge instanceof LinearPlatformEdge platformEdge) { - var platform = platformEdge.platform; + osmInfoGraphBuildService.findPlatform(edge).ifPresent(platform -> { if (matchesReference(stop, platform.references())) { - if (!nearbyLinearPlatformEdges.containsKey(platform)) { - var list = new ArrayList(); - list.add(platformEdge); - nearbyLinearPlatformEdges.put(platform, list); + if (!nearbyEdges.containsKey(platform)) { + var list = new ArrayList(); + list.add(edge); + nearbyEdges.put(platform, list); } else { - nearbyLinearPlatformEdges.get(platform).add(platformEdge); + nearbyEdges.get(platform).add(edge); } } - } + }); } - for (var platformEdgeList : nearbyLinearPlatformEdges.entrySet()) { - LinearPlatform platform = platformEdgeList.getKey(); + for (var platformEdgeList : nearbyEdges.entrySet()) { + Platform platform = platformEdgeList.getKey(); var name = platform.name(); var label = "platform-centroid/%s".formatted(stop.getId().toString()); var centroid = platform.geometry().getCentroid(); @@ -269,7 +267,7 @@ private void linkBoardingLocationToStop( ); } - private boolean matchesReference(StationElement stop, Set references) { + private boolean matchesReference(StationElement stop, Collection references) { var stopCode = stop.getCode(); var stopId = stop.getId().getId(); 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 d25d2c83d6c..673611f367f 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java @@ -27,13 +27,11 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.util.ElevationUtils; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; -import org.opentripplanner.service.osminfo.model.OsmWayReferences; +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; import org.opentripplanner.street.model.StreetTraversalPermission; -import org.opentripplanner.street.model.edge.LinearPlatform; -import org.opentripplanner.street.model.edge.LinearPlatformEdgeBuilder; import org.opentripplanner.street.model.edge.StreetEdge; import org.opentripplanner.street.model.edge.StreetEdgeBuilder; import org.opentripplanner.street.model.vertex.BarrierVertex; @@ -57,7 +55,6 @@ public class OsmModule implements GraphBuilderModule { */ private final List providers; private final Graph graph; - // TODO: Use this to store edge stop references private final OsmInfoGraphBuildRepository osmInfoGraphBuildRepository; private final VehicleParkingRepository parkingRepository; @@ -355,7 +352,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()); } @@ -413,8 +410,7 @@ private void buildBasicGraph() { way, i, permissions, - geometry, - platform + geometry ); params.edgeNamer().recordEdges(way, streets); @@ -423,7 +419,10 @@ private void buildBasicGraph() { StreetEdge backStreet = streets.back(); normalizer.applyWayProperties(street, backStreet, wayData, way); - osmInfoGraphBuildRepository.addReferences(street, new OsmWayReferences(List.of(street.toString()))); + platform.ifPresent(plat -> { + osmInfoGraphBuildRepository.addPlatform(street, plat); + osmInfoGraphBuildRepository.addPlatform(backStreet, plat); + }); applyEdgesToTurnRestrictions(way, startNode, endNode, street, backStreet); startNode = endNode; @@ -439,7 +438,7 @@ private void buildBasicGraph() { LOG.info(progress.completeMessage()); } - private Optional getPlatform(OsmWay way) { + private Optional getPlatform(OsmWay way) { if (way.isBoardingLocation()) { var nodeRefs = way.getNodeRefs(); var size = nodeRefs.size(); @@ -455,7 +454,7 @@ private Optional getPlatform(OsmWay way) { var references = way.getMultiTagValues(params.boardingAreaRefTags()); return Optional.of( - new LinearPlatform( + new Platform( params.edgeNamer().getNameForWay(way, "platform " + way.getId()), geometry, references @@ -523,8 +522,7 @@ private StreetEdgePair getEdgesForStreet( OsmWay way, int index, StreetTraversalPermission permissions, - LineString geometry, - Optional platform + LineString geometry ) { // No point in returning edges that can't be traversed by anyone. if (permissions.allowsNothing()) { @@ -550,8 +548,7 @@ private StreetEdgePair getEdgesForStreet( length, permissionsFront, geometry, - false, - platform + false ); } if (permissionsBack.allowsAnything()) { @@ -564,8 +561,7 @@ private StreetEdgePair getEdgesForStreet( length, permissionsBack, backGeometry, - true, - platform + true ); } if (street != null && backStreet != null) { @@ -582,19 +578,14 @@ private StreetEdge getEdgeForStreet( double length, StreetTraversalPermission permissions, LineString geometry, - boolean back, - Optional platform + boolean back ) { String label = "way " + way.getId() + " from " + index; label = label.intern(); I18NString name = params.edgeNamer().getNameForWay(way, label); float carSpeed = way.getOsmProvider().getOsmTagMapper().getCarSpeedForWay(way, back); - var seb = platform - .>map(p -> new LinearPlatformEdgeBuilder().withPlatform(p)) - .orElse(new StreetEdgeBuilder<>()); - - seb + StreetEdgeBuilder seb = new StreetEdgeBuilder<>() .withFromVertex(startEndpoint) .withToVertex(endEndpoint) .withGeometry(geometry) diff --git a/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildRepository.java b/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildRepository.java index c8a2b908893..e30e0dd19a7 100644 --- a/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildRepository.java +++ b/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildRepository.java @@ -2,7 +2,7 @@ import java.io.Serializable; import java.util.Optional; -import org.opentripplanner.service.osminfo.model.OsmWayReferences; +import org.opentripplanner.service.osminfo.model.Platform; import org.opentripplanner.street.model.edge.Edge; /** @@ -12,12 +12,12 @@ */ public interface OsmInfoGraphBuildRepository extends Serializable { /** - * TODO Add doc + * Associate the edge with a platform */ - void addReferences(Edge edge, OsmWayReferences info); + void addPlatform(Edge edge, Platform platform); /** - * TODO Add doc + * Find the platform the edge belongs to */ - Optional findReferences(Edge edge); + 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 index 784867ada30..6a50c3c92be 100644 --- a/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildService.java +++ b/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildService.java @@ -1,7 +1,7 @@ package org.opentripplanner.service.osminfo; import java.util.Optional; -import org.opentripplanner.service.osminfo.model.OsmWayReferences; +import org.opentripplanner.service.osminfo.model.Platform; import org.opentripplanner.street.model.edge.Edge; /** @@ -16,7 +16,10 @@ */ public interface OsmInfoGraphBuildService { /** - * TODO Add doc + * 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 findReferences(Edge edge); + Optional findPlatform(Edge edge); } 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 index 4ba575dc8b0..6505fdd67a4 100644 --- a/application/src/main/java/org/opentripplanner/service/osminfo/internal/DefaultOsmInfoGraphBuildRepository.java +++ b/application/src/main/java/org/opentripplanner/service/osminfo/internal/DefaultOsmInfoGraphBuildRepository.java @@ -7,31 +7,31 @@ import java.util.Objects; import java.util.Optional; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; -import org.opentripplanner.service.osminfo.model.OsmWayReferences; +import org.opentripplanner.service.osminfo.model.Platform; import org.opentripplanner.street.model.edge.Edge; public class DefaultOsmInfoGraphBuildRepository implements OsmInfoGraphBuildRepository, Serializable { - private final Map references = new HashMap<>(); + private final Map platforms = new HashMap<>(); @Inject public DefaultOsmInfoGraphBuildRepository() {} @Override - public void addReferences(Edge edge, OsmWayReferences info) { + public void addPlatform(Edge edge, Platform platform) { Objects.requireNonNull(edge); - Objects.requireNonNull(info); - this.references.put(edge, info); + Objects.requireNonNull(platform); + this.platforms.put(edge, platform); } @Override - public Optional findReferences(Edge edge) { - return Optional.ofNullable(references.get(edge)); + public Optional findPlatform(Edge edge) { + return Optional.ofNullable(platforms.get(edge)); } @Override public String toString() { - return "DefaultOsmInfoGraphBuildRepository{references size = " + references.size() + "}"; + 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 index 29271e17679..42eb5bf364f 100644 --- a/application/src/main/java/org/opentripplanner/service/osminfo/internal/DefaultOsmInfoGraphBuildService.java +++ b/application/src/main/java/org/opentripplanner/service/osminfo/internal/DefaultOsmInfoGraphBuildService.java @@ -4,7 +4,7 @@ import java.util.Optional; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildService; -import org.opentripplanner.service.osminfo.model.OsmWayReferences; +import org.opentripplanner.service.osminfo.model.Platform; import org.opentripplanner.street.model.edge.Edge; public class DefaultOsmInfoGraphBuildService implements OsmInfoGraphBuildService { @@ -17,8 +17,8 @@ public DefaultOsmInfoGraphBuildService(OsmInfoGraphBuildRepository repository) { } @Override - public Optional findReferences(Edge edge) { - return repository.findReferences(edge); + public Optional findPlatform(Edge edge) { + return repository.findPlatform(edge); } @Override diff --git a/application/src/main/java/org/opentripplanner/service/osminfo/model/OsmWayReferences.java b/application/src/main/java/org/opentripplanner/service/osminfo/model/OsmWayReferences.java deleted file mode 100644 index 6208758f153..00000000000 --- a/application/src/main/java/org/opentripplanner/service/osminfo/model/OsmWayReferences.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.opentripplanner.service.osminfo.model; - -import java.io.Serializable; -import java.util.Collection; -import java.util.List; - -/** - * TODO : Add java doc - */ -public class OsmWayReferences implements Serializable { - - private final List references; - - public OsmWayReferences(Collection references) { - this.references = references.stream().sorted().distinct().toList(); - } - - /** - * TODO : Add java doc - * - * returns a sorted distinct list of references - */ - public List references() { - return references; - } -} \ No newline at end of file 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..81ff388f69e --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/osminfo/model/Platform.java @@ -0,0 +1,8 @@ +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) { +} \ No newline at end of file diff --git a/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatform.java b/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatform.java deleted file mode 100644 index 688d4418530..00000000000 --- a/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatform.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.opentripplanner.street.model.edge; - -import java.io.Serializable; -import java.util.Set; -import org.locationtech.jts.geom.LineString; -import org.opentripplanner.framework.i18n.I18NString; - -public record LinearPlatform(I18NString name, LineString geometry, Set references) - implements Serializable {} diff --git a/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdge.java b/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdge.java deleted file mode 100644 index 849266166a2..00000000000 --- a/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdge.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.opentripplanner.street.model.edge; - -public class LinearPlatformEdge extends StreetEdge { - - public final LinearPlatform platform; - - protected LinearPlatformEdge(LinearPlatformEdgeBuilder builder) { - super(builder); - platform = builder.platform(); - } -} diff --git a/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdgeBuilder.java b/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdgeBuilder.java deleted file mode 100644 index 7057f9a7e42..00000000000 --- a/application/src/main/java/org/opentripplanner/street/model/edge/LinearPlatformEdgeBuilder.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.opentripplanner.street.model.edge; - -public class LinearPlatformEdgeBuilder extends StreetEdgeBuilder { - - private LinearPlatform platform; - - @Override - public LinearPlatformEdge buildAndConnect() { - return Edge.connectToGraph(new LinearPlatformEdge(this)); - } - - public LinearPlatform platform() { - return platform; - } - - public LinearPlatformEdgeBuilder withPlatform(LinearPlatform platform) { - this.platform = platform; - return this; - } -} diff --git a/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json b/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json index e513a44fcd9..66858390ab5 100644 --- a/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json +++ b/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json @@ -244,7 +244,6 @@ "class", "StreetEdge", "AreaEdge", - "LinearPlatformEdge", "EscalatorEdge", "PathwayEdge", "ElevatorHopEdge", @@ -421,7 +420,6 @@ "class", "StreetEdge", "AreaEdge", - "LinearPlatformEdge", "EscalatorEdge", "PathwayEdge", "ElevatorHopEdge", @@ -462,7 +460,6 @@ "class", "StreetEdge", "AreaEdge", - "LinearPlatformEdge", "EscalatorEdge", "PathwayEdge", "ElevatorHopEdge", @@ -495,7 +492,6 @@ "class", "StreetEdge", "AreaEdge", - "LinearPlatformEdge", "EscalatorEdge", "PathwayEdge", "ElevatorHopEdge", From 271f30587eec18e75e0a428b615c30b7a6bc7924 Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Thu, 19 Dec 2024 11:15:38 +0000 Subject: [PATCH 050/106] move Herrenberg data into the test method --- .../OsmBoardingLocationsModuleTest.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) 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 18c40bac898..e0cf32eda9b 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 @@ -42,16 +42,6 @@ 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() { return Stream.of( Arguments.of( @@ -70,6 +60,16 @@ static Stream testCases() { ) @MethodSource("testCases") 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); @@ -146,13 +146,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) @@ -162,7 +162,7 @@ void addAndLinkBoardingLocations(boolean areaVisibility, Set linkedVerti assertEquals( linkedVertices, - platform + platformCentroid .getIncomingStreetEdges() .stream() .map(Edge::getFromVertex) From 5cc681285d4ff5739a0aafe9895a3914f534c977 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 19 Dec 2024 16:15:40 +0100 Subject: [PATCH 051/106] Specify version in parent pom --- gtfs-realtime-protobuf/pom.xml | 4 ---- pom.xml | 7 ++++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/gtfs-realtime-protobuf/pom.xml b/gtfs-realtime-protobuf/pom.xml index d3c3305b9b2..dd3990207c9 100644 --- a/gtfs-realtime-protobuf/pom.xml +++ b/gtfs-realtime-protobuf/pom.xml @@ -11,15 +11,11 @@ gtfs-realtime-protobuf OpenTripPlanner - GTFS Realtime (protobuf) - - 4.28.3 - com.google.protobuf protobuf-java - ${protobuf.version} diff --git a/pom.xml b/pom.xml index 4108973c7ff..98cf77db29e 100644 --- a/pom.xml +++ b/pom.xml @@ -73,6 +73,7 @@ 2.0.15 1.27 4.0.5 + 4.28.3 UTF-8 opentripplanner/OpenTripPlanner @@ -485,7 +486,11 @@ java-snapshot-testing-junit5 2.3.0 - + + com.google.protobuf + protobuf-java + ${protobuf.version} + From eaafc689606dcd07d2da138cb5813c9cda9b3453 Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Thu, 19 Dec 2024 15:22:20 +0000 Subject: [PATCH 052/106] add test for linear platform --- .../module/OsmBoardingLocationsModule.java | 23 +- .../graph_builder/module/osm/OsmModule.java | 7 +- .../service/osminfo/model/Platform.java | 3 +- .../OsmBoardingLocationsModuleTest.java | 224 +++++++++++++++++- .../graph_builder/module/moorgate.osm.pbf | Bin 0 -> 165687 bytes 5 files changed, 236 insertions(+), 21 deletions(-) create mode 100644 application/src/test/resources/org/opentripplanner/graph_builder/module/moorgate.osm.pbf 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 7e2ba334f2f..07c39cc2d8a 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 @@ -115,7 +115,6 @@ public void buildGraph() { private boolean connectVertexToStop(TransitStopVertex ts, StreetIndex index) { var stop = ts.getStop(); var stopCode = stop.getCode(); - var stopId = stop.getId().getId(); Envelope envelope = new Envelope(ts.getCoordinate()); double xscale = Math.cos(ts.getCoordinate().y * Math.PI / 180); @@ -151,17 +150,19 @@ private boolean connectVertexToStop(TransitStopVertex ts, StreetIndex index) { var nearbyEdges = new HashMap>(); for (var edge : index.getEdgesForEnvelope(envelope)) { - 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); + 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()) { 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 673611f367f..195d36b9ed1 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 @@ -92,7 +92,12 @@ public static OsmModuleBuilder of( OsmInfoGraphBuildRepository osmInfoGraphBuildRepository, VehicleParkingRepository vehicleParkingRepository ) { - return new OsmModuleBuilder(providers, graph, osmInfoGraphBuildRepository, vehicleParkingRepository); + return new OsmModuleBuilder( + providers, + graph, + osmInfoGraphBuildRepository, + vehicleParkingRepository + ); } public static OsmModuleBuilder of( 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 index 81ff388f69e..91d78385a34 100644 --- a/application/src/main/java/org/opentripplanner/service/osminfo/model/Platform.java +++ b/application/src/main/java/org/opentripplanner/service/osminfo/model/Platform.java @@ -4,5 +4,4 @@ import org.locationtech.jts.geom.LineString; import org.opentripplanner.framework.i18n.I18NString; -public record Platform(I18NString name, LineString geometry, Set references) { -} \ No newline at end of file +public record Platform(I18NString name, LineString geometry, Set references) {} 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 e0cf32eda9b..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,14 +2,21 @@ 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; @@ -34,15 +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(); - static Stream testCases() { + static Stream herrenbergTestCases() { return Stream.of( Arguments.of( false, @@ -55,10 +58,14 @@ 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) @@ -69,7 +76,7 @@ void addAndLinkBoardingLocations(boolean areaVisibility, Set linkedVerti .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); @@ -182,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 @@ -192,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/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 0000000000000000000000000000000000000000..ee95a0e7c5165a9fcd3de992da15a4a065ac7009 GIT binary patch literal 165687 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

SsOp}4-6F_i%>4w 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 Date: Fri, 20 Dec 2024 14:12:33 +0100 Subject: [PATCH 053/106] refactor: Extract TextVariablesSubstitution from EnvironmentVariableReplacer. This enable us to use this in places where we do not want to inject environment variables. --- .../project/EnvironmentVariableReplacer.java | 67 ++++++------ .../utils/text/TextVariablesSubstitution.java | 102 ++++++++++++++++++ .../text/TextVariablesSubstitutionTest.java | 56 ++++++++++ 3 files changed, 192 insertions(+), 33 deletions(-) create mode 100644 utils/src/main/java/org/opentripplanner/utils/text/TextVariablesSubstitution.java create mode 100644 utils/src/test/java/org/opentripplanner/utils/text/TextVariablesSubstitutionTest.java 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/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); + } +} From 1c8bf7b5d795c46152659fda3498c1cd1f9f280f Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Thu, 2 Jan 2025 12:23:00 +0100 Subject: [PATCH 054/106] feature: Add the ability to load custom documentation from a properties file --- .../custom-documentation-entur.properties | 27 +++ .../injectdoc/ApiDocumentationProfile.java | 27 +++ .../injectdoc/CustomDocumentation.java | 171 ++++++++++++++++++ .../injectdoc/CustomDocumentationTest.java | 76 ++++++++ 4 files changed, 301 insertions(+) create mode 100644 application/src/ext/resources/org/opentripplanner/ext/apis/transmodel/custom-documentation-entur.properties create mode 100644 application/src/main/java/org/opentripplanner/apis/support/graphql/injectdoc/ApiDocumentationProfile.java create mode 100644 application/src/main/java/org/opentripplanner/apis/support/graphql/injectdoc/CustomDocumentation.java create mode 100644 application/src/test/java/org/opentripplanner/apis/support/graphql/injectdoc/CustomDocumentationTest.java 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/support/graphql/injectdoc/ApiDocumentationProfile.java b/application/src/main/java/org/opentripplanner/apis/support/graphql/injectdoc/ApiDocumentationProfile.java new file mode 100644 index 00000000000..71b4e06a864 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/support/graphql/injectdoc/ApiDocumentationProfile.java @@ -0,0 +1,27 @@ +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. " + + "The default should be used in most cases. A profile may be used to deprecate part of the " + + "API in case it is not supported."; + + @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..44629659b96 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/support/graphql/injectdoc/CustomDocumentation.java @@ -0,0 +1,171 @@ +package org.opentripplanner.apis.support.graphql.injectdoc; + +import java.io.IOException; +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; + + /** + * Pacakge 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(input); + 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/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)); + } +} From 85e124f3dfd88359fc0732b542441f7ea13403c3 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Thu, 2 Jan 2025 12:25:07 +0100 Subject: [PATCH 055/106] feature: Make a GraphQL schema visitor to inject custom documentation --- .../injectdoc/InjectCustomDocumentation.java | 173 ++++++++++++++++++ .../InjectCustomDocumentationTest.java | 134 ++++++++++++++ .../InjectCustomDocumentationTest.graphql | 52 ++++++ ...ctCustomDocumentationTest.graphql.expected | 93 ++++++++++ 4 files changed, 452 insertions(+) create mode 100644 application/src/main/java/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentation.java create mode 100644 application/src/test/java/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.java create mode 100644 application/src/test/resources/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.graphql create mode 100644 application/src/test/resources/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.graphql.expected 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..f2793a4e6c3 --- /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 witch inject 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 f1 = customDocumentation + .fieldDescription(typeName, fieldName, field.getDescription()) + .map(doc -> setDescription.apply(field, doc)); + + Optional f2 = customDocumentation + .fieldDeprecatedReason(typeName, fieldName, originalDeprecatedReason) + .map(doc -> setDeprecatedReason.apply(f1.orElse(field), doc)); + + f2.or(() -> f1).ifPresent(f -> changeNode(context, f)); + + return CONTINUE; + } +} 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..0f326a373aa --- /dev/null +++ b/application/src/test/java/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.java @@ -0,0 +1,134 @@ +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; + +/** + * This test read in a schema file, inject 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 sdl; + private String sdlExpected; + + @BeforeEach + void setUp() throws IOException { + 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", + "Duration.description", + "InputType.description", + "InputType.a.description", + "InputType.b.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) + // Some editors like IntelliJ remove space characters at the end of a + // line, so we do the same here to avoid false positive results. + .replaceAll(" +\\n", "\n"); + + 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("BType.a.deprecated", "CType.b.deprecated.append"); + + assertEquals(expected, missingValues); + assertEquals(sdlExpected, result); + } + + 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/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..599c4d3b12a --- /dev/null +++ b/application/src/test/resources/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.graphql @@ -0,0 +1,52 @@ +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 +} + +# Add doc to scalar +scalar Duration + +# Add doc to input type +input InputType { + a: String + b: String +} 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..c8cb7f680bf --- /dev/null +++ b/application/src/test/resources/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.graphql.expected @@ -0,0 +1,93 @@ +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") +} + +"Duration.description" +scalar Duration + +"InputType.description" +input InputType { + "InputType.a.description" + a: String + b: String @deprecated(reason : "InputType.b.deprecated") +} From d2fd01b2ca7c2a664f9e12996fe6ffa8cc968bbd Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Thu, 2 Jan 2025 12:33:24 +0100 Subject: [PATCH 056/106] feature: Make the API documentation profile configurable --- .../injectdoc/ApiDocumentationProfile.java | 9 +- .../config/routerconfig/ServerConfig.java | 26 ++++- doc/user/RouterConfiguration.md | 95 +++++++++++-------- 3 files changed, 83 insertions(+), 47 deletions(-) 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 index 71b4e06a864..1ed63a9bd96 100644 --- 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 @@ -7,9 +7,12 @@ public enum ApiDocumentationProfile implements DocumentedEnum 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/doc/user/RouterConfiguration.md b/doc/user/RouterConfiguration.md index 7dae97fd74c..10065eab6db 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 support 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 support 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"` From 90ac6d2bf29b8183707fdbd891e35eadbb6bcedb Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Thu, 2 Jan 2025 12:40:52 +0100 Subject: [PATCH 057/106] feature: Inject custom documentation in Transmodel API --- .../apis/transmodel/TransmodelAPI.java | 3 ++ .../transmodel/TransmodelGraphQLSchema.java | 31 ++++++++++++++++--- .../configure/ConstructApplication.java | 1 + .../TransmodelGraphQLSchemaTest.java | 2 ++ 4 files changed, 32 insertions(+), 5 deletions(-) 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 922f9f5244b..430aa8d740f 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java @@ -27,6 +27,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; @@ -42,8 +43,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; @@ -155,10 +160,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") @@ -1620,7 +1627,7 @@ private GraphQLSchema create() { .field(DatedServiceJourneyQuery.createQuery(datedServiceJourneyType)) .build(); - return GraphQLSchema + var schema = GraphQLSchema .newSchema() .query(queryType) .additionalType(placeInterface) @@ -1628,9 +1635,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/standalone/configure/ConstructApplication.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java index b4edbb36299..65a1146f8f2 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java @@ -183,6 +183,7 @@ private void setupTransitRoutingServer() { routerConfig().transmodelApi(), timetableRepository(), routerConfig().routingRequestDefaults(), + routerConfig().server().apiDocumentationProfile(), routerConfig().transitTuningConfig() ); } 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); From 0bf3c72fc2a8ff15e20b129348737cd1fef53473 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Thu, 2 Jan 2025 14:53:36 +0100 Subject: [PATCH 058/106] test: Ignore white-space when comparing text In the case used, we compare to GraphQL schemas. --- .../_support/text/TextAssertions.java | 64 +++++++++++++++++++ .../_support/text/TextAssertionsTest.java | 48 ++++++++++++++ .../InjectCustomDocumentationTest.java | 4 +- 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 application/src/test/java/org/opentripplanner/_support/text/TextAssertions.java create mode 100644 application/src/test/java/org/opentripplanner/_support/text/TextAssertionsTest.java 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..b1bdb3792cb --- /dev/null +++ b/application/src/test/java/org/opentripplanner/_support/text/TextAssertionsTest.java @@ -0,0 +1,48 @@ +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.assertEquals( + """ + Expected(@end-of-text): <> + Actual (@line 1): + """, + ex.getMessage() + ); + } +} 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 index 0f326a373aa..3e856faf413 100644 --- 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 @@ -18,6 +18,7 @@ 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 read in a schema file, inject documentation and convert the @@ -120,7 +121,8 @@ void test() { var expected = List.of("BType.a.deprecated", "CType.b.deprecated.append"); assertEquals(expected, missingValues); - assertEquals(sdlExpected, result); + + TextAssertions.assertLinesEquals(sdlExpected, result); } String loadSchemaResource(String suffix) throws IOException { From f2406e7cafb0b13f09f9b7769ec31cdeb61b5cd8 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Thu, 2 Jan 2025 16:53:29 +0100 Subject: [PATCH 059/106] refactor: Cleanup test and make test run on Windows OS --- .../_support/text/TextAssertionsTest.java | 13 ++++++------ .../InjectCustomDocumentationTest.java | 21 +++++++++++-------- .../InjectCustomDocumentationTest.graphql | 2 ++ ...ctCustomDocumentationTest.graphql.expected | 2 ++ 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/application/src/test/java/org/opentripplanner/_support/text/TextAssertionsTest.java b/application/src/test/java/org/opentripplanner/_support/text/TextAssertionsTest.java index b1bdb3792cb..739b7b59c4b 100644 --- a/application/src/test/java/org/opentripplanner/_support/text/TextAssertionsTest.java +++ b/application/src/test/java/org/opentripplanner/_support/text/TextAssertionsTest.java @@ -37,12 +37,13 @@ void testEndOfText() { org.opentest4j.AssertionFailedError.class, () -> assertLinesEquals("A\n", "A\nExtra Line") ); - Assertions.assertEquals( - """ - Expected(@end-of-text): <> - Actual (@line 1): - """, - ex.getMessage() + 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/support/graphql/injectdoc/InjectCustomDocumentationTest.java b/application/src/test/java/org/opentripplanner/apis/support/graphql/injectdoc/InjectCustomDocumentationTest.java index 3e856faf413..4231d7079e5 100644 --- 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 @@ -34,12 +34,11 @@ class InjectCustomDocumentationTest { private GraphQLSchema schema; - private String sdl; private String sdlExpected; @BeforeEach void setUp() throws IOException { - sdl = loadSchemaResource(".graphql"); + var sdl = loadSchemaResource(".graphql"); sdlExpected = loadSchemaResource(".graphql.expected"); var parser = new SchemaParser(); @@ -88,10 +87,12 @@ static Map text() { "AEnum.description", "AEnum.E1.description", "AEnum.E2.deprecated", + "AEnum.E3.deprecated", "Duration.description", "InputType.description", "InputType.a.description", - "InputType.b.deprecated" + "InputType.b.deprecated", + "InputType.c.deprecated" ) .collect(Collectors.toMap(e -> e, e -> e)); } @@ -103,11 +104,7 @@ void test() { var visitor = new InjectCustomDocumentation(customDocumentation); var newSchema = SchemaTransformer.transformSchema(schema, visitor); var p = new SchemaPrinter(); - var result = p - .print(newSchema) - // Some editors like IntelliJ remove space characters at the end of a - // line, so we do the same here to avoid false positive results. - .replaceAll(" +\\n", "\n"); + var result = p.print(newSchema); var missingValues = texts .values() @@ -118,13 +115,19 @@ void test() { // There is a bug in the Java GraphQL API, existing deprecated // doc is not updated or replaced. - var expected = List.of("BType.a.deprecated", "CType.b.deprecated.append"); + 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; 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 index 599c4d3b12a..33deaa2a364 100644 --- 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 @@ -40,6 +40,7 @@ type QueryType { enum AEnum { E1 E2 + E3 @deprecated(reason: "REPLACE") } # Add doc to scalar @@ -49,4 +50,5 @@ scalar Duration 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 index c8cb7f680bf..47319e07ae0 100644 --- 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 @@ -80,6 +80,7 @@ enum AEnum { "AEnum.E1.description" E1 E2 @deprecated(reason : "AEnum.E2.deprecated") + E3 @deprecated(reason : "REPLACE") } "Duration.description" @@ -90,4 +91,5 @@ input InputType { "InputType.a.description" a: String b: String @deprecated(reason : "InputType.b.deprecated") + c: String @deprecated(reason : "REPLACE") } From 713143b74df15a5cb25cafe3300530bdc5f7d6ba Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 2 Jan 2025 22:58:53 +0100 Subject: [PATCH 060/106] Use three states for accessibility --- .../module/osm/ElevatorProcessor.java | 4 ++-- .../graph_builder/module/osm/VertexGenerator.java | 4 +--- .../opentripplanner/osm/model/OsmWithTags.java | 2 +- .../mapping/StatesToWalkStepsMapper.java | 1 - .../model/vertex/StationEntranceVertex.java | 8 ++++---- .../street/model/vertex/VertexFactory.java | 7 +++++-- .../osm/model/OsmWithTagsTest.java | 15 +++++++++++++++ 7 files changed, 28 insertions(+), 13 deletions(-) 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/VertexGenerator.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java index b4c3c08aa8b..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 @@ -104,9 +104,7 @@ IntersectionVertex getVertexForOsmNode(OsmNode node, OsmWithTags way) { if (includeOsmSubwayEntrances && node.isSubwayEntrance()) { String ref = node.getTag("ref"); - - boolean accessible = node.isTag("wheelchair", "yes"); - iv = vertexFactory.stationEntrance(nid, coordinate, ref, accessible); + iv = vertexFactory.stationEntrance(nid, coordinate, ref, node.wheelchairAccessibility()); } if (iv == null) { diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmWithTags.java b/application/src/main/java/org/opentripplanner/osm/model/OsmWithTags.java index 3f47d4454bd..10214460b15 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmWithTags.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmWithTags.java @@ -139,7 +139,7 @@ public boolean isTagFalse(String tag) { /** * Returns the level of wheelchair access of the element. */ - public Accessibility getWheelchairAccessibility() { + public Accessibility wheelchairAccessibility() { if (isTagTrue("wheelchair")) { return Accessibility.POSSIBLE; } else if (isTagFalse("wheelchair")) { diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java index 9365c50509c..ae7674c7a96 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 @@ -29,7 +29,6 @@ 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.basic.Accessibility; import org.opentripplanner.transit.model.site.Entrance; /** diff --git a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java index 254c71c527d..7b9a94b0725 100644 --- a/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/StationEntranceVertex.java @@ -13,18 +13,18 @@ public class StationEntranceVertex extends OsmVertex { private static final String FEED_ID = "osm"; private final String code; - private final boolean wheelchairAccessible; + private final Accessibility wheelchairAccessibility; public StationEntranceVertex( double lat, double lon, long nodeId, String code, - boolean wheelchairAccessible + Accessibility wheelchairAccessibility ) { super(lat, lon, nodeId); this.code = code; - this.wheelchairAccessible = wheelchairAccessible; + this.wheelchairAccessibility = wheelchairAccessibility; } /** @@ -44,7 +44,7 @@ public String code() { } public Accessibility wheelchairAccessibility() { - return wheelchairAccessible ? Accessibility.POSSIBLE : Accessibility.NOT_POSSIBLE; + return wheelchairAccessibility; } @Override 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 853d66c56aa..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; @@ -98,9 +99,11 @@ public StationEntranceVertex stationEntrance( long nid, Coordinate coordinate, String code, - boolean accessible + Accessibility wheelchairAccessibility ) { - return addToGraph(new StationEntranceVertex(coordinate.x, coordinate.y, nid, code, accessible)); + return addToGraph( + new StationEntranceVertex(coordinate.x, coordinate.y, nid, code, wheelchairAccessibility) + ); } public OsmVertex osm( 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()); From 0b6a74a840c16b02b6bd8d85393084e572ce664d Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 2 Jan 2025 23:07:39 +0100 Subject: [PATCH 061/106] Update documentation --- .../opentripplanner/apis/gtfs/schema.graphqls | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) 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 6ac56be3d1d..f8a51325a69 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -3535,15 +3535,29 @@ 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 known then entrance information is in the `step.entity` field. + """ CONTINUE DEPART ELEVATOR + "Entering a public transport station. If known then entrance information is in the `step.entity` field." ENTER_STATION + "Exiting a public transport station. If known then entrance information is in the `step.entity` field." EXIT_STATION + "Follow the signs indicating a specific location like \"platform 1\" or \"exit B\"." FOLLOW_SIGNS HARD_LEFT HARD_RIGHT From 1518e38dd6f0ca9cc4656ce38dc9ccc009fdcef6 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Fri, 3 Jan 2025 10:44:48 +0100 Subject: [PATCH 062/106] Apply review feedback --- pom.xml | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index 98cf77db29e..9cf7b514ce7 100644 --- a/pom.xml +++ b/pom.xml @@ -59,21 +59,23 @@ 176 + 32.1 2.53 2.18.2 + 4.0.5 3.1.9 5.11.4 - 1.14.1 - 5.6.0 1.5.12 9.12.0 - 2.0.16 + 1.14.1 2.0.15 - 1.27 - 4.0.5 + 5.6.0 4.28.3 + 1.27 + 2.0.16 + UTF-8 opentripplanner/OpenTripPlanner @@ -390,7 +392,7 @@ - + com.google.cloud libraries-bom 26.51.0 @@ -398,6 +400,18 @@ import + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + + com.google.guava + guava + 33.3.1-jre + + org.slf4j @@ -420,11 +434,6 @@ trove4j 3.0.3 - - com.google.guava - guava - 33.3.1-jre - @@ -486,11 +495,7 @@ java-snapshot-testing-junit5 2.3.0 - - com.google.protobuf - protobuf-java - ${protobuf.version} - + From 249b01b9e16dadf2fc41df1e576b312795324c0b Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Fri, 3 Jan 2025 11:00:34 +0100 Subject: [PATCH 063/106] Update list of GTFS Fares v2 classes --- .../org/opentripplanner/gtfs/graphbuilder/GtfsModule.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/opentripplanner/gtfs/graphbuilder/GtfsModule.java b/application/src/main/java/org/opentripplanner/gtfs/graphbuilder/GtfsModule.java index a5fe3641e3c..fc5e5e276d7 100644 --- a/application/src/main/java/org/opentripplanner/gtfs/graphbuilder/GtfsModule.java +++ b/application/src/main/java/org/opentripplanner/gtfs/graphbuilder/GtfsModule.java @@ -14,6 +14,7 @@ import org.onebusaway.csv_entities.EntityHandler; import org.onebusaway.gtfs.impl.GtfsRelationalDaoImpl; import org.onebusaway.gtfs.model.Agency; +import org.onebusaway.gtfs.model.Area; import org.onebusaway.gtfs.model.FareAttribute; import org.onebusaway.gtfs.model.FareLegRule; import org.onebusaway.gtfs.model.FareMedium; @@ -28,6 +29,7 @@ import org.onebusaway.gtfs.model.ShapePoint; import org.onebusaway.gtfs.model.Stop; import org.onebusaway.gtfs.model.StopArea; +import org.onebusaway.gtfs.model.StopAreaElement; import org.onebusaway.gtfs.model.Trip; import org.onebusaway.gtfs.serialization.GtfsReader; import org.onebusaway.gtfs.services.GenericMutableDao; @@ -66,7 +68,9 @@ public class GtfsModule implements GraphBuilderModule { FareTransferRule.class, RiderCategory.class, FareMedium.class, - StopArea.class + StopArea.class, + StopAreaElement.class, + Area.class ); private static final Logger LOG = LoggerFactory.getLogger(GtfsModule.class); From 21f15bde2642a5019eeac847e34a8756eb1ef76d Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Fri, 3 Jan 2025 13:54:23 +0100 Subject: [PATCH 064/106] Update to latest OBA --- application/pom.xml | 2 +- .../java/org/opentripplanner/gtfs/graphbuilder/GtfsModule.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index c3b4a6ee582..231675a99de 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -296,7 +296,7 @@ org.onebusaway onebusaway-gtfs - 4.3.0 + 5.0.0 diff --git a/application/src/main/java/org/opentripplanner/gtfs/graphbuilder/GtfsModule.java b/application/src/main/java/org/opentripplanner/gtfs/graphbuilder/GtfsModule.java index fc5e5e276d7..3548312b79a 100644 --- a/application/src/main/java/org/opentripplanner/gtfs/graphbuilder/GtfsModule.java +++ b/application/src/main/java/org/opentripplanner/gtfs/graphbuilder/GtfsModule.java @@ -28,7 +28,6 @@ import org.onebusaway.gtfs.model.ServiceCalendarDate; import org.onebusaway.gtfs.model.ShapePoint; import org.onebusaway.gtfs.model.Stop; -import org.onebusaway.gtfs.model.StopArea; import org.onebusaway.gtfs.model.StopAreaElement; import org.onebusaway.gtfs.model.Trip; import org.onebusaway.gtfs.serialization.GtfsReader; @@ -68,7 +67,6 @@ public class GtfsModule implements GraphBuilderModule { FareTransferRule.class, RiderCategory.class, FareMedium.class, - StopArea.class, StopAreaElement.class, Area.class ); From 5b12c7f5bfdd4264d3b81322924b5a8f2e68e138 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Mon, 6 Jan 2025 09:33:22 +0100 Subject: [PATCH 065/106] Update docs --- .../opentripplanner/apis/gtfs/schema.graphqls | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) 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 f8a51325a69..afae31e19f0 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -3548,14 +3548,26 @@ enum RelativeDirection { - 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 known then entrance information is in the `step.entity` field. + entering or exiting. If it _is_ known then `ENTER_STATION`/`EXIT_STATION` is used. + + If available, entrance information is in the `step.feature` field. """ CONTINUE DEPART ELEVATOR - "Entering a public transport station. If known then entrance information is in the `step.entity` field." + """ + Entering a public transport station. If it's not known if the passenger is entering or exiting + then `CONTINUE` is used. + + If available, entrance information is in the `step.feature` field. + """ ENTER_STATION - "Exiting a public transport station. If known then entrance information is in the `step.entity` field." + """ + Exiting a public transport station. If it's not known if the passenger is entering or exiting + then `CONTINUE` is used. + + If available then entrance information is in the `step.feature` field. + """ EXIT_STATION "Follow the signs indicating a specific location like \"platform 1\" or \"exit B\"." FOLLOW_SIGNS From 8c6fda4e0df377deadd25e6649090e518290bac9 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Mon, 6 Jan 2025 09:46:37 +0100 Subject: [PATCH 066/106] Extract entrance from transit link --- .../model/plan/WalkStepBuilder.java | 2 +- .../mapping/StatesToWalkStepsMapper.java | 16 +++++++++++++--- .../model/edge/StreetTransitEntranceLink.java | 13 +++++++++++++ .../opentripplanner/apis/gtfs/schema.graphqls | 6 +++--- 4 files changed, 30 insertions(+), 7 deletions(-) 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 877b40f24fd..75589718861 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java +++ b/application/src/main/java/org/opentripplanner/model/plan/WalkStepBuilder.java @@ -76,7 +76,7 @@ public WalkStepBuilder withExit(String exit) { return this; } - public WalkStepBuilder withEntrance(Entrance entrance) { + public WalkStepBuilder withEntrance(@Nullable Entrance entrance) { this.entrance = entrance; return this; } 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 ae7674c7a96..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; @@ -160,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; } @@ -181,7 +182,14 @@ private void processState(State backState, State forwardState) { 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; } @@ -545,7 +553,8 @@ private void createAndSaveStep( State forwardState, I18NString name, RelativeDirection direction, - Edge edge + Edge edge, + @Nullable Entrance entrance ) { addStep( createWalkStep(forwardState, backState) @@ -553,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/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/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index afae31e19f0..8582fc7ba6f 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -3549,7 +3549,7 @@ enum RelativeDirection { - 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. - + If available, entrance information is in the `step.feature` field. """ CONTINUE @@ -3558,14 +3558,14 @@ enum RelativeDirection { """ Entering a public transport station. If it's not known if the passenger is entering or exiting then `CONTINUE` is used. - + If available, entrance information 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. - + If available then entrance information is in the `step.feature` field. """ EXIT_STATION From bd94b1390dc720379d7394feb8824e8225e1c2b4 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Mon, 6 Jan 2025 09:49:35 +0100 Subject: [PATCH 067/106] Add test for extracting entrance from pathway data --- .../algorithm/mapping/StatesToWalkStepsMapperTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 From 74106b5f2fd16c6fe8024089553bb58713df2faf Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Mon, 6 Jan 2025 10:00:57 +0100 Subject: [PATCH 068/106] Update schema docs --- .../org/opentripplanner/apis/gtfs/schema.graphqls | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 8582fc7ba6f..c9d0f3d73af 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -3549,8 +3549,7 @@ enum RelativeDirection { - 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. - - If available, entrance information is in the `step.feature` field. + More information about the entrance is in the `step.feature` field. """ CONTINUE DEPART @@ -3559,14 +3558,14 @@ enum RelativeDirection { Entering a public transport station. If it's not known if the passenger is entering or exiting then `CONTINUE` is used. - If available, entrance information is in the `step.feature` field. + 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. - If available then entrance information is in the `step.feature` field. + 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\"." From 51080a8510956dcd40cd578a4ac1320fb1de01ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:05:31 +0000 Subject: [PATCH 069/106] fix(deps): update dependency com.google.guava:guava to v33.4.0-jre --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fbc65c1e53a..58af808d0e1 100644 --- a/pom.xml +++ b/pom.xml @@ -422,7 +422,7 @@ com.google.guava guava - 33.3.1-jre + 33.4.0-jre From 628bf95363a64a7d7d3ccd3e66a7b08d66afe794 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Tue, 7 Jan 2025 17:48:06 +0100 Subject: [PATCH 070/106] Rename 'code' to 'publicCode' --- .../apis/gtfs/datafetchers/EntranceImpl.java | 2 +- .../apis/gtfs/generated/GraphQLDataFetchers.java | 4 ++-- .../opentripplanner/apis/gtfs/generated/GraphQLTypes.java | 6 +++++- .../org/opentripplanner/apis/gtfs/schema.graphqls | 8 ++++---- .../opentripplanner/apis/gtfs/GraphQLIntegrationTest.java | 3 --- .../apis/gtfs/expectations/walk-steps.json | 2 +- .../opentripplanner/apis/gtfs/queries/walk-steps.graphql | 2 +- 7 files changed, 14 insertions(+), 13 deletions(-) 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 index 9891d107479..f9faa9cc4d1 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EntranceImpl.java @@ -9,7 +9,7 @@ public class EntranceImpl implements GraphQLDataFetchers.GraphQLEntrance { @Override - public DataFetcher code() { + public DataFetcher publicCode() { return environment -> { Entrance entrance = environment.getSource(); return entrance.getCode(); 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 d9c9ceb67e8..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 @@ -394,12 +394,12 @@ public interface GraphQLEmissions { /** Station entrance or exit, originating from OSM or GTFS data. */ public interface GraphQLEntrance { - public DataFetcher code(); - public DataFetcher entranceId(); public DataFetcher name(); + public DataFetcher publicCode(); + public DataFetcher wheelchairAccessible(); } 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/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index c9d0f3d73af..ce808e546d1 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -493,12 +493,12 @@ type Emissions { "Station entrance or exit, originating from OSM or GTFS data." type Entrance { - "Short text or a number that identifies the entrance or exit for passengers. For example, `A` or `B`." - code: String "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 } @@ -3557,14 +3557,14 @@ enum RelativeDirection { """ 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 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 bfd2e1cc224..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; @@ -139,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(); 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 0e089aac428..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 @@ -28,7 +28,7 @@ "absoluteDirection": null, "feature": { "__typename": "Entrance", - "code": "A", + "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 565e620fed3..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 @@ -23,7 +23,7 @@ feature { __typename ... on Entrance { - code + publicCode entranceId wheelchairAccessible } From 8771d5c94d8decc4aeeed5d648e5b626d994b1b8 Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Wed, 8 Jan 2025 17:16:19 +0000 Subject: [PATCH 071/106] apply review suggestion Co-authored-by: Leonard Ehrenfried --- .../service/osminfo/OsmInfoGraphBuildRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildRepository.java b/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildRepository.java index e30e0dd19a7..ac8f7276072 100644 --- a/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildRepository.java +++ b/application/src/main/java/org/opentripplanner/service/osminfo/OsmInfoGraphBuildRepository.java @@ -6,7 +6,7 @@ import org.opentripplanner.street.model.edge.Edge; /** - * Store OSM data used during graph build, but after the OSM Graph Builder is done. + * Store OSM data used during graph build, but discard it after it is complete. *

* This is a repository to support the {@link OsmInfoGraphBuildService}. */ From c1749286c44c5cd7f4eee82bda34ad6565c69f9e Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Wed, 8 Jan 2025 17:22:45 +0000 Subject: [PATCH 072/106] add Javadoc --- .../org/opentripplanner/routing/linking/VertexLinker.java | 5 +++++ 1 file changed, 5 insertions(+) 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 a433f3882e1..2f09d618ffd 100644 --- a/application/src/main/java/org/opentripplanner/routing/linking/VertexLinker.java +++ b/application/src/main/java/org/opentripplanner/routing/linking/VertexLinker.java @@ -227,6 +227,11 @@ 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, From 1bfc8332c5a71a2ba390c03e5f75c1c287f506ac Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 8 Jan 2025 17:52:37 +0100 Subject: [PATCH 073/106] Fix ScheduledTransitAlertBuilder --- .../model/plan/ScheduledTransitLeg.java | 1 + .../plan/ScheduledTransitLegBuilder.java | 9 ++++++ .../plan/ScheduledTransitLegBuilderTest.java | 30 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/application/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java b/application/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java index 55e4bccfdec..ab912eae85b 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java +++ b/application/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java @@ -98,6 +98,7 @@ protected ScheduledTransitLeg(ScheduledTransitLegBuilder builder) { getDistanceFromCoordinates( List.of(transitLegCoordinates.getFirst(), transitLegCoordinates.getLast()) ); + this.transitAlerts.addAll(builder.alerts()); } public ZoneId getZoneId() { diff --git a/application/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilder.java b/application/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilder.java index 3e9b5540b1c..e132137e982 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilder.java +++ b/application/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilder.java @@ -3,7 +3,10 @@ import java.time.LocalDate; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.util.HashSet; +import java.util.Set; import org.opentripplanner.model.transfer.ConstrainedTransfer; +import org.opentripplanner.routing.alertpatch.TransitAlert; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; @@ -23,6 +26,7 @@ public class ScheduledTransitLegBuilder> private ConstrainedTransfer transferToNextLeg; private int generalizedCost; private Float accessibilityScore; + private Set alerts = new HashSet<>(); public ScheduledTransitLegBuilder() {} @@ -40,6 +44,7 @@ public ScheduledTransitLegBuilder(ScheduledTransitLeg original) { generalizedCost = original.getGeneralizedCost(); accessibilityScore = original.accessibilityScore(); zoneId = original.getZoneId(); + alerts = original.getTransitAlerts(); } public B withTripTimes(TripTimes tripTimes) { @@ -159,6 +164,10 @@ public Float accessibilityScore() { return accessibilityScore; } + public Set alerts() { + return alerts; + } + public ScheduledTransitLeg build() { return new ScheduledTransitLeg(this); } diff --git a/application/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilderTest.java b/application/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilderTest.java index 8d3d6ed7c1e..83871257ce2 100644 --- a/application/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilderTest.java +++ b/application/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilderTest.java @@ -1,15 +1,25 @@ package org.opentripplanner.model.plan; +import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id; import java.time.LocalDate; +import java.util.Set; import org.junit.jupiter.api.Test; import org.opentripplanner._support.time.ZoneIds; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.routing.alertpatch.TransitAlert; import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; import org.opentripplanner.transit.model.basic.TransitMode; class ScheduledTransitLegBuilderTest { + private static final TransitAlert ALERT = TransitAlert + .of(id("alert")) + .withDescriptionText(I18NString.of("alert")) + .build(); + @Test void transferZoneId() { var pattern = TimetableRepositoryForTest.of().pattern(TransitMode.BUS).build(); @@ -28,4 +38,24 @@ void transferZoneId() { assertEquals(ZoneIds.BERLIN, withScore.getZoneId()); } + + @Test + void alerts() { + var pattern = TimetableRepositoryForTest.of().pattern(TransitMode.BUS).build(); + var leg = new ScheduledTransitLegBuilder<>() + .withZoneId(ZoneIds.BERLIN) + .withServiceDate(LocalDate.of(2023, 11, 15)) + .withTripPattern(pattern) + .withBoardStopIndexInPattern(0) + .withAlightStopIndexInPattern(1) + .build(); + + leg.addAlert(ALERT); + + var newLeg = new ScheduledTransitLegBuilder<>(leg); + + var withScore = newLeg.withAccessibilityScore(4f).build(); + + assertEquals(Set.of(ALERT), withScore.getTransitAlerts()); + } } From 7163f6a9154ede5f384c91ceec450b403f82d4e6 Mon Sep 17 00:00:00 2001 From: OTP Changelog Bot Date: Thu, 9 Jan 2025 15:21:03 +0000 Subject: [PATCH 074/106] Add changelog entry for #6262 [ci skip] --- doc/user/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/user/Changelog.md b/doc/user/Changelog.md index b859f87f19a..14a9f6d83ed 100644 --- a/doc/user/Changelog.md +++ b/doc/user/Changelog.md @@ -70,6 +70,7 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Add fallback name for corridors [#6303](https://github.com/opentripplanner/OpenTripPlanner/pull/6303) - Implement SIRI Lite [#6284](https://github.com/opentripplanner/OpenTripPlanner/pull/6284) - 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) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.6.0 (2024-09-18) From 98d8fd82cf282d434efadd795dc347d1e865ea12 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 9 Jan 2025 16:33:31 +0100 Subject: [PATCH 075/106] Remove unused import --- .../model/plan/ScheduledTransitLegBuilderTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/application/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilderTest.java b/application/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilderTest.java index 83871257ce2..c8ae617fd20 100644 --- a/application/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilderTest.java +++ b/application/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilderTest.java @@ -1,6 +1,5 @@ package org.opentripplanner.model.plan; -import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id; From f53245fdef475829f5a14cca1795fcce3287cb1a Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 9 Jan 2025 16:35:49 +0100 Subject: [PATCH 076/106] Fix formatting --- .../updater/alert/GtfsRealtimeAlertsUpdater.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java b/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java index 604a721c57d..8deddd35b61 100644 --- a/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java @@ -62,7 +62,7 @@ protected void runPolling() throws InterruptedException, ExecutionException { final FeedMessage feed = otpHttpClient.getAndMap( URI.create(url), this.headers.asMap(), - FeedMessage::parseFrom + FeedMessage::parseFrom ); long feedTimestamp = feed.getHeader().getTimestamp(); From 81d6c9b54b2f1d8063aa687b7ee346be0af742bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:43:59 +0000 Subject: [PATCH 077/106] fix(deps): update jersey monorepo to v3.1.10 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7eb8dfffde8..eb494243d23 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ 2.53 2.18.2 4.0.5 - 3.1.9 + 3.1.10 5.11.4 1.14.1 5.6.0 From ffa99abb60851136751d42520989e86d097917fb Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 9 Jan 2025 16:52:50 +0100 Subject: [PATCH 078/106] Automerge patch version of jersey [ci skip] --- renovate.json5 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/renovate.json5 b/renovate.json5 index 557857bf54c..1394c6484b9 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -142,6 +142,15 @@ "automerge": true, "schedule": "on the 4th day of the month" }, + { + "groupName": "Low-risk dependencies (patch)", + "matchUpdateTypes": ["patch"], + "schedule": ["on the 27th day of the month"], + "reviewers": ["testower"], + "matchPackageNames": [ + "org.glassfish.jersey.{/,}**", + ] + }, { "description": "give some projects time to publish a changelog before opening the PR", "matchPackageNames": [ From edc3a7f4f920732fa3d499cb6784871291c952da Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 9 Jan 2025 17:05:28 +0100 Subject: [PATCH 079/106] Automerge protobuf only once a month [ci skip] --- renovate.json5 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/renovate.json5 b/renovate.json5 index 1394c6484b9..50134776150 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -101,7 +101,8 @@ "com.google.cloud:libraries-bom", "com.google.guava:guava", "io.micrometer:micrometer-registry-prometheus", - "io.micrometer:micrometer-registry-influx" + "io.micrometer:micrometer-registry-influx", + "com.google.protobuf:protobuf-java" ], "schedule": "on the 7th through 8th day of the month" }, From c5d1183e2a0412b48fadbf0a1b0bb7075b347465 Mon Sep 17 00:00:00 2001 From: Samuel Cedarbaum Date: Thu, 9 Jan 2025 19:46:09 -0500 Subject: [PATCH 080/106] Move TestDataFetcherDecorator to correct package path --- .../transmodel}/_support/TestDataFetcherDecorator.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename application/src/test/java/org/opentripplanner/{ext/transmodelapi => apis/transmodel}/_support/TestDataFetcherDecorator.java (100%) diff --git a/application/src/test/java/org/opentripplanner/ext/transmodelapi/_support/TestDataFetcherDecorator.java b/application/src/test/java/org/opentripplanner/apis/transmodel/_support/TestDataFetcherDecorator.java similarity index 100% rename from application/src/test/java/org/opentripplanner/ext/transmodelapi/_support/TestDataFetcherDecorator.java rename to application/src/test/java/org/opentripplanner/apis/transmodel/_support/TestDataFetcherDecorator.java From 872dbb36ee4f6fa7e1b076df7f88a1d67235f43c Mon Sep 17 00:00:00 2001 From: Samuel Cedarbaum Date: Thu, 9 Jan 2025 19:46:46 -0500 Subject: [PATCH 081/106] Add explicit cast when calling createFromDocumentedEnum --- .../org/opentripplanner/apis/transmodel/model/EnumTypes.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java b/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java index 2f8e69cc593..aab06381100 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java @@ -221,7 +221,7 @@ public class EnumTypes { public static final GraphQLEnumType OCCUPANCY_STATUS = createFromDocumentedEnum( "OccupancyStatus", - List.of( + List.>of( map("noData", OccupancyStatus.NO_DATA_AVAILABLE), map("empty", OccupancyStatus.EMPTY), map("manySeatsAvailable", OccupancyStatus.MANY_SEATS_AVAILABLE), From e23144bf9b611019f95cb1b4b2fa1f26df906b34 Mon Sep 17 00:00:00 2001 From: Samuel Cedarbaum Date: Thu, 9 Jan 2025 19:56:28 -0500 Subject: [PATCH 082/106] Add type annotation to OccupancyStatus instead of explicit cast --- .../org/opentripplanner/apis/transmodel/model/EnumTypes.java | 2 +- .../transit/model/timetable/OccupancyStatus.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java b/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java index aab06381100..2f8e69cc593 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java @@ -221,7 +221,7 @@ public class EnumTypes { public static final GraphQLEnumType OCCUPANCY_STATUS = createFromDocumentedEnum( "OccupancyStatus", - List.>of( + List.of( map("noData", OccupancyStatus.NO_DATA_AVAILABLE), map("empty", OccupancyStatus.EMPTY), map("manySeatsAvailable", OccupancyStatus.MANY_SEATS_AVAILABLE), diff --git a/application/src/main/java/org/opentripplanner/transit/model/timetable/OccupancyStatus.java b/application/src/main/java/org/opentripplanner/transit/model/timetable/OccupancyStatus.java index 9c32ca39394..fe760747ef4 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/timetable/OccupancyStatus.java +++ b/application/src/main/java/org/opentripplanner/transit/model/timetable/OccupancyStatus.java @@ -9,7 +9,7 @@ *

* Descriptions are copied from the GTFS-RT specification with additions of SIRI nordic profile documentation. */ -public enum OccupancyStatus implements DocumentedEnum { +public enum OccupancyStatus implements DocumentedEnum { NO_DATA_AVAILABLE, EMPTY, MANY_SEATS_AVAILABLE, From 6a540d18e0d720a55c32b64c0575caf44016d7f1 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Fri, 10 Jan 2025 09:15:30 +0100 Subject: [PATCH 083/106] Remove testower from reviewers of jersey [ci skip] --- renovate.json5 | 1 - 1 file changed, 1 deletion(-) diff --git a/renovate.json5 b/renovate.json5 index 50134776150..100a91d959a 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -147,7 +147,6 @@ "groupName": "Low-risk dependencies (patch)", "matchUpdateTypes": ["patch"], "schedule": ["on the 27th day of the month"], - "reviewers": ["testower"], "matchPackageNames": [ "org.glassfish.jersey.{/,}**", ] From 1eb0556da113a86f560b3bdccdf8f9e8dc47ba8d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:53:23 +0000 Subject: [PATCH 084/106] chore(deps): update dependency jsdom to v26 --- client/package-lock.json | 183 ++++++++++++++++++++++++++++++++++----- client/package.json | 2 +- 2 files changed, 164 insertions(+), 21 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index a50d6d07f73..258494649cc 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -39,7 +39,7 @@ "eslint-plugin-react-hooks": "5.1.0", "eslint-plugin-react-refresh": "0.4.16", "globals": "15.14.0", - "jsdom": "25.0.1", + "jsdom": "26.0.0", "prettier": "3.4.2", "typescript": "5.7.2", "typescript-eslint": "8.19.0", @@ -207,6 +207,30 @@ "node": ">=14" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.2.tgz", + "integrity": "sha512-RtWv9jFN2/bLExuZgFFZ0I3pWWeezAHGgrmjqGGWclATl1aDe3yhCUaI0Ilkp6OCk9zX7+FjvDasEX8Q9Rxc5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.1", + "@csstools/css-color-parser": "^3.0.7", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^11.0.2" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -1007,6 +1031,121 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.1.tgz", + "integrity": "sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.1.tgz", + "integrity": "sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.7.tgz", + "integrity": "sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.1", + "@csstools/css-calc": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@envelop/core": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.0.2.tgz", @@ -5178,12 +5317,14 @@ } }, "node_modules/cssstyle": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", - "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz", + "integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==", "dev": true, + "license": "MIT", "dependencies": { - "rrweb-cssom": "^0.7.1" + "@asamuzakjp/css-color": "^2.8.2", + "rrweb-cssom": "^0.8.0" }, "engines": { "node": ">=18" @@ -7761,22 +7902,23 @@ "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==" }, "node_modules/jsdom": { - "version": "25.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", - "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.0.0.tgz", + "integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==", "dev": true, + "license": "MIT", "dependencies": { - "cssstyle": "^4.1.0", + "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", - "form-data": "^4.0.0", + "form-data": "^4.0.1", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.5", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.12", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.7.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.0.0", @@ -7784,7 +7926,7 @@ "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0", + "whatwg-url": "^14.1.0", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, @@ -7792,7 +7934,7 @@ "node": ">=18" }, "peerDependencies": { - "canvas": "^2.11.2" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -9455,10 +9597,11 @@ } }, "node_modules/rrweb-cssom": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", - "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", - "dev": true + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" }, "node_modules/run-async": { "version": "2.4.1", diff --git a/client/package.json b/client/package.json index b16a1fe8527..d81511d93c9 100644 --- a/client/package.json +++ b/client/package.json @@ -48,7 +48,7 @@ "eslint-plugin-react-hooks": "5.1.0", "eslint-plugin-react-refresh": "0.4.16", "globals": "15.14.0", - "jsdom": "25.0.1", + "jsdom": "26.0.0", "prettier": "3.4.2", "typescript": "5.7.2", "typescript-eslint": "8.19.0", From 4248ffe1644ae50d1970d26310b5593acf7fe508 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Fri, 10 Jan 2025 15:38:12 +0100 Subject: [PATCH 085/106] Fix mapping in Transmodel API --- .../mapping/RelativeDirectionMapper.java | 33 +++++++++++++++++++ .../model/plan/PathGuidanceType.java | 5 ++- .../apis/transmodel/model/EnumTypesTest.java | 21 ++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 application/src/main/java/org/opentripplanner/apis/transmodel/mapping/RelativeDirectionMapper.java 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 86c8359c026..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( 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); From 77e29087dea17be0312171364cd1cd732c17e607 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 22:14:36 +0000 Subject: [PATCH 086/106] fix(deps): update debug ui dependencies (non-major) --- client/package-lock.json | 217 ++++++++++++++++++++++----------------- client/package.json | 16 +-- 2 files changed, 129 insertions(+), 104 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 258494649cc..bb145274bd4 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -17,21 +17,21 @@ "react": "19.0.0", "react-bootstrap": "2.10.7", "react-dom": "19.0.0", - "react-map-gl": "7.1.7" + "react-map-gl": "7.1.8" }, "devDependencies": { - "@eslint/compat": "1.2.4", - "@eslint/js": "9.17.0", + "@eslint/compat": "1.2.5", + "@eslint/js": "9.18.0", "@graphql-codegen/cli": "5.0.3", "@graphql-codegen/client-preset": "4.5.1", "@graphql-codegen/introspection": "4.0.3", "@parcel/watcher": "2.5.0", "@testing-library/react": "16.1.0", - "@types/react": "19.0.1", + "@types/react": "19.0.4", "@types/react-dom": "19.0.2", "@vitejs/plugin-react": "4.3.4", "@vitest/coverage-v8": "2.1.8", - "eslint": "9.17.0", + "eslint": "9.18.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.31.0", "eslint-plugin-jsx-a11y": "6.10.2", @@ -41,9 +41,9 @@ "globals": "15.14.0", "jsdom": "26.0.0", "prettier": "3.4.2", - "typescript": "5.7.2", - "typescript-eslint": "8.19.0", - "vite": "6.0.3", + "typescript": "5.7.3", + "typescript-eslint": "8.19.1", + "vite": "6.0.7", "vitest": "2.1.8" } }, @@ -1611,10 +1611,11 @@ } }, "node_modules/@eslint/compat": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.4.tgz", - "integrity": "sha512-S8ZdQj/N69YAtuqFt7653jwcvuUj131+6qGLUyDqfDg1OIoBQ66OCuXC473YQfO2AaxITTutiRQiDwoo7ZLYyg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.5.tgz", + "integrity": "sha512-5iuG/StT+7OfvhoBHPlmxkPA9om6aDUFgmD4+mWKAGsYt4vCe8rypneG03AuseyRHBmcCLXQtIH5S26tIoggLg==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1642,10 +1643,11 @@ } }, "node_modules/@eslint/core": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", - "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -1689,10 +1691,11 @@ } }, "node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", + "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -1707,11 +1710,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", - "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", "dev": true, + "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.10.0", "levn": "^0.4.1" }, "engines": { @@ -3875,9 +3880,10 @@ "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==" }, "node_modules/@types/react": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.1.tgz", - "integrity": "sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ==", + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.4.tgz", + "integrity": "sha512-3O4QisJDYr1uTUMZHA2YswiQZRq+Pd8D+GdVFYikTutYsTz+QZgWkAPnP7rx9txoI6EXKcPiluMqWPFV3tT9Wg==", + "license": "MIT", "dependencies": { "csstype": "^3.0.2" } @@ -3922,20 +3928,21 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.0.tgz", - "integrity": "sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz", + "integrity": "sha512-tJzcVyvvb9h/PB96g30MpxACd9IrunT7GF9wfA9/0TJ1LxGOJx1TdPzSbBBnNED7K9Ka8ybJsnEpiXPktolTLg==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/type-utils": "8.19.0", - "@typescript-eslint/utils": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/type-utils": "8.19.1", + "@typescript-eslint/utils": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3951,15 +3958,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.0.tgz", - "integrity": "sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.1.tgz", + "integrity": "sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/typescript-estree": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", "debug": "^4.3.4" }, "engines": { @@ -3975,13 +3983,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", - "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz", + "integrity": "sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0" + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3992,15 +4001,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.0.tgz", - "integrity": "sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.1.tgz", + "integrity": "sha512-Rp7k9lhDKBMRJB/nM9Ksp1zs4796wVNyihG9/TU9R6KCJDNkQbc2EOKjrBtLYh3396ZdpXLtr/MkaSEmNMtykw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/utils": "8.19.0", + "@typescript-eslint/typescript-estree": "8.19.1", + "@typescript-eslint/utils": "8.19.1", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4015,10 +4025,11 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", - "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.1.tgz", + "integrity": "sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -4028,19 +4039,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz", - "integrity": "sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz", + "integrity": "sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4058,6 +4070,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -4067,6 +4080,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -4082,6 +4096,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4090,15 +4105,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.0.tgz", - "integrity": "sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.1.tgz", + "integrity": "sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/typescript-estree": "8.19.0" + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/typescript-estree": "8.19.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4113,12 +4129,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.0.tgz", - "integrity": "sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz", + "integrity": "sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/types": "8.19.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -5915,18 +5932,19 @@ } }, "node_modules/eslint": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", - "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", + "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", + "@eslint/core": "^0.10.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.17.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/js": "9.18.0", + "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", @@ -9317,9 +9335,10 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "node_modules/react-map-gl": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-7.1.7.tgz", - "integrity": "sha512-mwjc0obkBJOXCcoXQr3VoLqmqwo9vS4bXfbGsdxXzEgVCv/PM0v+1QggL7W0d/ccIy+VCjbXNlGij+PENz6VNg==", + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-7.1.8.tgz", + "integrity": "sha512-zwF16XMOdOKH4py5ehS1bgQIChqW8UN3b1bXps+JnADbYLSbOoUPQ3tNw0EZ2OTBWArR5aaQlhlEqg4lE47T8A==", + "license": "MIT", "dependencies": { "@maplibre/maplibre-gl-style-spec": "^19.2.1", "@types/mapbox-gl": ">=1.0.0" @@ -9343,6 +9362,7 @@ "version": "19.3.3", "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.3.tgz", "integrity": "sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==", + "license": "ISC", "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", @@ -9360,7 +9380,8 @@ "node_modules/react-map-gl/node_modules/json-stringify-pretty-compact": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz", - "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==" + "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==", + "license": "MIT" }, "node_modules/react-refresh": { "version": "0.14.2", @@ -10529,15 +10550,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-log": { @@ -10674,10 +10696,11 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10687,14 +10710,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.19.0.tgz", - "integrity": "sha512-Ni8sUkVWYK4KAcTtPjQ/UTiRk6jcsuDhPpxULapUDi8A/l8TSBk+t1GtJA1RsCzIJg0q6+J7bf35AwQigENWRQ==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.19.1.tgz", + "integrity": "sha512-LKPUQpdEMVOeKluHi8md7rwLcoXHhwvWp3x+sJkMuq3gGm9yaYJtPo8sRZSblMFJ5pcOGCAak/scKf1mvZDlQw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.19.0", - "@typescript-eslint/parser": "8.19.0", - "@typescript-eslint/utils": "8.19.0" + "@typescript-eslint/eslint-plugin": "8.19.1", + "@typescript-eslint/parser": "8.19.1", + "@typescript-eslint/utils": "8.19.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10899,12 +10923,13 @@ } }, "node_modules/vite": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.3.tgz", - "integrity": "sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==", + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", + "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "^0.24.0", + "esbuild": "^0.24.2", "postcss": "^8.4.49", "rollup": "^4.23.0" }, diff --git a/client/package.json b/client/package.json index d81511d93c9..78180de6b6a 100644 --- a/client/package.json +++ b/client/package.json @@ -26,21 +26,21 @@ "react": "19.0.0", "react-bootstrap": "2.10.7", "react-dom": "19.0.0", - "react-map-gl": "7.1.7" + "react-map-gl": "7.1.8" }, "devDependencies": { - "@eslint/compat": "1.2.4", - "@eslint/js": "9.17.0", + "@eslint/compat": "1.2.5", + "@eslint/js": "9.18.0", "@graphql-codegen/cli": "5.0.3", "@graphql-codegen/client-preset": "4.5.1", "@graphql-codegen/introspection": "4.0.3", "@parcel/watcher": "2.5.0", "@testing-library/react": "16.1.0", - "@types/react": "19.0.1", + "@types/react": "19.0.4", "@types/react-dom": "19.0.2", "@vitejs/plugin-react": "4.3.4", "@vitest/coverage-v8": "2.1.8", - "eslint": "9.17.0", + "eslint": "9.18.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.31.0", "eslint-plugin-jsx-a11y": "6.10.2", @@ -50,9 +50,9 @@ "globals": "15.14.0", "jsdom": "26.0.0", "prettier": "3.4.2", - "typescript": "5.7.2", - "typescript-eslint": "8.19.0", - "vite": "6.0.3", + "typescript": "5.7.3", + "typescript-eslint": "8.19.1", + "vite": "6.0.7", "vitest": "2.1.8" } } From 71bb5fa9583f837c06899f5a378dba0c05cda38e Mon Sep 17 00:00:00 2001 From: OTP Bot Date: Sat, 11 Jan 2025 19:24:18 +0000 Subject: [PATCH 087/106] Upgrade debug client to version 2025/01/2025-01-11T19:23 --- application/src/client/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/client/index.html b/application/src/client/index.html index db412af9df5..d3dd8544a53 100644 --- a/application/src/client/index.html +++ b/application/src/client/index.html @@ -5,8 +5,8 @@ OTP Debug - - + +

From 3fcd54f98c69c89475fcc9569e1dd40fe1c0c5ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:18:25 +0000 Subject: [PATCH 088/106] fix(deps): update google.dagger.version to v2.54 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index eb494243d23..f77d753472d 100644 --- a/pom.xml +++ b/pom.xml @@ -62,7 +62,7 @@ 32.1 - 2.53 + 2.54 2.18.2 4.0.5 3.1.10 From 521c87aeb3f2781d5358bb0d90e3e089ff419985 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Fri, 10 Jan 2025 18:17:43 +0100 Subject: [PATCH 089/106] Update GraphiQL, add headers to UI --- application/src/client/graphiql/index.html | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/application/src/client/graphiql/index.html b/application/src/client/graphiql/index.html index 0e8d4afd1ea..e3a4a1a0f8f 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...
- + +