From a829afb71707bd92f4aead30d2dbb22c24077c0e Mon Sep 17 00:00:00 2001 From: Samuel Cedarbaum Date: Sat, 14 Sep 2024 03:47:37 -0400 Subject: [PATCH] Expose station entrances in GraphQL API --- .../apis/gtfs/datafetchers/QueryTypeImpl.java | 31 +++++ .../apis/gtfs/datafetchers/StopImpl.java | 124 ++++++++++++++---- .../gtfs/generated/GraphQLDataFetchers.java | 13 ++ .../apis/gtfs/generated/GraphQLTypes.java | 30 +++++ .../transit/model/site/Entrance.java | 4 + .../transit/model/site/Station.java | 11 ++ .../service/DefaultTransitService.java | 12 ++ .../transit/service/StopModel.java | 25 ++++ .../transit/service/StopModelBuilder.java | 12 ++ .../transit/service/TransitService.java | 5 + .../opentripplanner/apis/gtfs/schema.graphqls | 9 ++ .../apis/gtfs/GraphQLIntegrationTest.java | 14 +- .../model/_data/TransitModelForTest.java | 15 +++ .../transit/model/site/EntranceTest.java | 6 + .../transit/model/site/StationTest.java | 2 - .../transit/service/StopModelTest.java | 25 +++- .../apis/gtfs/expectations/entrances-ids.json | 14 ++ .../gtfs/expectations/entrances-name.json | 14 ++ .../apis/gtfs/expectations/entrances.json | 22 ++++ .../apis/gtfs/expectations/stations.json | 40 ++++++ .../apis/gtfs/expectations/stops.json | 9 +- .../apis/gtfs/queries/entrances-ids.graphql | 10 ++ .../apis/gtfs/queries/entrances-name.graphql | 10 ++ .../apis/gtfs/queries/entrances.graphql | 10 ++ .../apis/gtfs/queries/stations.graphql | 24 ++++ 25 files changed, 460 insertions(+), 31 deletions(-) create mode 100644 src/test/resources/org/opentripplanner/apis/gtfs/expectations/entrances-ids.json create mode 100644 src/test/resources/org/opentripplanner/apis/gtfs/expectations/entrances-name.json create mode 100644 src/test/resources/org/opentripplanner/apis/gtfs/expectations/entrances.json create mode 100644 src/test/resources/org/opentripplanner/apis/gtfs/expectations/stations.json create mode 100644 src/test/resources/org/opentripplanner/apis/gtfs/queries/entrances-ids.graphql create mode 100644 src/test/resources/org/opentripplanner/apis/gtfs/queries/entrances-name.graphql create mode 100644 src/test/resources/org/opentripplanner/apis/gtfs/queries/entrances.graphql create mode 100644 src/test/resources/org/opentripplanner/apis/gtfs/queries/stations.graphql diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java index 0e70c13074b..fe19e979b64 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java @@ -65,6 +65,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.Station; import org.opentripplanner.transit.model.site.StopLocation; @@ -668,6 +669,36 @@ public DataFetcher> stations() { }; } + @Override + public DataFetcher> entrances() { + return environment -> { + var args = new GraphQLTypes.GraphQLQueryTypeEntrancesArgs(environment.getArguments()); + + TransitService transitService = getTransitService(environment); + + if (args.getGraphQLIds() != null) { + return args + .getGraphQLIds() + .stream() + .map(FeedScopedId::parse) + .map(transitService::getEntranceById) + .collect(Collectors.toList()); + } + + Stream entranceStream = transitService.getEntrances().stream(); + + if (args.getGraphQLName() != null) { + String name = args.getGraphQLName().toLowerCase(environment.getLocale()); + entranceStream = + entranceStream.filter(entrance -> + GraphQLUtils.startsWith(entrance.getName(), name, environment.getLocale()) + ); + } + + return entranceStream.collect(Collectors.toList()); + }; + } + @Override public DataFetcher stop() { return environment -> diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java index 0730d2fbc91..d263d776909 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java @@ -31,6 +31,7 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.Entrance; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.site.StopLocation; @@ -44,7 +45,12 @@ public DataFetcher> alerts() { TransitAlertService alertService = getTransitService(environment).getTransitAlertService(); var args = new GraphQLTypes.GraphQLStopAlertsArgs(environment.getArguments()); List types = args.getGraphQLTypes(); - FeedScopedId id = getValue(environment, StopLocation::getId, AbstractTransitEntity::getId); + FeedScopedId id = getValue( + environment, + StopLocation::getId, + AbstractTransitEntity::getId, + Entrance::getId + ); if (types != null) { Collection alerts = new ArrayList<>(); if (types.contains(GraphQLTypes.GraphQLStopAlertType.STOP)) { @@ -127,7 +133,8 @@ public DataFetcher cluster() { @Override public DataFetcher code() { - return environment -> getValue(environment, StopLocation::getCode, Station::getCode); + return environment -> + getValue(environment, StopLocation::getCode, Station::getCode, Entrance::getCode); } @Override @@ -144,6 +151,11 @@ public DataFetcher desc() { org.opentripplanner.framework.graphql.GraphQLUtils.getTranslation( station.getDescription(), environment + ), + entrance -> + org.opentripplanner.framework.graphql.GraphQLUtils.getTranslation( + entrance.getDescription(), + environment ) ); } @@ -162,18 +174,27 @@ public DataFetcher url() { org.opentripplanner.framework.graphql.GraphQLUtils.getTranslation( station.getUrl(), environment - ) + ), + // TODO: Entrances could theoretically support URLs, but not currently in model. + entrance -> null ); } @Override public DataFetcher locationType() { - return environment -> getValue(environment, stop -> "STOP", station -> "STATION"); + return environment -> + getValue(environment, stop -> "STOP", station -> "STATION", entrance -> "ENTRANCE"); } @Override public DataFetcher parentStation() { - return environment -> getValue(environment, StopLocation::getParentStation, station -> null); + return environment -> + getValue( + environment, + StopLocation::getParentStation, + station -> null, + Entrance::getParentStation + ); } // TODO @@ -184,13 +205,19 @@ public DataFetcher direction() { @Override public DataFetcher geometries() { - return environment -> getValue(environment, StopLocation::getGeometry, Station::getGeometry); + return environment -> + getValue(environment, StopLocation::getGeometry, Station::getGeometry, null); } @Override public DataFetcher gtfsId() { return environment -> - getValue(environment, stop -> stop.getId().toString(), station -> station.getId().toString()); + getValue( + environment, + stop -> stop.getId().toString(), + station -> station.getId().toString(), + entrance -> entrance.getId().toString() + ); } @Override @@ -199,18 +226,31 @@ public DataFetcher id() { getValue( environment, stop -> new Relay.ResolvedGlobalId("Stop", stop.getId().toString()), - station -> new Relay.ResolvedGlobalId("Stop", station.getId().toString()) + station -> new Relay.ResolvedGlobalId("Stop", station.getId().toString()), + entrance -> new Relay.ResolvedGlobalId("Stop", entrance.getId().toString()) ); } @Override public DataFetcher lat() { - return environment -> getValue(environment, StopLocation::getLat, Station::getLat); + return environment -> + getValue( + environment, + StopLocation::getLat, + Station::getLat, + entrance -> entrance.getCoordinate().latitude() + ); } @Override public DataFetcher lon() { - return environment -> getValue(environment, StopLocation::getLon, Station::getLon); + return environment -> + getValue( + environment, + StopLocation::getLon, + Station::getLon, + entrance -> entrance.getCoordinate().longitude() + ); } @Override @@ -227,6 +267,11 @@ public DataFetcher name() { org.opentripplanner.framework.graphql.GraphQLUtils.getTranslation( station.getName(), environment + ), + entrance -> + org.opentripplanner.framework.graphql.GraphQLUtils.getTranslation( + entrance.getName(), + environment ) ); } @@ -238,7 +283,8 @@ public DataFetcher> patterns() { @Override public DataFetcher platformCode() { - return environment -> getValue(environment, StopLocation::getPlatformCode, station -> null); + return environment -> + getValue(environment, StopLocation::getPlatformCode, station -> null, entrance -> null); } @Override @@ -283,7 +329,8 @@ public DataFetcher> stopTimesForPattern() { !args.getGraphQLOmitCanceled() ); }, - station -> null + station -> null, + entrance -> null ); } @@ -293,7 +340,24 @@ public DataFetcher> stops() { getValue( environment, stop -> null, - station -> new ArrayList(station.getChildStops()) + station -> new ArrayList(station.getChildStops()), + entrance -> null + ); + } + + @Override + public DataFetcher> entrances() { + return environment -> + getValue( + environment, + stop -> null, + station -> + station + .getEntrances() + .stream() + .sorted((e1, e2) -> e1.getId().compareTo(e2.getId())) + .collect(Collectors.toList()), + entrances -> null ); } @@ -322,7 +386,8 @@ public DataFetcher> stoptimesForPatterns() { .stream() .map(stopTFunction) .flatMap(Collection::stream) - .collect(Collectors.toList()) + .collect(Collectors.toList()), + entrance -> null ); }; } @@ -358,7 +423,8 @@ public DataFetcher> stoptimesForServiceDate() { .stream() .map(stopTFunction) .flatMap(Collection::stream) - .collect(Collectors.toList()) + .collect(Collectors.toList()), + entrance -> null ); }; } @@ -384,7 +450,8 @@ public DataFetcher> stoptimesWithoutPatterns() { Stream stream = getValue( environment, stopTFunction, - station -> station.getChildStops().stream().flatMap(stopTFunction) + station -> station.getChildStops().stream().flatMap(stopTFunction), + entrance -> Stream.of() ); return stream @@ -401,7 +468,8 @@ public DataFetcher timezone() { getValue( environment, stop -> stop.getTimeZone().toString(), - station -> station.getTimezone().toString() + station -> station.getTimezone().toString(), + entrance -> null ); } @@ -426,7 +494,8 @@ public DataFetcher> transfers() { ) .collect(Collectors.toList()); }, - station -> null + station -> null, + entrance -> null ); } @@ -449,7 +518,8 @@ public DataFetcher vehicleMode() { .stream() .findFirst() .map(Enum::toString) - .orElse(null) + .orElse(null), + entrance -> null ); }; } @@ -466,7 +536,8 @@ public DataFetcher wheelchairBoarding() var boarding = getValue( environment, StopLocation::getWheelchairAccessibility, - station -> null + station -> null, + Entrance::getWheelchairAccessibility ); return GraphQLUtils.toGraphQL(boarding); }; @@ -475,14 +546,15 @@ public DataFetcher wheelchairBoarding() @Override public DataFetcher zoneId() { return environment -> - getValue(environment, StopLocation::getFirstZoneAsString, station -> null); + getValue(environment, StopLocation::getFirstZoneAsString, station -> null, null); } private Collection getPatterns(DataFetchingEnvironment environment) { return getValue( environment, stop -> getTransitService(environment).getPatternsForStop(stop, true), - station -> null + station -> null, + entrance -> null ); } @@ -490,7 +562,8 @@ private Collection getRoutes(DataFetchingEnvironment environment) { return getValue( environment, stop -> getTransitService(environment).getRoutesForStop(stop), - station -> null + station -> null, + entrance -> null ); } @@ -557,13 +630,16 @@ private Stream getRealtimeAddedPatternsAsStream( private static T getValue( DataFetchingEnvironment environment, Function stopTFunction, - Function stationTFunction + Function stationTFunction, + Function entranceFunction ) { Object source = environment.getSource(); if (source instanceof StopLocation) { return stopTFunction.apply((StopLocation) source); } else if (source instanceof Station) { return stationTFunction.apply((Station) source); + } else if (source instanceof Entrance) { + return entranceFunction.apply((Entrance) source); } return null; } 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..61e0d5d3f33 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; @@ -775,6 +784,8 @@ public interface GraphQLQueryType { public DataFetcher departureRow(); + public DataFetcher> entrances(); + public DataFetcher> feeds(); public DataFetcher fuzzyTrip(); @@ -984,6 +995,8 @@ public interface GraphQLStop { public DataFetcher direction(); + public DataFetcher> entrances(); + public DataFetcher geometries(); public DataFetcher gtfsId(); 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..93a55c3b815 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; @@ -2383,6 +2384,35 @@ public void setGraphQLId(String id) { } } + public static class GraphQLQueryTypeEntrancesArgs { + + private List ids; + private String name; + + public GraphQLQueryTypeEntrancesArgs(Map args) { + if (args != null) { + this.ids = (List) args.get("ids"); + this.name = (String) args.get("name"); + } + } + + public List getGraphQLIds() { + return this.ids; + } + + public String getGraphQLName() { + return this.name; + } + + public void setGraphQLIds(List ids) { + this.ids = ids; + } + + public void setGraphQLName(String name) { + this.name = name; + } + } + public static class GraphQLQueryTypeFuzzyTripArgs { private String date; diff --git a/src/main/java/org/opentripplanner/transit/model/site/Entrance.java b/src/main/java/org/opentripplanner/transit/model/site/Entrance.java index 717aae9140b..85dd128e6ce 100644 --- a/src/main/java/org/opentripplanner/transit/model/site/Entrance.java +++ b/src/main/java/org/opentripplanner/transit/model/site/Entrance.java @@ -13,6 +13,10 @@ public final class Entrance extends StationElement { super(builder); // Verify coordinate is not null Objects.requireNonNull(getCoordinate()); + + if (isPartOfStation()) { + getParentStation().addEntrance(this); + } } public static EntranceBuilder of(FeedScopedId id) { diff --git a/src/main/java/org/opentripplanner/transit/model/site/Station.java b/src/main/java/org/opentripplanner/transit/model/site/Station.java index 68ce2d6d5a2..9d40f6228d1 100644 --- a/src/main/java/org/opentripplanner/transit/model/site/Station.java +++ b/src/main/java/org/opentripplanner/transit/model/site/Station.java @@ -43,6 +43,8 @@ public class Station @JsonBackReference private final Set childStops = new HashSet<>(); + private final Set entrances = new HashSet<>(); + private GeometryCollection geometry; Station(StationBuilder builder) { @@ -73,6 +75,10 @@ void addChildStop(RegularStop stop) { this.geometry = computeGeometry(coordinate, childStops); } + void addEntrance(Entrance entrance) { + this.entrances.add(entrance); + } + public boolean includes(StopLocation stop) { return childStops.contains(stop); } @@ -87,6 +93,11 @@ public Collection getChildStops() { return childStops; } + @Nonnull + public Collection getEntrances() { + return entrances; + } + @Override public double getLat() { return coordinate.latitude(); diff --git a/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java b/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java index 8fa18443bab..4067db50430 100644 --- a/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java @@ -46,6 +46,7 @@ import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.organization.Operator; import org.opentripplanner.transit.model.site.AreaStop; +import org.opentripplanner.transit.model.site.Entrance; import org.opentripplanner.transit.model.site.GroupStop; import org.opentripplanner.transit.model.site.MultiModalStation; import org.opentripplanner.transit.model.site.RegularStop; @@ -162,6 +163,17 @@ public Collection getStations() { return this.transitModel.getStopModel().listStations(); } + @Override + public Entrance getEntranceById(FeedScopedId id) { + return this.transitModel.getStopModel().getEntranceById(id); + } + + @Override + public Collection getEntrances() { + OTPRequestTimeoutException.checkForTimeout(); + return this.transitModel.getStopModel().listEntrances(); + } + @Override public Integer getServiceCodeForId(FeedScopedId id) { return this.transitModel.getServiceCodes().get(id); diff --git a/src/main/java/org/opentripplanner/transit/service/StopModel.java b/src/main/java/org/opentripplanner/transit/service/StopModel.java index 4f55ca25b5b..16375fa7689 100644 --- a/src/main/java/org/opentripplanner/transit/service/StopModel.java +++ b/src/main/java/org/opentripplanner/transit/service/StopModel.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import javax.annotation.Nullable; import org.locationtech.jts.geom.Envelope; import org.opentripplanner.framework.collection.CollectionsView; @@ -13,6 +14,7 @@ import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.site.AreaStop; +import org.opentripplanner.transit.model.site.Entrance; import org.opentripplanner.transit.model.site.GroupOfStations; import org.opentripplanner.transit.model.site.GroupStop; import org.opentripplanner.transit.model.site.MultiModalStation; @@ -33,6 +35,7 @@ public class StopModel implements Serializable { private final AtomicInteger stopIndexCounter; private final Map regularStopById; private final Map stationById; + private final Map entranceById; private final Map multiModalStationById; private final Map groupOfStationsById; private final Map areaStopById; @@ -44,6 +47,7 @@ public StopModel() { this.stopIndexCounter = new AtomicInteger(0); this.regularStopById = Map.of(); this.stationById = Map.of(); + this.entranceById = Map.of(); this.multiModalStationById = Map.of(); this.groupOfStationsById = Map.of(); this.areaStopById = Map.of(); @@ -55,6 +59,7 @@ public StopModel() { this.stopIndexCounter = builder.stopIndexCounter(); this.regularStopById = builder.regularStopsById().asImmutableMap(); this.stationById = builder.stationById().asImmutableMap(); + this.entranceById = builder.entranceById().asImmutableMap(); this.multiModalStationById = builder.multiModalStationById().asImmutableMap(); this.groupOfStationsById = builder.groupOfStationById().asImmutableMap(); this.areaStopById = builder.areaStopById().asImmutableMap(); @@ -77,6 +82,7 @@ private StopModel(StopModel main, StopModel child) { this.multiModalStationById = MapUtils.combine(main.multiModalStationById, child.multiModalStationById); this.stationById = MapUtils.combine(main.stationById, child.stationById); + this.entranceById = MapUtils.combine(main.entranceById, child.entranceById); reindex(); } @@ -204,6 +210,19 @@ public MultiModalStation getMultiModalStation(FeedScopedId id) { return multiModalStationById.get(id); } + @Nullable + public Entrance getEntranceById(FeedScopedId id) { + return entranceById.get(id); + } + + public Collection listEntrances() { + return entranceById + .values() + .stream() + .sorted((e1, e2) -> e1.getId().compareTo(e2.getId())) + .collect(Collectors.toList()); + } + public Collection listMultiModalStations() { return multiModalStationById.values(); } @@ -259,6 +278,12 @@ public WgsCoordinate getCoordinateById(FeedScopedId id) { if (station != null) { return station.getCoordinate(); } + // Entrance + Entrance entrance = entranceById.get(id); + if (entrance != null) { + return entrance.getCoordinate(); + } + // Single stop (regular transit and flex) StopLocation stop = getStopLocation(id); return stop == null ? null : stop.getCoordinate(); diff --git a/src/main/java/org/opentripplanner/transit/service/StopModelBuilder.java b/src/main/java/org/opentripplanner/transit/service/StopModelBuilder.java index 52ac778c559..76854816e1a 100644 --- a/src/main/java/org/opentripplanner/transit/service/StopModelBuilder.java +++ b/src/main/java/org/opentripplanner/transit/service/StopModelBuilder.java @@ -9,6 +9,7 @@ import org.opentripplanner.transit.model.framework.ImmutableEntityById; import org.opentripplanner.transit.model.site.AreaStop; import org.opentripplanner.transit.model.site.AreaStopBuilder; +import org.opentripplanner.transit.model.site.Entrance; import org.opentripplanner.transit.model.site.GroupOfStations; import org.opentripplanner.transit.model.site.GroupStop; import org.opentripplanner.transit.model.site.GroupStopBuilder; @@ -25,6 +26,7 @@ public class StopModelBuilder { private final EntityById areaStopById = new DefaultEntityById<>(); private final EntityById groupStopById = new DefaultEntityById<>(); private final EntityById stationById = new DefaultEntityById<>(); + private final EntityById entranceById = new DefaultEntityById<>(); private final EntityById multiModalStationById = new DefaultEntityById<>(); private final EntityById groupOfStationById = new DefaultEntityById<>(); @@ -63,6 +65,16 @@ public ImmutableEntityById stationById() { public StopModelBuilder withStation(Station station) { stationById.add(station); + entranceById.addAll(station.getEntrances()); + return this; + } + + public ImmutableEntityById entranceById() { + return entranceById; + } + + public StopModelBuilder withEntrance(Entrance entrance) { + entranceById.add(entrance); return this; } diff --git a/src/main/java/org/opentripplanner/transit/service/TransitService.java b/src/main/java/org/opentripplanner/transit/service/TransitService.java index 1836b5612d2..a3a86e2bf54 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitService.java @@ -35,6 +35,7 @@ import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.organization.Operator; import org.opentripplanner.transit.model.site.AreaStop; +import org.opentripplanner.transit.model.site.Entrance; import org.opentripplanner.transit.model.site.GroupStop; import org.opentripplanner.transit.model.site.MultiModalStation; import org.opentripplanner.transit.model.site.RegularStop; @@ -94,6 +95,10 @@ public interface TransitService { Collection getStations(); + Entrance getEntranceById(FeedScopedId id); + + Collection getEntrances(); + Integer getServiceCodeForId(FeedScopedId id); TIntSet getServiceCodesRunningForDate(LocalDate date); diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 927af19f8b1..86a7eed4f52 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -1144,6 +1144,13 @@ type QueryType { clusters: [Cluster] "Get a single departure row based on its ID (ID format is `FeedId:StopId:PatternId`)" departureRow(id: String!): DepartureRow + "Station entrance/exit points." + entrances( + "Only return entrances that match one of the ids in this list" + ids: [String], + "Query entrances by name" + name: String + ): [Stop] "Get all available feeds" feeds: [Feed] """ @@ -1961,6 +1968,8 @@ type Stop implements Node & PlaceInterface { language: String ): String direction: String + "Returns all entrances of this station (Only applicable for stations)" + entrances: [Stop] """ Representations of this stop's geometry. This is mainly interesting for flex stops which can be a polygon or a group of stops either consisting of either points or polygons. diff --git a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index 79590ca2775..52948971ab5 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -88,7 +88,9 @@ import org.opentripplanner.transit.model.network.BikeAccess; 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.Station; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; import org.opentripplanner.transit.model.timetable.TripTimesFactory; @@ -110,9 +112,15 @@ class GraphQLIntegrationTest { private static final Place G = TEST_MODEL.place("G", 9.5, 11.0); private static final Place H = TEST_MODEL.place("H", 10.0, 11.5); + private static final Station STATION_1 = TEST_MODEL.station("station").build(); + private static final RegularStop CHILD_STOP = TEST_MODEL.stop("child-stop", STATION_1).build(); + private static final Entrance ENTRANCE = TEST_MODEL + .entrance("station-entrance", STATION_1) + .build(); + private static final Entrance EXIT = TEST_MODEL.entrance("station-exit", STATION_1).build(); + private static final List STOP_LOCATIONS = Stream - .of(A, B, C, D, E, F, G, H) - .map(p -> (RegularStop) p.stop) + .concat(Stream.of(A, B, C, D, E, F, G, H).map(p -> (RegularStop) p.stop), Stream.of(CHILD_STOP)) .toList(); private static VehicleRentalStation VEHICLE_RENTAL_STATION = new TestVehicleRentalStationBuilder() @@ -157,7 +165,7 @@ static void setup() { var stopModel = TEST_MODEL.stopModelBuilder(); STOP_LOCATIONS.forEach(stopModel::withRegularStop); - var model = stopModel.build(); + var model = stopModel.withStation(STATION_1).withEntrance(ENTRANCE).withEntrance(EXIT).build(); var transitModel = new TransitModel(model, DEDUPLICATOR); final TripPattern pattern = TEST_MODEL.pattern(BUS).build(); diff --git a/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java b/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java index 0f6d872c639..f721b263832 100644 --- a/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java +++ b/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java @@ -29,6 +29,8 @@ import org.opentripplanner.transit.model.network.TripPatternBuilder; import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.site.AreaStopBuilder; +import org.opentripplanner.transit.model.site.Entrance; +import org.opentripplanner.transit.model.site.EntranceBuilder; import org.opentripplanner.transit.model.site.GroupStop; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.RegularStopBuilder; @@ -156,6 +158,10 @@ public RegularStopBuilder stop(String idAndName, double lat, double lon) { return stop(idAndName).withCoordinate(lat, lon); } + public RegularStopBuilder stop(String idAndName, Station station) { + return stop(idAndName).withParentStation(station); + } + public StationBuilder station(String idAndName) { return Station .of(new FeedScopedId(FEED_ID, idAndName)) @@ -166,6 +172,15 @@ public StationBuilder station(String idAndName) { .withPriority(StopTransferPriority.ALLOWED); } + public EntranceBuilder entrance(String idAndName, Station station) { + return Entrance + .of(new FeedScopedId(FEED_ID, idAndName)) + .withName(new NonLocalizedString(idAndName)) + .withParentStation(station) + .withCoordinate(70.0, 20.0) + .withDescription(new NonLocalizedString("Entrance " + idAndName)); + } + public GroupStop groupStop(String idAndName, RegularStop... stops) { var builder = stopModelBuilder .groupStop(id(idAndName)) diff --git a/src/test/java/org/opentripplanner/transit/model/site/EntranceTest.java b/src/test/java/org/opentripplanner/transit/model/site/EntranceTest.java index 738238caa0f..2643382934d 100644 --- a/src/test/java/org/opentripplanner/transit/model/site/EntranceTest.java +++ b/src/test/java/org/opentripplanner/transit/model/site/EntranceTest.java @@ -73,4 +73,10 @@ void sameAs() { subject.sameAs(subject.copy().withDescription(new NonLocalizedString("X")).build()) ); } + + @Test + void isAddedToParentStation() { + assertTrue(subject.isPartOfStation()); + assertTrue(PARENT_STATION.getEntrances().contains(subject)); + } } diff --git a/src/test/java/org/opentripplanner/transit/model/site/StationTest.java b/src/test/java/org/opentripplanner/transit/model/site/StationTest.java index 9ff75583bfc..a47143abe6e 100644 --- a/src/test/java/org/opentripplanner/transit/model/site/StationTest.java +++ b/src/test/java/org/opentripplanner/transit/model/site/StationTest.java @@ -24,8 +24,6 @@ class StationTest { private static final StopTransferPriority PRIORITY = StopTransferPriority.ALLOWED; private static final ZoneId TIMEZONE = ZoneId.of(TransitModelForTest.TIME_ZONE_ID); private static final I18NString URL = new NonLocalizedString("url"); - private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); - private static final Station PARENT_STATION = TEST_MODEL.station("stationId").build(); private static final Station subject = Station .of(TransitModelForTest.id(ID)) diff --git a/src/test/java/org/opentripplanner/transit/service/StopModelTest.java b/src/test/java/org/opentripplanner/transit/service/StopModelTest.java index b24b547a07e..5183a4cf0b4 100644 --- a/src/test/java/org/opentripplanner/transit/service/StopModelTest.java +++ b/src/test/java/org/opentripplanner/transit/service/StopModelTest.java @@ -14,6 +14,7 @@ import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.site.AreaStop; +import org.opentripplanner.transit.model.site.Entrance; import org.opentripplanner.transit.model.site.GroupOfStations; import org.opentripplanner.transit.model.site.GroupStop; import org.opentripplanner.transit.model.site.MultiModalStation; @@ -24,6 +25,7 @@ class StopModelTest { private static final WgsCoordinate COOR_A = new WgsCoordinate(60.0, 11.0); private static final WgsCoordinate COOR_B = new WgsCoordinate(62.0, 12.0); + private static final WgsCoordinate COOR_C = new WgsCoordinate(64.0, 13.0); private static final Geometry GEOMETRY = GeometryUtils .getGeometryFactory() .createPoint(COOR_A.asJtsCoordinate()); @@ -35,7 +37,15 @@ class StopModelTest { .withCoordinate(COOR_B) .build(); private static final String EXP_STATIONS = List.of(STATION).toString(); - + private static final FeedScopedId ENTRANCE_ID = TransitModelForTest.id("ENTRANCE"); + public static final NonLocalizedString ENTRANCE_NAME = NonLocalizedString.ofNullable("ENTRANCE"); + private static final Entrance ENTRANCE = Entrance + .of(ENTRANCE_ID) + .withName(ENTRANCE_NAME) + .withCoordinate(COOR_C) + .withParentStation(STATION) + .build(); + private static final String EXP_ENTRANCES = List.of(ENTRANCE).toString(); private final StopModelBuilder stopModelBuilder = StopModel.of(); private final RegularStop stop = stopModelBuilder .regularStop(ID) @@ -109,6 +119,10 @@ void testStations() { assertEquals(EXP_STATIONS, m.listStopLocationGroups().toString()); assertEquals(COOR_B, m.getCoordinateById(ID)); assertFalse(m.hasAreaStops()); + // Stations should also implicitly add all of their entrances + assertEquals(ENTRANCE, m.getEntranceById(ENTRANCE_ID)); + assertEquals(EXP_ENTRANCES, m.listEntrances().toString()); + assertEquals(COOR_C, m.getCoordinateById(ENTRANCE_ID)); } @Test @@ -140,4 +154,13 @@ void testNullStopLocationId() { var m = StopModel.of().build(); assertNull(m.getStopLocation(null)); } + + @Test + void testEntrances() { + var m = stopModelBuilder.withEntrance(ENTRANCE).build(); + assertEquals(ENTRANCE, m.getEntranceById(ENTRANCE_ID)); + assertEquals(EXP_ENTRANCES, m.listEntrances().toString()); + assertEquals(COOR_C, m.getCoordinateById(ENTRANCE_ID)); + assertFalse(m.hasAreaStops()); + } } diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/entrances-ids.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/entrances-ids.json new file mode 100644 index 00000000000..a11f8e6a7b1 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/entrances-ids.json @@ -0,0 +1,14 @@ +{ + "data" : { + "entrances" : [ + { + "gtfsId" : "F:station-entrance", + "lat" : 70.0, + "lon" : 20.0, + "name" : "station-entrance", + "vehicleMode" : null, + "locationType" : "ENTRANCE" + } + ] + } +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/entrances-name.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/entrances-name.json new file mode 100644 index 00000000000..a226c1df548 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/entrances-name.json @@ -0,0 +1,14 @@ +{ + "data" : { + "entrances" : [ + { + "gtfsId" : "F:station-exit", + "lat" : 70.0, + "lon" : 20.0, + "name" : "station-exit", + "vehicleMode" : null, + "locationType" : "ENTRANCE" + } + ] + } +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/entrances.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/entrances.json new file mode 100644 index 00000000000..3bc30323118 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/entrances.json @@ -0,0 +1,22 @@ +{ + "data" : { + "entrances" : [ + { + "gtfsId" : "F:station-entrance", + "lat" : 70.0, + "lon" : 20.0, + "name" : "station-entrance", + "vehicleMode" : null, + "locationType" : "ENTRANCE" + }, + { + "gtfsId" : "F:station-exit", + "lat" : 70.0, + "lon" : 20.0, + "name" : "station-exit", + "vehicleMode" : null, + "locationType" : "ENTRANCE" + } + ] + } +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stations.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stations.json new file mode 100644 index 00000000000..e2168aeecf3 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stations.json @@ -0,0 +1,40 @@ +{ + "data" : { + "stations" : [ + { + "gtfsId" : "F:station", + "lat" : 60.0, + "lon" : 10.0, + "name" : "station", + "stops" : [ + { + "gtfsId" : "F:child-stop", + "lat" : 60.0, + "lon" : 10.0, + "name" : "child-stop", + "vehicleMode" : "BUS", + "locationType" : "STOP" + } + ], + "entrances" : [ + { + "gtfsId" : "F:station-entrance", + "lat" : 70.0, + "lon" : 20.0, + "name" : "station-entrance", + "vehicleMode" : null, + "locationType" : "ENTRANCE" + }, + { + "gtfsId" : "F:station-exit", + "lat" : 70.0, + "lon" : 20.0, + "name" : "station-exit", + "vehicleMode" : null, + "locationType" : "ENTRANCE" + } + ] + } + ] + } +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stops.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stops.json index 307b07b58aa..0348202ce07 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stops.json +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stops.json @@ -56,7 +56,14 @@ "lon" : 11.5, "name" : "H", "vehicleMode" : "BUS" + }, + { + "gtfsId" : "F:child-stop", + "lat" : 60.0, + "lon" : 10.0, + "name" : "child-stop", + "vehicleMode" : "BUS" } ] } -} \ No newline at end of file +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/entrances-ids.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/entrances-ids.graphql new file mode 100644 index 00000000000..7c04e579768 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/entrances-ids.graphql @@ -0,0 +1,10 @@ +{ + entrances(ids: ["F:station-entrance"]) { + gtfsId + lat + lon + name + vehicleMode + locationType + } +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/entrances-name.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/entrances-name.graphql new file mode 100644 index 00000000000..b14563fcba4 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/entrances-name.graphql @@ -0,0 +1,10 @@ +{ + entrances(name: "station-exit") { + gtfsId + lat + lon + name + vehicleMode + locationType + } +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/entrances.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/entrances.graphql new file mode 100644 index 00000000000..7b485eec866 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/entrances.graphql @@ -0,0 +1,10 @@ +{ + entrances { + gtfsId + lat + lon + name + vehicleMode + locationType + } +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/stations.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/stations.graphql new file mode 100644 index 00000000000..8c3a0539bdd --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/stations.graphql @@ -0,0 +1,24 @@ +{ + stations { + gtfsId + lat + lon + name + stops { + gtfsId + lat + lon + name + vehicleMode + locationType + } + entrances { + gtfsId + lat + lon + name + vehicleMode + locationType + } + } +}