diff --git a/application/src/client/index.html b/application/src/client/index.html index 4d0e989196c..e463c510830 100644 --- a/application/src/client/index.html +++ b/application/src/client/index.html @@ -5,8 +5,8 @@ OTP Debug Client - - + +
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 3cd98b15652..d4f1bb52ec4 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 @@ -1492,6 +1492,7 @@ public enum GraphQLPlanAccessMode { BICYCLE, BICYCLE_PARKING, BICYCLE_RENTAL, + CAR, CAR_DROP_OFF, CAR_PARKING, CAR_RENTAL, @@ -1575,6 +1576,7 @@ public enum GraphQLPlanDirectMode { public enum GraphQLPlanEgressMode { BICYCLE, BICYCLE_RENTAL, + CAR, CAR_PICKUP, CAR_RENTAL, FLEX, @@ -1877,6 +1879,7 @@ public void setGraphQLWalk(GraphQLWalkPreferencesInput walk) { public enum GraphQLPlanTransferMode { BICYCLE, + CAR, WALK, } diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/AccessModeMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/AccessModeMapper.java index ac4c90a1a56..e0e3ac0bbb2 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/AccessModeMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/AccessModeMapper.java @@ -13,6 +13,7 @@ public static StreetMode map(GraphQLTypes.GraphQLPlanAccessMode mode) { case BICYCLE -> StreetMode.BIKE; case BICYCLE_RENTAL -> StreetMode.BIKE_RENTAL; case BICYCLE_PARKING -> StreetMode.BIKE_TO_PARK; + case CAR -> StreetMode.CAR; case CAR_RENTAL -> StreetMode.CAR_RENTAL; case CAR_PARKING -> StreetMode.CAR_TO_PARK; case CAR_DROP_OFF -> StreetMode.CAR_PICKUP; diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/EgressModeMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/EgressModeMapper.java index f03b160ac97..ddcaa255f2a 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/EgressModeMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/EgressModeMapper.java @@ -12,6 +12,7 @@ public static StreetMode map(GraphQLTypes.GraphQLPlanEgressMode mode) { return switch (mode) { case BICYCLE -> StreetMode.BIKE; case BICYCLE_RENTAL -> StreetMode.BIKE_RENTAL; + case CAR -> StreetMode.CAR; case CAR_RENTAL -> StreetMode.CAR_RENTAL; case CAR_PICKUP -> StreetMode.CAR_PICKUP; case FLEX -> StreetMode.FLEXIBLE; diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ModePreferencesMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ModePreferencesMapper.java index 32d3456df57..663e93acca9 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ModePreferencesMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ModePreferencesMapper.java @@ -168,5 +168,10 @@ private static void validateStreetModes(JourneyRequest journey) { "If BICYCLE is used for access, egress or transfer, then it should be used for all." ); } + if (modes.contains(StreetMode.CAR) && modes.size() != 1) { + throw new IllegalArgumentException( + "If CAR is used for access, egress or transfer, then it should be used for all." + ); + } } } diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/TransferModeMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/TransferModeMapper.java index ffa7363e3a7..18d2c0e3811 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/TransferModeMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/TransferModeMapper.java @@ -10,6 +10,7 @@ public class TransferModeMapper { public static StreetMode map(GraphQLTypes.GraphQLPlanTransferMode mode) { return switch (mode) { + case CAR -> StreetMode.CAR; case BICYCLE -> StreetMode.BIKE; case WALK -> StreetMode.WALK; }; diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapper.java b/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapper.java index 974b8dd10c3..bf9abd3a60d 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapper.java @@ -10,7 +10,8 @@ class RequestModesMapper { - private static final Predicate IS_BIKE = m -> m == StreetMode.BIKE; + private static final Predicate IS_BIKE_OR_CAR = m -> + m == StreetMode.BIKE || m == StreetMode.CAR; private static final String accessModeKey = "accessMode"; private static final String egressModeKey = "egressMode"; private static final String directModeKey = "directMode"; @@ -27,7 +28,10 @@ static RequestModes mapRequestModes(Map modesInput) { ensureValueAndSet(accessMode, mBuilder::withAccessMode); ensureValueAndSet((StreetMode) modesInput.get(egressModeKey), mBuilder::withEgressMode); ensureValueAndSet((StreetMode) modesInput.get(directModeKey), mBuilder::withDirectMode); - Optional.ofNullable(accessMode).filter(IS_BIKE).ifPresent(mBuilder::withTransferMode); + // The only cases in which the transferMode isn't WALK are when the accessMode is either BIKE or CAR. + // In these cases, the transferMode is the same as the accessMode. This check is not strictly necessary + // if there is a need for more freedom for specifying the transferMode. + Optional.ofNullable(accessMode).filter(IS_BIKE_OR_CAR).ifPresent(mBuilder::withTransferMode); return mBuilder.build(); } 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 0dde1494f0a..1fc87176af8 100644 --- a/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java +++ b/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java @@ -1,5 +1,6 @@ package org.opentripplanner.apis.vectortiles; +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -10,7 +11,9 @@ import org.opentripplanner.apis.vectortiles.model.VectorSourceLayer; import org.opentripplanner.apis.vectortiles.model.ZoomDependentNumber; import org.opentripplanner.apis.vectortiles.model.ZoomDependentNumber.ZoomStop; +import org.opentripplanner.framework.collection.ListUtils; import org.opentripplanner.service.vehiclerental.street.StreetVehicleRentalLink; +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; @@ -44,17 +47,15 @@ public class DebugStyleSpec { private static final String DARK_GREEN = "#136b04"; private static final String PURPLE = "#BC55F2"; private static final String BLACK = "#140d0e"; + private static final int MAX_ZOOM = 23; - private static final ZoomDependentNumber LINE_WIDTH = new ZoomDependentNumber( - 1.3f, - List.of(new ZoomStop(13, 0.5f), new ZoomStop(MAX_ZOOM, 10)) - ); private static final ZoomDependentNumber LINE_OFFSET = new ZoomDependentNumber( - 1.3f, List.of(new ZoomStop(13, 0.3f), new ZoomStop(MAX_ZOOM, 6)) ); + private static final ZoomDependentNumber LINE_WIDTH = new ZoomDependentNumber( + List.of(new ZoomStop(13, 0.2f), new ZoomStop(MAX_ZOOM, 8)) + ); private static final ZoomDependentNumber CIRCLE_STROKE = new ZoomDependentNumber( - 1, List.of(new ZoomStop(15, 0.2f), new ZoomStop(MAX_ZOOM, 3)) ); private static final Class[] EDGES_TO_DISPLAY = new Class[] { @@ -66,6 +67,10 @@ public class DebugStyleSpec { TemporaryPartialStreetEdge.class, TemporaryFreeEdge.class, }; + private static final String EDGES_GROUP = "Edges"; + private static final String STOPS_GROUP = "Stops"; + private static final String VERTICES_GROUP = "Vertices"; + private static final String TRAVERSAL_PERMISSIONS_GROUP = "Traversal permissions"; static StyleSpec build( VectorSourceLayer regularStops, @@ -83,104 +88,178 @@ static StyleSpec build( return new StyleSpec( "OTP Debug Tiles", allSources, - List.of( - StyleBuilder.ofId("background").typeRaster().source(BACKGROUND_SOURCE).minZoom(0), - StyleBuilder - .ofId("edge") - .typeLine() - .vectorSourceLayer(edges) - .lineColor(MAGENTA) - .edgeFilter(EDGES_TO_DISPLAY) - .lineWidth(LINE_WIDTH) - .lineOffset(LINE_OFFSET) - .minZoom(6) - .maxZoom(MAX_ZOOM) - .intiallyHidden(), + ListUtils.combine( + List.of(StyleBuilder.ofId("background").typeRaster().source(BACKGROUND_SOURCE).minZoom(0)), + traversalPermissions(edges), + edges(edges), + vertices(vertices), + stops(regularStops, areaStops, groupStops) + ) + ); + } + + private static List stops( + VectorSourceLayer regularStops, + VectorSourceLayer areaStops, + VectorSourceLayer groupStops + ) { + return List.of( + StyleBuilder + .ofId("area-stop") + .group(STOPS_GROUP) + .typeFill() + .vectorSourceLayer(areaStops) + .fillColor(BRIGHT_GREEN) + .fillOpacity(0.5f) + .fillOutlineColor(BLACK) + .minZoom(6) + .maxZoom(MAX_ZOOM), + StyleBuilder + .ofId("group-stop") + .group(STOPS_GROUP) + .typeFill() + .vectorSourceLayer(groupStops) + .fillColor(BRIGHT_GREEN) + .fillOpacity(0.5f) + .fillOutlineColor(BLACK) + .minZoom(6) + .maxZoom(MAX_ZOOM), + StyleBuilder + .ofId("regular-stop") + .group(STOPS_GROUP) + .typeCircle() + .vectorSourceLayer(regularStops) + .circleStroke( + BLACK, + new ZoomDependentNumber(List.of(new ZoomStop(11, 0.5f), new ZoomStop(MAX_ZOOM, 5))) + ) + .circleRadius( + new ZoomDependentNumber(List.of(new ZoomStop(11, 0.5f), new ZoomStop(MAX_ZOOM, 10))) + ) + .circleColor("#fcf9fa") + .minZoom(10) + .maxZoom(MAX_ZOOM) + ); + } + + private static List vertices(VectorSourceLayer vertices) { + return List.of( + StyleBuilder + .ofId("vertex") + .group(VERTICES_GROUP) + .typeCircle() + .vectorSourceLayer(vertices) + .circleStroke(BLACK, CIRCLE_STROKE) + .circleRadius( + new ZoomDependentNumber(List.of(new ZoomStop(15, 1), new ZoomStop(MAX_ZOOM, 7))) + ) + .circleColor(PURPLE) + .minZoom(15) + .maxZoom(MAX_ZOOM) + .intiallyHidden(), + StyleBuilder + .ofId("parking-vertex") + .group(VERTICES_GROUP) + .typeCircle() + .vectorSourceLayer(vertices) + .vertexFilter(VehicleParkingEntranceVertex.class) + .circleStroke(BLACK, CIRCLE_STROKE) + .circleRadius( + new ZoomDependentNumber(List.of(new ZoomStop(13, 1.4f), new ZoomStop(MAX_ZOOM, 10))) + ) + .circleColor(DARK_GREEN) + .minZoom(13) + .maxZoom(MAX_ZOOM) + .intiallyHidden() + ); + } + + private static List edges(VectorSourceLayer edges) { + return List.of( + StyleBuilder + .ofId("edge") + .group(EDGES_GROUP) + .typeLine() + .vectorSourceLayer(edges) + .lineColor(MAGENTA) + .edgeFilter(EDGES_TO_DISPLAY) + .lineWidth(LINE_WIDTH) + .lineOffset(LINE_OFFSET) + .minZoom(6) + .maxZoom(MAX_ZOOM) + .intiallyHidden(), + StyleBuilder + .ofId("edge-name") + .group(EDGES_GROUP) + .typeSymbol() + .lineText("name") + .vectorSourceLayer(edges) + .edgeFilter(EDGES_TO_DISPLAY) + .minZoom(17) + .maxZoom(MAX_ZOOM) + .intiallyHidden(), + StyleBuilder + .ofId("link") + .group(EDGES_GROUP) + .typeLine() + .vectorSourceLayer(edges) + .lineColor(BRIGHT_GREEN) + .edgeFilter( + StreetTransitStopLink.class, + StreetTransitEntranceLink.class, + BoardingLocationToStopLink.class, + StreetVehicleRentalLink.class, + StreetVehicleParkingLink.class, + StreetStationCentroidLink.class + ) + .lineWidth(LINE_WIDTH) + .lineOffset(LINE_OFFSET) + .minZoom(13) + .maxZoom(MAX_ZOOM) + .intiallyHidden() + ); + } + + private static List traversalPermissions(VectorSourceLayer edges) { + var permissionStyles = Arrays + .stream(StreetTraversalPermission.values()) + .map(p -> StyleBuilder - .ofId("edge-name") - .typeSymbol() - .lineText("name") + .ofId(p.name()) .vectorSourceLayer(edges) - .edgeFilter(EDGES_TO_DISPLAY) - .minZoom(17) - .maxZoom(MAX_ZOOM) - .intiallyHidden(), - StyleBuilder - .ofId("link") + .group(TRAVERSAL_PERMISSIONS_GROUP) .typeLine() - .vectorSourceLayer(edges) - .lineColor(BRIGHT_GREEN) - .edgeFilter( - StreetTransitStopLink.class, - StreetTransitEntranceLink.class, - BoardingLocationToStopLink.class, - StreetVehicleRentalLink.class, - StreetVehicleParkingLink.class, - StreetStationCentroidLink.class - ) + .lineColor(permissionColor(p)) + .permissionsFilter(p) .lineWidth(LINE_WIDTH) .lineOffset(LINE_OFFSET) - .minZoom(13) - .maxZoom(MAX_ZOOM) - .intiallyHidden(), - StyleBuilder - .ofId("vertex") - .typeCircle() - .vectorSourceLayer(vertices) - .circleStroke(BLACK, CIRCLE_STROKE) - .circleRadius( - new ZoomDependentNumber(1, List.of(new ZoomStop(15, 1), new ZoomStop(MAX_ZOOM, 7))) - ) - .circleColor(PURPLE) - .minZoom(15) - .maxZoom(MAX_ZOOM) - .intiallyHidden(), - StyleBuilder - .ofId("parking-vertex") - .typeCircle() - .vectorSourceLayer(vertices) - .vertexFilter(VehicleParkingEntranceVertex.class) - .circleStroke(BLACK, CIRCLE_STROKE) - .circleRadius( - new ZoomDependentNumber(1, List.of(new ZoomStop(13, 1.4f), new ZoomStop(MAX_ZOOM, 10))) - ) - .circleColor(DARK_GREEN) - .minZoom(13) - .maxZoom(MAX_ZOOM) - .intiallyHidden(), - StyleBuilder - .ofId("area-stop") - .typeFill() - .vectorSourceLayer(areaStops) - .fillColor(BRIGHT_GREEN) - .fillOpacity(0.5f) - .fillOutlineColor(BLACK) - .minZoom(6) - .maxZoom(MAX_ZOOM), - StyleBuilder - .ofId("group-stop") - .typeFill() - .vectorSourceLayer(groupStops) - .fillColor(BRIGHT_GREEN) - .fillOpacity(0.5f) - .fillOutlineColor(BLACK) .minZoom(6) - .maxZoom(MAX_ZOOM), - StyleBuilder - .ofId("regular-stop") - .typeCircle() - .vectorSourceLayer(regularStops) - .circleStroke( - BLACK, - new ZoomDependentNumber(1, List.of(new ZoomStop(11, 0.5f), new ZoomStop(MAX_ZOOM, 5))) - ) - .circleRadius( - new ZoomDependentNumber(1, List.of(new ZoomStop(11, 0.5f), new ZoomStop(MAX_ZOOM, 10))) - ) - .circleColor("#fcf9fa") - .minZoom(10) .maxZoom(MAX_ZOOM) + .intiallyHidden() ) - ); + .toList(); + var textStyle = StyleBuilder + .ofId("permission-text") + .vectorSourceLayer(edges) + .group(TRAVERSAL_PERMISSIONS_GROUP) + .typeSymbol() + .lineText("permission") + .textOffset(1) + .edgeFilter(EDGES_TO_DISPLAY) + .minZoom(17) + .maxZoom(MAX_ZOOM) + .intiallyHidden(); + return ListUtils.combine(permissionStyles, List.of(textStyle)); + } + + private static String permissionColor(StreetTraversalPermission p) { + return switch (p) { + case NONE -> "#000"; + case PEDESTRIAN -> "#2ba812"; + case BICYCLE, PEDESTRIAN_AND_BICYCLE -> "#10d3b6"; + case CAR -> "#f92e13"; + case BICYCLE_AND_CAR, PEDESTRIAN_AND_CAR -> "#e25f8f"; + case ALL -> "#adb2b0"; + }; } } diff --git a/application/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java b/application/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java index 14b2fe8b014..93b7ea91e7c 100644 --- a/application/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java +++ b/application/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java @@ -11,6 +11,7 @@ import org.opentripplanner.apis.vectortiles.model.ZoomDependentNumber.ZoomStop; import org.opentripplanner.framework.collection.ListUtils; import org.opentripplanner.framework.json.ObjectMappers; +import org.opentripplanner.street.model.StreetTraversalPermission; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.vertex.Vertex; @@ -26,6 +27,7 @@ public class StyleBuilder { private final Map props = new LinkedHashMap<>(); private final Map paint = new LinkedHashMap<>(); private final Map layout = new LinkedHashMap<>(); + private final Map metadata = new LinkedHashMap<>(); private final Map line = new LinkedHashMap<>(); private List filter = List.of(); @@ -48,6 +50,7 @@ public enum LayerType { private StyleBuilder(String id) { props.put("id", id); + metadata.put("group", "Other"); } public StyleBuilder minZoom(int i) { @@ -107,18 +110,28 @@ private StyleBuilder type(LayerType type) { return this; } + /** + * Puts the layer into an arbitrarily defined group in the layer selector. This allows you + * to switch the entire group on and off. + */ + public StyleBuilder group(String group) { + metadata.put("group", group); + return this; + } + public StyleBuilder lineText(String name) { - layout.put("symbol-placement", "line"); - layout.put("symbol-spacing", 500); + layout.put("symbol-placement", "line-center"); + layout.put("symbol-spacing", 1000); layout.put("text-field", "{%s}".formatted(name)); layout.put("text-font", List.of("KlokanTech Noto Sans Regular")); layout.put( "text-size", - new ZoomDependentNumber(14, List.of(new ZoomStop(14, 12), new ZoomStop(20, 14))).toJson() + new ZoomDependentNumber(List.of(new ZoomStop(10, 6), new ZoomStop(24, 12))).toJson() ); - layout.put("text-max-width", 5); + layout.put("text-max-width", 100); layout.put("text-keep-upright", true); layout.put("text-rotation-alignment", "map"); + layout.put("text-overlap", "never"); paint.put("text-color", "#000"); paint.put("text-halo-color", "#fff"); paint.put("text-halo-blur", 4); @@ -126,6 +139,11 @@ public StyleBuilder lineText(String name) { return this; } + public StyleBuilder textOffset(float offset) { + layout.put("text-offset", List.of(0, offset)); + return this; + } + public StyleBuilder circleColor(String color) { paint.put("circle-color", validateColor(color)); return this; @@ -201,6 +219,14 @@ public final StyleBuilder edgeFilter(Class... classToFilter) { return filterClasses(classToFilter); } + /** + * Filter the entities by their "permission" property. + */ + public final StyleBuilder permissionsFilter(StreetTraversalPermission p) { + filter = List.of("==", "permission", p.name()); + return this; + } + /** * Only apply the style to the given vertices. */ @@ -225,6 +251,7 @@ public JsonNode toJson() { if (!line.isEmpty()) { copy.put("line", line); } + copy.put("metadata", metadata); return OBJECT_MAPPER.valueToTree(copy); } diff --git a/application/src/main/java/org/opentripplanner/apis/vectortiles/model/ZoomDependentNumber.java b/application/src/main/java/org/opentripplanner/apis/vectortiles/model/ZoomDependentNumber.java index d83c4b63495..4d1d83ee8a9 100644 --- a/application/src/main/java/org/opentripplanner/apis/vectortiles/model/ZoomDependentNumber.java +++ b/application/src/main/java/org/opentripplanner/apis/vectortiles/model/ZoomDependentNumber.java @@ -2,21 +2,26 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import java.util.stream.Stream; +import org.opentripplanner.framework.collection.ListUtils; import org.opentripplanner.framework.json.ObjectMappers; /** * A style parameter that allows you to specify a number that changes dependent on the zoom level. */ -public record ZoomDependentNumber(float base, List stops) { +public record ZoomDependentNumber(List stops) { private static final ObjectMapper OBJECT_MAPPER = ObjectMappers.ignoringExtraFields(); public JsonNode toJson() { - var props = new LinkedHashMap<>(); - props.put("base", base); - var vals = stops.stream().map(ZoomStop::toList).toList(); - props.put("stops", vals); - return OBJECT_MAPPER.valueToTree(props); + var interpolation = new ArrayList<>(); + interpolation.add("interpolate"); + interpolation.add(List.of("linear")); + interpolation.add(List.of("zoom")); + stops.forEach(s -> interpolation.addAll(s.toList())); + + return OBJECT_MAPPER.valueToTree(interpolation); } /** diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/StreetLinkerModule.java b/application/src/main/java/org/opentripplanner/graph_builder/module/StreetLinkerModule.java index 59b6a8b48a0..dce445d381a 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/StreetLinkerModule.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/StreetLinkerModule.java @@ -28,6 +28,7 @@ import org.opentripplanner.street.model.vertex.Vertex; import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.street.search.TraverseModeSet; +import org.opentripplanner.transit.model.network.CarAccess; import org.opentripplanner.transit.model.site.GroupStop; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.StopLocation; @@ -87,25 +88,12 @@ public void linkTransitStops(Graph graph, TimetableRepository timetableRepositor LOG.info(progress.startMessage()); Set stopLocationsUsedForFlexTrips = Set.of(); - if (OTPFeature.FlexRouting.isOn()) { - stopLocationsUsedForFlexTrips = - timetableRepository - .getAllFlexTrips() - .stream() - .flatMap(t -> t.getStops().stream()) - .collect(Collectors.toSet()); - - stopLocationsUsedForFlexTrips.addAll( - stopLocationsUsedForFlexTrips - .stream() - .filter(GroupStop.class::isInstance) - .map(GroupStop.class::cast) - .flatMap(g -> g.getChildLocations().stream().filter(RegularStop.class::isInstance)) - .toList() - ); + stopLocationsUsedForFlexTrips = getStopLocationsUsedForFlexTrips(timetableRepository); } + Set stopLocationsUsedForCarsAllowedTrips = timetableRepository.getStopLocationsUsedForCarsAllowedTrips(); + for (TransitStopVertex tStop : vertices) { // Stops with pathways do not need to be connected to the street network, since there are explicit entrances defined for that if (tStop.hasPathways()) { @@ -120,7 +108,10 @@ public void linkTransitStops(Graph graph, TimetableRepository timetableRepositor StopLinkType linkType = StopLinkType.WALK_ONLY; if ( - OTPFeature.FlexRouting.isOn() && stopLocationsUsedForFlexTrips.contains(tStop.getStop()) + ( + OTPFeature.FlexRouting.isOn() && stopLocationsUsedForFlexTrips.contains(tStop.getStop()) + ) || + stopLocationsUsedForCarsAllowedTrips.contains(tStop.getStop()) ) { linkType = StopLinkType.WALK_AND_CAR; } @@ -366,6 +357,26 @@ private VehicleParking removeVehicleParkingEntranceVertexFromGraph( } } + private Set getStopLocationsUsedForFlexTrips( + TimetableRepository timetableRepository + ) { + Set stopLocations = timetableRepository + .getAllFlexTrips() + .stream() + .flatMap(t -> t.getStops().stream()) + .collect(Collectors.toSet()); + + stopLocations.addAll( + stopLocations + .stream() + .filter(GroupStop.class::isInstance) + .map(GroupStop.class::cast) + .flatMap(g -> g.getChildLocations().stream().filter(RegularStop.class::isInstance)) + .toList() + ); + return stopLocations; + } + private enum StopLinkType { /** * Only ensure that the link leads to a walkable edge. diff --git a/application/src/main/java/org/opentripplanner/gtfs/mapping/CarAccessMapper.java b/application/src/main/java/org/opentripplanner/gtfs/mapping/CarAccessMapper.java new file mode 100644 index 00000000000..4034814462b --- /dev/null +++ b/application/src/main/java/org/opentripplanner/gtfs/mapping/CarAccessMapper.java @@ -0,0 +1,19 @@ +package org.opentripplanner.gtfs.mapping; + +import org.onebusaway.gtfs.model.Trip; +import org.opentripplanner.transit.model.network.CarAccess; + +/** + * Model car access for GTFS trips. + */ +class CarAccessMapper { + + public static CarAccess mapForTrip(Trip rhs) { + int carsAllowed = rhs.getCarsAllowed(); + return switch (carsAllowed) { + case 1 -> CarAccess.ALLOWED; + case 2 -> CarAccess.NOT_ALLOWED; + default -> CarAccess.UNKNOWN; + }; + } +} diff --git a/application/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java b/application/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java index fdef2bb4e0c..4e805d6d1df 100644 --- a/application/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java +++ b/application/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java @@ -71,6 +71,7 @@ private Trip doMap(org.onebusaway.gtfs.model.Trip rhs) { lhs.withShapeId(AgencyAndIdMapper.mapAgencyAndId(rhs.getShapeId())); lhs.withWheelchairBoarding(WheelchairAccessibilityMapper.map(rhs.getWheelchairAccessible())); lhs.withBikesAllowed(BikeAccessMapper.mapForTrip(rhs)); + lhs.withCarsAllowed(CarAccessMapper.mapForTrip(rhs)); var trip = lhs.build(); mapSafeTimePenalty(rhs).ifPresent(f -> flexSafeTimePenalties.put(trip, f)); diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RouteRequestTransitDataProviderFilter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RouteRequestTransitDataProviderFilter.java index 44d9f3cdb3d..514a5b56f9a 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RouteRequestTransitDataProviderFilter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RouteRequestTransitDataProviderFilter.java @@ -12,6 +12,7 @@ import org.opentripplanner.transit.model.basic.Accessibility; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.BikeAccess; +import org.opentripplanner.transit.model.network.CarAccess; import org.opentripplanner.transit.model.network.RoutingTripPattern; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; @@ -20,6 +21,8 @@ public class RouteRequestTransitDataProviderFilter implements TransitDataProvide private final boolean requireBikesAllowed; + private final boolean requireCarsAllowed; + private final boolean wheelchairEnabled; private final WheelchairPreferences wheelchairPreferences; @@ -41,6 +44,7 @@ public class RouteRequestTransitDataProviderFilter implements TransitDataProvide public RouteRequestTransitDataProviderFilter(RouteRequest request) { this( request.journey().transfer().mode() == StreetMode.BIKE, + request.journey().transfer().mode() == StreetMode.CAR, request.wheelchair(), request.preferences().wheelchair(), request.preferences().transit().includePlannedCancellations(), @@ -53,6 +57,7 @@ public RouteRequestTransitDataProviderFilter(RouteRequest request) { // This constructor is used only for testing public RouteRequestTransitDataProviderFilter( boolean requireBikesAllowed, + boolean requireCarsAllowed, boolean wheelchairEnabled, WheelchairPreferences wheelchairPreferences, boolean includePlannedCancellations, @@ -61,6 +66,7 @@ public RouteRequestTransitDataProviderFilter( List filters ) { this.requireBikesAllowed = requireBikesAllowed; + this.requireCarsAllowed = requireCarsAllowed; this.wheelchairEnabled = wheelchairEnabled; this.wheelchairPreferences = wheelchairPreferences; this.includePlannedCancellations = includePlannedCancellations; @@ -97,10 +103,12 @@ public boolean tripPatternPredicate(TripPatternForDate tripPatternForDate) { public boolean tripTimesPredicate(TripTimes tripTimes, boolean withFilters) { final Trip trip = tripTimes.getTrip(); - if (requireBikesAllowed) { - if (bikeAccessForTrip(trip) != BikeAccess.ALLOWED) { - return false; - } + if (requireBikesAllowed && bikeAccessForTrip(trip) != BikeAccess.ALLOWED) { + return false; + } + + if (requireCarsAllowed && trip.getCarsAllowed() != CarAccess.ALLOWED) { + return false; } if (wheelchairEnabled) { diff --git a/application/src/main/java/org/opentripplanner/routing/api/request/StreetMode.java b/application/src/main/java/org/opentripplanner/routing/api/request/StreetMode.java index 56e716d9d62..cbc2764f030 100644 --- a/application/src/main/java/org/opentripplanner/routing/api/request/StreetMode.java +++ b/application/src/main/java/org/opentripplanner/routing/api/request/StreetMode.java @@ -36,10 +36,8 @@ public enum StreetMode implements DocumentedEnum { SCOOTER_RENTAL(Feature.ACCESS, Feature.EGRESS, Feature.WALKING, Feature.SCOOTER, Feature.RENTING), /** * Car only - *

- * Direct mode only. */ - CAR(Feature.ACCESS, Feature.DRIVING), + CAR(Feature.ACCESS, Feature.TRANSFER, Feature.EGRESS, Feature.DRIVING), /** * Start in the car, drive to a parking area, and walk the rest of the way. *

diff --git a/application/src/main/java/org/opentripplanner/transit/model/network/BikeAccess.java b/application/src/main/java/org/opentripplanner/transit/model/network/BikeAccess.java index 536a01a9b14..c0d850e9ce7 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/network/BikeAccess.java +++ b/application/src/main/java/org/opentripplanner/transit/model/network/BikeAccess.java @@ -1,6 +1,8 @@ package org.opentripplanner.transit.model.network; /** + * This represents the state of whether bikes are allowed on board trips (or routes). + *

* GTFS codes: * 0 = unknown / unspecified, 1 = bikes allowed, 2 = bikes NOT allowed */ diff --git a/application/src/main/java/org/opentripplanner/transit/model/network/CarAccess.java b/application/src/main/java/org/opentripplanner/transit/model/network/CarAccess.java new file mode 100644 index 00000000000..2fca43cfb32 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/transit/model/network/CarAccess.java @@ -0,0 +1,13 @@ +package org.opentripplanner.transit.model.network; + +/** + * This represents the state of whether cars are allowed on board trips. + *

+ * GTFS codes: + * 0 = unknown / unspecified, 1 = cars allowed, 2 = cars NOT allowed + */ +public enum CarAccess { + UNKNOWN, + NOT_ALLOWED, + ALLOWED, +} diff --git a/application/src/main/java/org/opentripplanner/transit/model/timetable/Trip.java b/application/src/main/java/org/opentripplanner/transit/model/timetable/Trip.java index 53ec7207adc..1a07e96c029 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/timetable/Trip.java +++ b/application/src/main/java/org/opentripplanner/transit/model/timetable/Trip.java @@ -16,6 +16,7 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.framework.LogInfo; import org.opentripplanner.transit.model.network.BikeAccess; +import org.opentripplanner.transit.model.network.CarAccess; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.organization.Operator; @@ -45,6 +46,7 @@ public final class Trip extends AbstractTransitEntity impleme private final Direction direction; private final BikeAccess bikesAllowed; + private final CarAccess carsAllowed; private final Accessibility wheelchairBoarding; private final String gtfsBlockId; @@ -64,6 +66,7 @@ public final class Trip extends AbstractTransitEntity impleme : route.getNetexSubmode(); this.direction = requireNonNullElse(builder.getDirection(), Direction.UNKNOWN); this.bikesAllowed = requireNonNullElse(builder.getBikesAllowed(), route.getBikesAllowed()); + this.carsAllowed = requireNonNullElse(builder.getCarsAllowed(), CarAccess.UNKNOWN); this.wheelchairBoarding = requireNonNullElse(builder.getWheelchairBoarding(), Accessibility.NO_INFORMATION); this.netexAlteration = requireNonNullElse(builder.getNetexAlteration(), TripAlteration.PLANNED); @@ -144,6 +147,10 @@ public BikeAccess getBikesAllowed() { return bikesAllowed; } + public CarAccess getCarsAllowed() { + return carsAllowed; + } + public Accessibility getWheelchairBoarding() { return wheelchairBoarding; } @@ -205,6 +212,7 @@ public boolean sameAs(Trip other) { Objects.equals(this.shapeId, other.shapeId) && Objects.equals(this.direction, other.direction) && Objects.equals(this.bikesAllowed, other.bikesAllowed) && + Objects.equals(this.carsAllowed, other.carsAllowed) && Objects.equals(this.wheelchairBoarding, other.wheelchairBoarding) && Objects.equals(this.netexAlteration, other.netexAlteration) ); diff --git a/application/src/main/java/org/opentripplanner/transit/model/timetable/TripBuilder.java b/application/src/main/java/org/opentripplanner/transit/model/timetable/TripBuilder.java index 063dfe10da2..5ed0616831d 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/timetable/TripBuilder.java +++ b/application/src/main/java/org/opentripplanner/transit/model/timetable/TripBuilder.java @@ -6,6 +6,7 @@ import org.opentripplanner.transit.model.framework.AbstractEntityBuilder; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.BikeAccess; +import org.opentripplanner.transit.model.network.CarAccess; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.organization.Operator; @@ -21,6 +22,7 @@ public class TripBuilder extends AbstractEntityBuilder { private FeedScopedId shapeId; private Direction direction; private BikeAccess bikesAllowed; + private CarAccess carsAllowed; private Accessibility wheelchairBoarding; private String gtfsBlockId; private String netexInternalPlanningCode; @@ -44,6 +46,7 @@ public class TripBuilder extends AbstractEntityBuilder { this.shapeId = original.getShapeId(); this.direction = original.getDirection(); this.bikesAllowed = original.getBikesAllowed(); + this.carsAllowed = original.getCarsAllowed(); this.wheelchairBoarding = original.getWheelchairBoarding(); this.netexInternalPlanningCode = original.getNetexInternalPlanningCode(); } @@ -151,11 +154,20 @@ public BikeAccess getBikesAllowed() { return bikesAllowed; } + public CarAccess getCarsAllowed() { + return carsAllowed; + } + public TripBuilder withBikesAllowed(BikeAccess bikesAllowed) { this.bikesAllowed = bikesAllowed; return this; } + public TripBuilder withCarsAllowed(CarAccess carsAllowed) { + this.carsAllowed = carsAllowed; + return this; + } + public Accessibility getWheelchairBoarding() { return wheelchairBoarding; } diff --git a/application/src/main/java/org/opentripplanner/transit/service/TimetableRepository.java b/application/src/main/java/org/opentripplanner/transit/service/TimetableRepository.java index dc092cc26c2..f12ade0f83c 100644 --- a/application/src/main/java/org/opentripplanner/transit/service/TimetableRepository.java +++ b/application/src/main/java/org/opentripplanner/transit/service/TimetableRepository.java @@ -20,6 +20,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.Nullable; import org.opentripplanner.ext.flex.trip.FlexTrip; import org.opentripplanner.framework.lang.ObjectUtils; @@ -44,9 +45,12 @@ import org.opentripplanner.transit.model.framework.AbstractTransitEntity; import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.CarAccess; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.organization.Operator; +import org.opentripplanner.transit.model.site.GroupStop; +import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.updater.GraphUpdaterManager; @@ -564,6 +568,36 @@ public FlexTrip getFlexTrip(FeedScopedId tripId) { return flexTripsById.get(tripId); } + /** + * The stops that are used by transit capable of transporting cars need to be + * connected to the road network (e.g. car ferries). This method returns the + * stops that are used by trips that allow cars. + * @return set of stop locations that are used for trips that allow cars + */ + public Set getStopLocationsUsedForCarsAllowedTrips() { + Set stopLocations = getAllTripPatterns() + .stream() + .filter(t -> + t + .getScheduledTimetable() + .getTripTimes() + .stream() + .anyMatch(tt -> tt.getTrip().getCarsAllowed() == CarAccess.ALLOWED) + ) + .flatMap(t -> t.getStops().stream()) + .collect(Collectors.toSet()); + + stopLocations.addAll( + stopLocations + .stream() + .filter(GroupStop.class::isInstance) + .map(GroupStop.class::cast) + .flatMap(g -> g.getChildLocations().stream().filter(RegularStop.class::isInstance)) + .toList() + ); + return stopLocations; + } + private void invalidateIndex() { this.index = null; } 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 839fd3c6d61..28024656e28 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -3081,6 +3081,12 @@ enum PlanAccessMode { """ BICYCLE_RENTAL """ + Driving to a stop and boarding a vehicle with the car. + Access can use driving only if the mode used for transfers + and egress is also `CAR`. + """ + CAR + """ Getting dropped off by a car to a location that is accessible with a car. Note, this can include walking after the drop-off. """ @@ -3190,6 +3196,11 @@ enum PlanEgressMode { """ BICYCLE_RENTAL """ + Driving from a stop to the destination. Egress can use driving only if the mode + used for access and transfers is also `CAR`. + """ + CAR + """ Getting picked up by a car from a location that is accessible with a car. Note, this can include walking before the pickup. """ @@ -3227,6 +3238,11 @@ enum PlanTransferMode { cycling if the mode used for access and egress is also `BICYCLE`. """ BICYCLE + """ + Driving between transit vehicles. Transfers can only use driving if the mode + used for access and egress is also `CAR`. + """ + CAR "Walking between transit vehicles (typically between stops)." WALK } diff --git a/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/BikesAllowedMapperTest.java b/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/BikesAllowedMapperTest.java index 5ed267f1f4f..035d3605a21 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/BikesAllowedMapperTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/BikesAllowedMapperTest.java @@ -2,20 +2,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.Arrays; import org.junit.jupiter.api.Test; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLBikesAllowed; import org.opentripplanner.transit.model.network.BikeAccess; class BikesAllowedMapperTest { @Test void mapping() { - Arrays - .stream(BikeAccess.values()) - .filter(ba -> ba != BikeAccess.UNKNOWN) - .forEach(d -> { - var mapped = BikesAllowedMapper.map(d); - assertEquals(d.toString(), mapped.toString()); - }); + assertEquals(GraphQLBikesAllowed.NO_INFORMATION, BikesAllowedMapper.map(BikeAccess.UNKNOWN)); + assertEquals(GraphQLBikesAllowed.NOT_ALLOWED, BikesAllowedMapper.map(BikeAccess.NOT_ALLOWED)); + assertEquals(GraphQLBikesAllowed.ALLOWED, BikesAllowedMapper.map(BikeAccess.ALLOWED)); } } diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java index f9957a09b95..3973a7e506a 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java @@ -1,6 +1,6 @@ package org.opentripplanner.graph_builder.module; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -9,12 +9,14 @@ import static org.opentripplanner.street.model.StreetTraversalPermission.PEDESTRIAN; import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id; +import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; import org.opentripplanner.ext.flex.trip.UnscheduledTrip; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.model.StopTime; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.street.model._data.StreetModelForTest; import org.opentripplanner.street.model.edge.Edge; @@ -23,7 +25,15 @@ import org.opentripplanner.street.model.vertex.TransitStopVertex; import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; import org.opentripplanner.transit.model.framework.Deduplicator; +import org.opentripplanner.transit.model.network.CarAccess; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.network.StopPattern; +import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.opentripplanner.transit.service.StopModel; import org.opentripplanner.transit.service.TimetableRepository; @@ -41,7 +51,7 @@ void linkingIsIdempotent() { module.buildGraph(); assertTrue(model.stopVertex().isConnectedToGraph()); - assertEquals(1, model.stopVertex().getOutgoing().size()); + assertThat(model.stopVertex().getOutgoing()).hasSize(1); } @Test @@ -53,7 +63,7 @@ void linkRegularStop() { assertTrue(model.stopVertex().isConnectedToGraph()); - assertEquals(1, model.stopVertex().getOutgoing().size()); + assertThat(model.stopVertex().getOutgoing()).hasSize(1); var outgoing = model.outgoingLinks().getFirst(); assertInstanceOf(StreetTransitStopLink.class, outgoing); @@ -77,7 +87,7 @@ void linkFlexStop() { assertTrue(model.stopVertex().isConnectedToGraph()); // stop is used by a flex trip, needs to be linked to both the walk and car edge - assertEquals(2, model.stopVertex().getOutgoing().size()); + assertThat(model.stopVertex().getOutgoing()).hasSize(2); var linkToWalk = model.outgoingLinks().getFirst(); SplitterVertex walkSplit = (SplitterVertex) linkToWalk.getToVertex(); @@ -92,6 +102,37 @@ void linkFlexStop() { }); } + @Test + void linkCarsAllowedStop() { + var model = new TestModel(); + var carsAllowedTrip = TimetableRepositoryForTest + .of() + .trip("carsAllowedTrip") + .withCarsAllowed(CarAccess.ALLOWED) + .build(); + model.withCarsAllowedTrip(carsAllowedTrip, model.stop()); + + var module = model.streetLinkerModule(); + + module.buildGraph(); + + assertTrue(model.stopVertex().isConnectedToGraph()); + + // Because the stop is used by a carsAllowed trip it needs to be linked to both the walk and car edge + assertThat(model.stopVertex().getOutgoing()).hasSize(2); + var linkToWalk = model.outgoingLinks().getFirst(); + SplitterVertex walkSplit = (SplitterVertex) linkToWalk.getToVertex(); + + assertTrue(walkSplit.isConnectedToWalkingEdge()); + assertFalse(walkSplit.isConnectedToDriveableEdge()); + + var linkToCar = model.outgoingLinks().getLast(); + SplitterVertex carSplit = (SplitterVertex) linkToCar.getToVertex(); + + assertFalse(carSplit.isConnectedToWalkingEdge()); + assertTrue(carSplit.isConnectedToDriveableEdge()); + } + private static class TestModel { private final TransitStopVertex stopVertex; @@ -155,5 +196,33 @@ public RegularStop stop() { public void withFlexTrip(UnscheduledTrip flexTrip) { timetableRepository.addFlexTrip(flexTrip.getId(), flexTrip); } + + public void withCarsAllowedTrip(Trip trip, StopLocation... stops) { + Route route = TimetableRepositoryForTest.route("carsAllowedRoute").build(); + var stopTimes = Arrays + .stream(stops) + .map(s -> { + var stopTime = new StopTime(); + stopTime.setStop(s); + stopTime.setArrivalTime(30); + stopTime.setDepartureTime(60); + stopTime.setTrip(trip); + return stopTime; + }) + .toList(); + StopPattern stopPattern = new StopPattern(stopTimes); + RealTimeTripTimes tripTimes = TripTimesFactory.tripTimes( + trip, + stopTimes, + timetableRepository.getDeduplicator() + ); + TripPattern tripPattern = TimetableRepositoryForTest + .tripPattern("carsAllowedTripPattern", route) + .withStopPattern(stopPattern) + .withScheduledTimeTableBuilder(builder -> builder.addTripTimes(tripTimes)) + .build(); + + timetableRepository.addTripPattern(tripPattern.getId(), tripPattern); + } } } diff --git a/application/src/test/java/org/opentripplanner/gtfs/mapping/CarAccessMapperTest.java b/application/src/test/java/org/opentripplanner/gtfs/mapping/CarAccessMapperTest.java new file mode 100644 index 00000000000..a312d9967d6 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/gtfs/mapping/CarAccessMapperTest.java @@ -0,0 +1,26 @@ +package org.opentripplanner.gtfs.mapping; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.onebusaway.gtfs.model.Route; +import org.onebusaway.gtfs.model.Trip; +import org.opentripplanner.transit.model.network.CarAccess; + +public class CarAccessMapperTest { + + private static final int CARS_ALLOWED = 1; + private static final int CARS_NOT_ALLOWED = 2; + + @Test + public void testTripProvidedValues() { + Trip trip = new Trip(); + assertEquals(CarAccess.UNKNOWN, CarAccessMapper.mapForTrip(trip)); + + trip.setCarsAllowed(CARS_ALLOWED); + assertEquals(CarAccess.ALLOWED, CarAccessMapper.mapForTrip(trip)); + + trip.setCarsAllowed(CARS_NOT_ALLOWED); + assertEquals(CarAccess.NOT_ALLOWED, CarAccessMapper.mapForTrip(trip)); + } +} diff --git a/application/src/test/java/org/opentripplanner/gtfs/mapping/TripMapperTest.java b/application/src/test/java/org/opentripplanner/gtfs/mapping/TripMapperTest.java index 581cff59e79..4132b73826c 100644 --- a/application/src/test/java/org/opentripplanner/gtfs/mapping/TripMapperTest.java +++ b/application/src/test/java/org/opentripplanner/gtfs/mapping/TripMapperTest.java @@ -19,6 +19,7 @@ import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.transit.model.basic.Accessibility; import org.opentripplanner.transit.model.network.BikeAccess; +import org.opentripplanner.transit.model.network.CarAccess; import org.opentripplanner.transit.model.timetable.Direction; public class TripMapperTest { @@ -26,6 +27,7 @@ public class TripMapperTest { private static final String FEED_ID = "FEED"; private static final AgencyAndId AGENCY_AND_ID = new AgencyAndId("A", "1"); private static final int BIKES_ALLOWED = 1; + private static final int CARS_ALLOWED = 1; private static final String BLOCK_ID = "Block Id"; private static final int DIRECTION_ID = 1; private static final String TRIP_HEADSIGN = "Trip Headsign"; @@ -52,6 +54,7 @@ private static TripMapper defaultTripMapper() { TRIP.setId(AGENCY_AND_ID); TRIP.setBikesAllowed(BIKES_ALLOWED); + TRIP.setCarsAllowed(CARS_ALLOWED); TRIP.setBlockId(BLOCK_ID); TRIP.setDirectionId(Integer.toString(DIRECTION_ID)); TRIP.setRoute(data.route); @@ -83,6 +86,7 @@ void testMap() throws Exception { assertEquals(TRIP_SHORT_NAME, result.getShortName()); assertEquals(Accessibility.POSSIBLE, result.getWheelchairBoarding()); assertEquals(BikeAccess.ALLOWED, result.getBikesAllowed()); + assertEquals(CarAccess.ALLOWED, result.getCarsAllowed()); } @Test @@ -104,6 +108,7 @@ void testMapWithNulls() throws Exception { assertEquals(Direction.UNKNOWN, result.getDirection()); assertEquals(Accessibility.NO_INFORMATION, result.getWheelchairBoarding()); assertEquals(BikeAccess.UNKNOWN, result.getBikesAllowed()); + assertEquals(CarAccess.UNKNOWN, result.getCarsAllowed()); } /** Mapping the same object twice, should return the same instance. */ diff --git a/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RouteRequestTransitDataProviderFilterTest.java b/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RouteRequestTransitDataProviderFilterTest.java index 8b52f1b58fa..ea4b7174e79 100644 --- a/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RouteRequestTransitDataProviderFilterTest.java +++ b/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RouteRequestTransitDataProviderFilterTest.java @@ -1,5 +1,6 @@ package org.opentripplanner.routing.algorithm.raptoradapter.transit.request; +import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -34,6 +35,7 @@ import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.BikeAccess; +import org.opentripplanner.transit.model.network.CarAccess; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.RouteBuilder; import org.opentripplanner.transit.model.network.RoutingTripPattern; @@ -107,6 +109,7 @@ void testWheelchairAccess(Accessibility wheelchair, WheelchairPreferences access .getRoutingTripPattern(); var filter = new RouteRequestTransitDataProviderFilter( + false, false, true, accessibility, @@ -157,6 +160,7 @@ void testRealtimeCancelledStops(boolean includeRealtimeCancellations) { .getRoutingTripPattern(); var filter = new RouteRequestTransitDataProviderFilter( + false, false, false, DEFAULT_ACCESSIBILITY, @@ -202,6 +206,7 @@ void notFilteringExpectedTripPatternForDateTest() { TripPatternForDate tripPatternForDate = createTestTripPatternForDate(); var filter = new RouteRequestTransitDataProviderFilter( + false, false, false, DEFAULT_ACCESSIBILITY, @@ -221,6 +226,7 @@ void bannedRouteFilteringTest() { TripPatternForDate tripPatternForDate = createTestTripPatternForDate(); var filter = new RouteRequestTransitDataProviderFilter( + false, false, false, DEFAULT_ACCESSIBILITY, @@ -246,6 +252,7 @@ void bannedTripFilteringTest() { TRIP_ID, ROUTE, BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, null, Accessibility.NOT_POSSIBLE, @@ -253,6 +260,7 @@ void bannedTripFilteringTest() { ); var filter = new RouteRequestTransitDataProviderFilter( + false, false, false, DEFAULT_ACCESSIBILITY, @@ -281,6 +289,7 @@ void matchModeFilterAndBannedAgencyFilter() { ); var filter = new RouteRequestTransitDataProviderFilter( + false, false, false, DEFAULT_ACCESSIBILITY, @@ -316,6 +325,7 @@ void matchCombinedModesAndBannedAgencyFilter() { ); var filter = new RouteRequestTransitDataProviderFilter( + false, false, false, DEFAULT_ACCESSIBILITY, @@ -347,6 +357,7 @@ void matchSelectedAgencyExcludedSubMode() { ); var filter = new RouteRequestTransitDataProviderFilter( + false, false, false, DEFAULT_ACCESSIBILITY, @@ -389,6 +400,7 @@ void transitModeFilteringTest() { TRIP_ID, ROUTE, BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, TransmodelTransportSubmode.LOCAL_BUS.getValue(), Accessibility.NOT_POSSIBLE, @@ -415,6 +427,7 @@ void notFilteringExpectedTripTimesTest() { TRIP_ID, ROUTE, BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, null, Accessibility.NOT_POSSIBLE, @@ -422,6 +435,7 @@ void notFilteringExpectedTripTimesTest() { ); var filter = new RouteRequestTransitDataProviderFilter( + false, false, false, DEFAULT_ACCESSIBILITY, @@ -442,6 +456,36 @@ void bikesAllowedFilteringTest() { TRIP_ID, ROUTE, BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, + TransitMode.BUS, + null, + Accessibility.NOT_POSSIBLE, + null + ); + + var filter = new RouteRequestTransitDataProviderFilter( + true, + false, + true, + WheelchairPreferences.DEFAULT, + false, + false, + Set.of(), + List.of(AllowAllTransitFilter.of()) + ); + + boolean valid = filter.tripTimesPredicate(tripTimes, true); + + assertFalse(valid); + } + + @Test + void carsAllowedFilteringTest() { + TripTimes tripTimes = createTestTripTimes( + TRIP_ID, + ROUTE, + BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, null, Accessibility.NOT_POSSIBLE, @@ -449,6 +493,7 @@ void bikesAllowedFilteringTest() { ); var filter = new RouteRequestTransitDataProviderFilter( + false, true, true, WheelchairPreferences.DEFAULT, @@ -469,6 +514,7 @@ void removeInaccessibleTrip() { TRIP_ID, ROUTE, BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, null, Accessibility.NOT_POSSIBLE, @@ -476,6 +522,7 @@ void removeInaccessibleTrip() { ); var filter = new RouteRequestTransitDataProviderFilter( + false, false, true, WheelchairPreferences.DEFAULT, @@ -496,6 +543,7 @@ void keepAccessibleTrip() { TRIP_ID, ROUTE, BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, null, Accessibility.POSSIBLE, @@ -503,6 +551,7 @@ void keepAccessibleTrip() { ); var filter = new RouteRequestTransitDataProviderFilter( + false, false, true, WheelchairPreferences.DEFAULT, @@ -523,6 +572,7 @@ void keepRealTimeAccessibleTrip() { TRIP_ID, ROUTE, BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, null, Accessibility.NOT_POSSIBLE, @@ -530,6 +580,7 @@ void keepRealTimeAccessibleTrip() { ); var filter = new RouteRequestTransitDataProviderFilter( + false, false, true, WheelchairPreferences.DEFAULT, @@ -552,6 +603,7 @@ void includePlannedCancellationsTest() { TRIP_ID, ROUTE, BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, null, Accessibility.NOT_POSSIBLE, @@ -561,6 +613,7 @@ void includePlannedCancellationsTest() { TRIP_ID, ROUTE, BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, null, Accessibility.NOT_POSSIBLE, @@ -569,6 +622,7 @@ void includePlannedCancellationsTest() { // Given var filter1 = new RouteRequestTransitDataProviderFilter( + false, false, false, WheelchairPreferences.DEFAULT, @@ -590,6 +644,7 @@ void includePlannedCancellationsTest() { // Given var filter2 = new RouteRequestTransitDataProviderFilter( + false, false, false, DEFAULT_ACCESSIBILITY, @@ -616,6 +671,7 @@ void includeRealtimeCancellationsTest() { TRIP_ID, ROUTE, BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, null, Accessibility.NOT_POSSIBLE, @@ -626,6 +682,7 @@ void includeRealtimeCancellationsTest() { TRIP_ID, ROUTE, BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, null, Accessibility.NOT_POSSIBLE, @@ -635,6 +692,7 @@ void includeRealtimeCancellationsTest() { // Given var filter1 = new RouteRequestTransitDataProviderFilter( + false, false, false, WheelchairPreferences.DEFAULT, @@ -656,6 +714,7 @@ void includeRealtimeCancellationsTest() { // Given var filter2 = new RouteRequestTransitDataProviderFilter( + false, false, false, DEFAULT_ACCESSIBILITY, @@ -712,12 +771,64 @@ void testBikesAllowed() { ); } + @Test + void testCarsAllowed() { + TripTimes tripTimesCarsAllowed = createTestTripTimes( + TRIP_ID, + ROUTE, + BikeAccess.UNKNOWN, + CarAccess.ALLOWED, + TransitMode.FERRY, + null, + Accessibility.NO_INFORMATION, + TripAlteration.PLANNED + ); + + TripTimes tripTimesCarsNotAllowed = createTestTripTimes( + TRIP_ID, + ROUTE, + BikeAccess.UNKNOWN, + CarAccess.NOT_ALLOWED, + TransitMode.FERRY, + null, + Accessibility.NO_INFORMATION, + TripAlteration.PLANNED + ); + + TripTimes tripTimesCarsUnknown = createTestTripTimes( + TRIP_ID, + ROUTE, + BikeAccess.UNKNOWN, + CarAccess.UNKNOWN, + TransitMode.FERRY, + null, + Accessibility.NO_INFORMATION, + TripAlteration.PLANNED + ); + + RouteRequestTransitDataProviderFilter filter = new RouteRequestTransitDataProviderFilter( + false, + true, + false, + DEFAULT_ACCESSIBILITY, + false, + false, + Set.of(), + List.of(AllowAllTransitFilter.of()) + ); + + assertThat(filter.tripTimesPredicate(tripTimesCarsAllowed, false)).isTrue(); + assertThat(filter.tripTimesPredicate(tripTimesCarsNotAllowed, false)).isFalse(); + assertThat(filter.tripTimesPredicate(tripTimesCarsUnknown, false)).isFalse(); + } + @Test void multipleFilteringTest() { TripTimes matchingTripTimes = createTestTripTimes( TRIP_ID, ROUTE, BikeAccess.ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, null, Accessibility.POSSIBLE, @@ -727,6 +838,7 @@ void multipleFilteringTest() { TRIP_ID, ROUTE, BikeAccess.ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.RAIL, null, Accessibility.POSSIBLE, @@ -736,6 +848,7 @@ void multipleFilteringTest() { TRIP_ID, ROUTE, BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.RAIL, null, Accessibility.POSSIBLE, @@ -745,6 +858,7 @@ void multipleFilteringTest() { TRIP_ID, ROUTE, BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.RAIL, null, Accessibility.NOT_POSSIBLE, @@ -754,6 +868,7 @@ void multipleFilteringTest() { TRIP_ID, ROUTE, BikeAccess.ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, null, Accessibility.NOT_POSSIBLE, @@ -763,6 +878,7 @@ void multipleFilteringTest() { TRIP_ID, ROUTE, BikeAccess.ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, null, Accessibility.POSSIBLE, @@ -771,6 +887,7 @@ void multipleFilteringTest() { var filter = new RouteRequestTransitDataProviderFilter( true, + false, true, DEFAULT_ACCESSIBILITY, false, @@ -793,6 +910,7 @@ private boolean validateModesOnTripTimes( TripTimes tripTimes ) { var filter = new RouteRequestTransitDataProviderFilter( + false, false, false, DEFAULT_ACCESSIBILITY, @@ -870,6 +988,7 @@ private RealTimeTripTimes createTestTripTimes( FeedScopedId tripId, Route route, BikeAccess bikeAccess, + CarAccess carAccess, TransitMode mode, String submode, Accessibility wheelchairBoarding, @@ -881,6 +1000,7 @@ private RealTimeTripTimes createTestTripTimes( .withMode(mode) .withNetexSubmode(submode) .withBikesAllowed(bikeAccess) + .withCarsAllowed(carAccess) .withWheelchairBoarding(wheelchairBoarding) .withNetexAlteration(tripAlteration) .build(); @@ -899,6 +1019,7 @@ private TripTimes createTestTripTimesWithSubmode(String submode) { TRIP_ID, ROUTE, BikeAccess.NOT_ALLOWED, + CarAccess.NOT_ALLOWED, TransitMode.BUS, submode, Accessibility.NOT_POSSIBLE, diff --git a/application/src/test/java/org/opentripplanner/transit/model/timetable/TripTest.java b/application/src/test/java/org/opentripplanner/transit/model/timetable/TripTest.java index 5d50548564a..441ce74927a 100644 --- a/application/src/test/java/org/opentripplanner/transit/model/timetable/TripTest.java +++ b/application/src/test/java/org/opentripplanner/transit/model/timetable/TripTest.java @@ -12,6 +12,7 @@ import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.BikeAccess; +import org.opentripplanner.transit.model.network.CarAccess; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.organization.Operator; @@ -24,6 +25,7 @@ class TripTest { private static final Direction DIRECTION = Direction.INBOUND; public static final NonLocalizedString HEAD_SIGN = new NonLocalizedString("head sign"); private static final BikeAccess BIKE_ACCESS = BikeAccess.ALLOWED; + private static final CarAccess CAR_ACCESS = CarAccess.ALLOWED; private static final TransitMode TRANSIT_MODE = TransitMode.BUS; private static final String BLOCK_ID = "blockId"; private static final TripAlteration TRIP_ALTERATION = TripAlteration.CANCELLATION; @@ -43,6 +45,7 @@ class TripTest { .withDirection(DIRECTION) .withHeadsign(HEAD_SIGN) .withBikesAllowed(BIKE_ACCESS) + .withCarsAllowed(CAR_ACCESS) .withMode(TRANSIT_MODE) .withGtfsBlockId(BLOCK_ID) .withNetexAlteration(TRIP_ALTERATION) @@ -84,6 +87,7 @@ void copy() { assertEquals(DIRECTION, copy.getDirection()); assertEquals(HEAD_SIGN, copy.getHeadsign()); assertEquals(BIKE_ACCESS, copy.getBikesAllowed()); + assertEquals(CAR_ACCESS, copy.getCarsAllowed()); assertEquals(TRANSIT_MODE, copy.getMode()); assertEquals(BLOCK_ID, copy.getGtfsBlockId()); assertEquals(TRIP_ALTERATION, copy.getNetexAlteration()); @@ -110,6 +114,7 @@ void sameAs() { assertFalse(subject.sameAs(subject.copy().withDirection(Direction.OUTBOUND).build())); assertFalse(subject.sameAs(subject.copy().withHeadsign(new NonLocalizedString("X")).build())); assertFalse(subject.sameAs(subject.copy().withBikesAllowed(BikeAccess.NOT_ALLOWED).build())); + assertFalse(subject.sameAs(subject.copy().withCarsAllowed(CarAccess.NOT_ALLOWED).build())); assertFalse(subject.sameAs(subject.copy().withMode(TransitMode.RAIL).build())); assertFalse(subject.sameAs(subject.copy().withGtfsBlockId("X").build())); assertFalse( 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 f15bc45b2e8..8a0e457396e 100644 --- a/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json +++ b/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json @@ -22,7 +22,460 @@ "id" : "background", "type" : "raster", "source" : "background", - "minzoom" : 0 + "minzoom" : 0, + "metadata" : { + "group" : "Other" + } + }, + { + "id" : "NONE", + "source" : "vectorSource", + "source-layer" : "edges", + "type" : "line", + "minzoom" : 6, + "maxzoom" : 23, + "paint" : { + "line-color" : "#000", + "line-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.2, + 23, + 8.0 + ], + "line-offset" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.3, + 23, + 6.0 + ] + }, + "filter" : [ + "==", + "permission", + "NONE" + ], + "layout" : { + "line-cap" : "round", + "visibility" : "none" + }, + "metadata" : { + "group" : "Traversal permissions" + } + }, + { + "id" : "PEDESTRIAN", + "source" : "vectorSource", + "source-layer" : "edges", + "type" : "line", + "minzoom" : 6, + "maxzoom" : 23, + "paint" : { + "line-color" : "#2ba812", + "line-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.2, + 23, + 8.0 + ], + "line-offset" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.3, + 23, + 6.0 + ] + }, + "filter" : [ + "==", + "permission", + "PEDESTRIAN" + ], + "layout" : { + "line-cap" : "round", + "visibility" : "none" + }, + "metadata" : { + "group" : "Traversal permissions" + } + }, + { + "id" : "BICYCLE", + "source" : "vectorSource", + "source-layer" : "edges", + "type" : "line", + "minzoom" : 6, + "maxzoom" : 23, + "paint" : { + "line-color" : "#10d3b6", + "line-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.2, + 23, + 8.0 + ], + "line-offset" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.3, + 23, + 6.0 + ] + }, + "filter" : [ + "==", + "permission", + "BICYCLE" + ], + "layout" : { + "line-cap" : "round", + "visibility" : "none" + }, + "metadata" : { + "group" : "Traversal permissions" + } + }, + { + "id" : "PEDESTRIAN_AND_BICYCLE", + "source" : "vectorSource", + "source-layer" : "edges", + "type" : "line", + "minzoom" : 6, + "maxzoom" : 23, + "paint" : { + "line-color" : "#10d3b6", + "line-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.2, + 23, + 8.0 + ], + "line-offset" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.3, + 23, + 6.0 + ] + }, + "filter" : [ + "==", + "permission", + "PEDESTRIAN_AND_BICYCLE" + ], + "layout" : { + "line-cap" : "round", + "visibility" : "none" + }, + "metadata" : { + "group" : "Traversal permissions" + } + }, + { + "id" : "CAR", + "source" : "vectorSource", + "source-layer" : "edges", + "type" : "line", + "minzoom" : 6, + "maxzoom" : 23, + "paint" : { + "line-color" : "#f92e13", + "line-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.2, + 23, + 8.0 + ], + "line-offset" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.3, + 23, + 6.0 + ] + }, + "filter" : [ + "==", + "permission", + "CAR" + ], + "layout" : { + "line-cap" : "round", + "visibility" : "none" + }, + "metadata" : { + "group" : "Traversal permissions" + } + }, + { + "id" : "PEDESTRIAN_AND_CAR", + "source" : "vectorSource", + "source-layer" : "edges", + "type" : "line", + "minzoom" : 6, + "maxzoom" : 23, + "paint" : { + "line-color" : "#e25f8f", + "line-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.2, + 23, + 8.0 + ], + "line-offset" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.3, + 23, + 6.0 + ] + }, + "filter" : [ + "==", + "permission", + "PEDESTRIAN_AND_CAR" + ], + "layout" : { + "line-cap" : "round", + "visibility" : "none" + }, + "metadata" : { + "group" : "Traversal permissions" + } + }, + { + "id" : "BICYCLE_AND_CAR", + "source" : "vectorSource", + "source-layer" : "edges", + "type" : "line", + "minzoom" : 6, + "maxzoom" : 23, + "paint" : { + "line-color" : "#e25f8f", + "line-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.2, + 23, + 8.0 + ], + "line-offset" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.3, + 23, + 6.0 + ] + }, + "filter" : [ + "==", + "permission", + "BICYCLE_AND_CAR" + ], + "layout" : { + "line-cap" : "round", + "visibility" : "none" + }, + "metadata" : { + "group" : "Traversal permissions" + } + }, + { + "id" : "ALL", + "source" : "vectorSource", + "source-layer" : "edges", + "type" : "line", + "minzoom" : 6, + "maxzoom" : 23, + "paint" : { + "line-color" : "#adb2b0", + "line-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.2, + 23, + 8.0 + ], + "line-offset" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.3, + 23, + 6.0 + ] + }, + "filter" : [ + "==", + "permission", + "ALL" + ], + "layout" : { + "line-cap" : "round", + "visibility" : "none" + }, + "metadata" : { + "group" : "Traversal permissions" + } + }, + { + "id" : "permission-text", + "source" : "vectorSource", + "source-layer" : "edges", + "type" : "symbol", + "minzoom" : 17, + "maxzoom" : 23, + "paint" : { + "text-color" : "#000", + "text-halo-color" : "#fff", + "text-halo-blur" : 4, + "text-halo-width" : 3 + }, + "filter" : [ + "in", + "class", + "StreetEdge", + "AreaEdge", + "EscalatorEdge", + "PathwayEdge", + "ElevatorHopEdge", + "TemporaryPartialStreetEdge", + "TemporaryFreeEdge" + ], + "layout" : { + "symbol-placement" : "line-center", + "symbol-spacing" : 1000, + "text-field" : "{permission}", + "text-font" : [ + "KlokanTech Noto Sans Regular" + ], + "text-size" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 6.0, + 24, + 12.0 + ], + "text-max-width" : 100, + "text-keep-upright" : true, + "text-rotation-alignment" : "map", + "text-overlap" : "never", + "text-offset" : [ + 0, + 1.0 + ], + "visibility" : "none" + }, + "metadata" : { + "group" : "Traversal permissions" + } }, { "id" : "edge", @@ -33,32 +486,32 @@ "maxzoom" : 23, "paint" : { "line-color" : "#f21d52", - "line-width" : { - "base" : 1.3, - "stops" : [ - [ - 13, - 0.5 - ], - [ - 23, - 10.0 - ] - ] - }, - "line-offset" : { - "base" : 1.3, - "stops" : [ - [ - 13, - 0.3 - ], - [ - 23, - 6.0 - ] - ] - } + "line-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.2, + 23, + 8.0 + ], + "line-offset" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.3, + 23, + 6.0 + ] }, "filter" : [ "in", @@ -74,6 +527,9 @@ "layout" : { "line-cap" : "round", "visibility" : "none" + }, + "metadata" : { + "group" : "Edges" } }, { @@ -101,29 +557,33 @@ "TemporaryFreeEdge" ], "layout" : { - "symbol-placement" : "line", - "symbol-spacing" : 500, + "symbol-placement" : "line-center", + "symbol-spacing" : 1000, "text-field" : "{name}", "text-font" : [ "KlokanTech Noto Sans Regular" ], - "text-size" : { - "base" : 14.0, - "stops" : [ - [ - 14, - 12.0 - ], - [ - 20, - 14.0 - ] - ] - }, - "text-max-width" : 5, + "text-size" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 6.0, + 24, + 12.0 + ], + "text-max-width" : 100, "text-keep-upright" : true, "text-rotation-alignment" : "map", + "text-overlap" : "never", "visibility" : "none" + }, + "metadata" : { + "group" : "Edges" } }, { @@ -135,32 +595,32 @@ "maxzoom" : 23, "paint" : { "line-color" : "#22DD9E", - "line-width" : { - "base" : 1.3, - "stops" : [ - [ - 13, - 0.5 - ], - [ - 23, - 10.0 - ] - ] - }, - "line-offset" : { - "base" : 1.3, - "stops" : [ - [ - 13, - 0.3 - ], - [ - 23, - 6.0 - ] - ] - } + "line-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.2, + 23, + 8.0 + ], + "line-offset" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.3, + 23, + 6.0 + ] }, "filter" : [ "in", @@ -175,6 +635,9 @@ "layout" : { "line-cap" : "round", "visibility" : "none" + }, + "metadata" : { + "group" : "Edges" } }, { @@ -186,36 +649,39 @@ "maxzoom" : 23, "paint" : { "circle-stroke-color" : "#140d0e", - "circle-stroke-width" : { - "base" : 1.0, - "stops" : [ - [ - 15, - 0.2 - ], - [ - 23, - 3.0 - ] - ] - }, - "circle-radius" : { - "base" : 1.0, - "stops" : [ - [ - 15, - 1.0 - ], - [ - 23, - 7.0 - ] - ] - }, + "circle-stroke-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + 0.2, + 23, + 3.0 + ], + "circle-radius" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + 1.0, + 23, + 7.0 + ], "circle-color" : "#BC55F2" }, "layout" : { "visibility" : "none" + }, + "metadata" : { + "group" : "Vertices" } }, { @@ -227,32 +693,32 @@ "maxzoom" : 23, "paint" : { "circle-stroke-color" : "#140d0e", - "circle-stroke-width" : { - "base" : 1.0, - "stops" : [ - [ - 15, - 0.2 - ], - [ - 23, - 3.0 - ] - ] - }, - "circle-radius" : { - "base" : 1.0, - "stops" : [ - [ - 13, - 1.4 - ], - [ - 23, - 10.0 - ] - ] - }, + "circle-stroke-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15, + 0.2, + 23, + 3.0 + ], + "circle-radius" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 1.4, + 23, + 10.0 + ], "circle-color" : "#136b04" }, "filter" : [ @@ -262,6 +728,9 @@ ], "layout" : { "visibility" : "none" + }, + "metadata" : { + "group" : "Vertices" } }, { @@ -275,6 +744,9 @@ "fill-color" : "#22DD9E", "fill-opacity" : 0.5, "fill-outline-color" : "#140d0e" + }, + "metadata" : { + "group" : "Stops" } }, { @@ -288,6 +760,9 @@ "fill-color" : "#22DD9E", "fill-opacity" : 0.5, "fill-outline-color" : "#140d0e" + }, + "metadata" : { + "group" : "Stops" } }, { @@ -299,33 +774,36 @@ "maxzoom" : 23, "paint" : { "circle-stroke-color" : "#140d0e", - "circle-stroke-width" : { - "base" : 1.0, - "stops" : [ - [ - 11, - 0.5 - ], - [ - 23, - 5.0 - ] - ] - }, - "circle-radius" : { - "base" : 1.0, - "stops" : [ - [ - 11, - 0.5 - ], - [ - 23, - 10.0 - ] - ] - }, + "circle-stroke-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 11, + 0.5, + 23, + 5.0 + ], + "circle-radius" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 11, + 0.5, + 23, + 10.0 + ], "circle-color" : "#fcf9fa" + }, + "metadata" : { + "group" : "Stops" } } ], diff --git a/client/src/components/MapView/LayerControl.tsx b/client/src/components/MapView/LayerControl.tsx index 1517a9ce7c8..d6be2d641d7 100644 --- a/client/src/components/MapView/LayerControl.tsx +++ b/client/src/components/MapView/LayerControl.tsx @@ -1,6 +1,6 @@ import type { ControlPosition } from 'react-map-gl'; import { useControl } from 'react-map-gl'; -import { IControl, Map } from 'maplibre-gl'; +import { IControl, Map, TypedStyleLayer } from 'maplibre-gl'; type LayerControlProps = { position: ControlPosition; @@ -24,10 +24,11 @@ class LayerControl implements IControl { this.container.removeChild(this.container.firstChild); } - const title = document.createElement('h6'); + const title = document.createElement('h4'); title.textContent = 'Debug layers'; this.container.appendChild(title); + const groups: Record = {}; map .getLayersOrder() .map((l) => map.getLayer(l)) @@ -38,28 +39,22 @@ class LayerControl implements IControl { .reverse() .forEach((layer) => { if (layer) { - const div = document.createElement('div'); - const input = document.createElement('input'); - input.type = 'checkbox'; - input.value = layer.id; - input.id = layer.id; - input.onchange = (e) => { - e.preventDefault(); - e.stopPropagation(); - - if (this.layerVisible(map, layer)) { - map.setLayoutProperty(layer.id, 'visibility', 'none'); - } else { - map.setLayoutProperty(layer.id, 'visibility', 'visible'); - } - }; - input.checked = this.layerVisible(map, layer); - const label = document.createElement('label'); - label.textContent = layer.id; - label.htmlFor = layer.id; - div.appendChild(input); - div.appendChild(label); - this.container.appendChild(div); + const meta: { group: string } = layer.metadata as { group: string }; + + let groupName: string = 'Misc'; + if (meta.group) { + groupName = meta.group; + } + + const layerDiv = this.buildLayerDiv(layer as TypedStyleLayer, map); + + if (groups[groupName]) { + groups[groupName]?.appendChild(layerDiv); + } else { + const groupDiv = this.buildGroupDiv(groupName, layerDiv); + groups[groupName] = groupDiv; + this.container.appendChild(groupDiv); + } } }); }); @@ -67,6 +62,58 @@ class LayerControl implements IControl { return this.container; } + private buildLayerDiv(layer: TypedStyleLayer, map: Map) { + const layerDiv = document.createElement('div'); + layerDiv.className = 'layer'; + const input = document.createElement('input'); + input.type = 'checkbox'; + input.value = layer.id; + input.id = layer.id; + input.onchange = (e) => { + e.preventDefault(); + e.stopPropagation(); + if (input.checked) { + map.setLayoutProperty(layer.id, 'visibility', 'visible'); + } else { + map.setLayoutProperty(layer.id, 'visibility', 'none'); + } + }; + input.checked = this.layerVisible(map, layer); + input.className = 'layer'; + const label = document.createElement('label'); + label.textContent = layer.id; + label.htmlFor = layer.id; + layerDiv.appendChild(input); + layerDiv.appendChild(label); + return layerDiv; + } + + private buildGroupDiv(groupName: string, layerDiv: HTMLDivElement) { + const groupDiv = document.createElement('div'); + groupDiv.className = 'group'; + + const groupInput = document.createElement('input'); + groupInput.onchange = () => { + groupDiv.querySelectorAll('input.layer').forEach((input) => { + input.checked = groupInput.checked; + const event = new Event('change'); + input.dispatchEvent(event); + }); + }; + groupInput.type = 'checkbox'; + groupInput.id = groupName; + + const groupLabel = document.createElement('label'); + groupLabel.textContent = groupName; + groupLabel.htmlFor = groupName; + groupLabel.className = 'group-label'; + + groupDiv.appendChild(groupInput); + groupDiv.appendChild(groupLabel); + groupDiv.appendChild(layerDiv); + return groupDiv; + } + private layerVisible(map: Map, layer: { id: string }) { return map.getLayoutProperty(layer.id, 'visibility') !== 'none'; } diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index 7b1ee58b902..73df12fe103 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -16,6 +16,7 @@ import { useRef, useState } from 'react'; import logo from '../../static/img/otp-logo.svg'; import GraphiQLRouteButton from './GraphiQLRouteButton.tsx'; import WheelchairAccessibleCheckBox from './WheelchairAccessibleCheckBox.tsx'; +import { SwapLocationsButton } from './SwapLocationsButton.tsx'; type SearchBarProps = { onRoute: () => void; @@ -38,6 +39,7 @@ export function SearchBar({ onRoute, tripQueryVariables, setTripQueryVariables, + diff --git a/client/src/components/SearchBar/SwapLocationsButton.tsx b/client/src/components/SearchBar/SwapLocationsButton.tsx new file mode 100644 index 00000000000..28d25f2fac7 --- /dev/null +++ b/client/src/components/SearchBar/SwapLocationsButton.tsx @@ -0,0 +1,24 @@ +import { TripQueryVariables } from '../../gql/graphql.ts'; +import icon from '../../static/img/swap.svg'; + +export function SwapLocationsButton({ + tripQueryVariables, + setTripQueryVariables, +}: { + tripQueryVariables: TripQueryVariables; + setTripQueryVariables: (tripQueryVariables: TripQueryVariables) => void; +}) { + const swapFromTo = () => { + setTripQueryVariables({ + ...tripQueryVariables, + from: tripQueryVariables.to, + to: tripQueryVariables.from, + }); + }; + + return ( + + ); +} diff --git a/client/src/static/img/swap.svg b/client/src/static/img/swap.svg new file mode 100644 index 00000000000..858e5e99831 --- /dev/null +++ b/client/src/static/img/swap.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/style.css b/client/src/style.css index 86310fd857d..c33f5dff711 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -67,6 +67,16 @@ margin-top: -2px; } +.search-bar .swap-from-to { + border: none; + background: none; + margin: 30px 0 auto 0; +} + +.search-bar .swap-from-to img { + width: 15px; +} + .itinerary-list-container { width: 36rem; overflow-y: auto; @@ -170,3 +180,20 @@ .maplibregl-ctrl-group.layer-select label { margin-left: 6px; } + +.maplibregl-ctrl-group.layer-select h4 { + font-size: 17px; +} + +.maplibregl-ctrl-group.layer-select .group-label { + font-size: 15px; + margin-bottom: 5px; +} + +.maplibregl-ctrl-group.layer-select div.group { + margin-top: 10px; +} + +.maplibregl-ctrl-group.layer-select div.layer { + margin-left: 17px; +}