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 289841a53d6..b061a00df50 100644 --- a/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java +++ b/application/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java @@ -47,6 +47,7 @@ public class DebugStyleSpec { private static final String MAGENTA = "#f21d52"; private static final String BRIGHT_GREEN = "#22DD9E"; private static final String DARK_GREEN = "#136b04"; + private static final String RED = "#fc0f2a"; private static final String PURPLE = "#BC55F2"; private static final String BLACK = "#140d0e"; @@ -84,6 +85,7 @@ public class DebugStyleSpec { StreetTraversalPermission.BICYCLE, StreetTraversalPermission.CAR, }; + private static final String WHEELCHAIR_GROUP = "Wheelchair accessibility"; static StyleSpec build( VectorSourceLayer regularStops, @@ -103,6 +105,7 @@ static StyleSpec build( allSources, ListUtils.combine( List.of(StyleBuilder.ofId("background").typeRaster().source(BACKGROUND_SOURCE).minZoom(0)), + wheelchair(edges), edges(edges), traversalPermissions(edges), noThruTraffic(edges), @@ -319,6 +322,35 @@ private static List permissionColors() { .toList(); } + private static List wheelchair(VectorSourceLayer edges) { + return List.of( + StyleBuilder + .ofId("wheelchair-accessible") + .vectorSourceLayer(edges) + .group(WHEELCHAIR_GROUP) + .typeLine() + .lineColor(DARK_GREEN) + .booleanFilter("wheelchairAccessible", true) + .lineWidth(LINE_WIDTH) + .lineOffset(LINE_OFFSET) + .minZoom(6) + .maxZoom(MAX_ZOOM) + .intiallyHidden(), + StyleBuilder + .ofId("wheelchair-inaccessible") + .vectorSourceLayer(edges) + .group(WHEELCHAIR_GROUP) + .typeLine() + .lineColor(RED) + .booleanFilter("wheelchairAccessible", false) + .lineWidth(LINE_WIDTH) + .lineOffset(LINE_OFFSET) + .minZoom(6) + .maxZoom(MAX_ZOOM) + .intiallyHidden() + ); + } + private static String permissionColor(StreetTraversalPermission p) { return switch (p) { case NONE -> BLACK; 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 d84ee0f533d..a70acbb8685 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 @@ -250,6 +250,14 @@ public final StyleBuilder edgeFilter(Class... classToFilter) { return filterClasses(classToFilter); } + /** + * Filter the entities by a boolean property. + */ + public final StyleBuilder booleanFilter(String propertyName, boolean value) { + filter = List.of("==", propertyName, value); + return this; + } + /** * Only apply the style to the given vertices. */ @@ -290,7 +298,7 @@ public JsonNode toJson() { private StyleBuilder filterClasses(Class... classToFilter) { var clazzes = Arrays.stream(classToFilter).map(Class::getSimpleName).toList(); - filter = ListUtils.combine(List.of("in", "class"), clazzes); + filter = new ArrayList<>(ListUtils.combine(List.of("in", "class"), clazzes)); return this; } diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/ParkingProcessor.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/ParkingProcessor.java index f372f0c82e2..d2bbbe7e27f 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/ParkingProcessor.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/ParkingProcessor.java @@ -312,7 +312,7 @@ private List createArtificialEntra ); } - private VehicleParking createVehicleParkingObjectFromOsmEntity( + VehicleParking createVehicleParkingObjectFromOsmEntity( boolean isCarParkAndRide, Coordinate coordinate, OsmWithTags entity, @@ -421,7 +421,7 @@ private OptionalInt parseCapacity(OsmWithTags element) { } private OptionalInt parseCapacity(OsmWithTags element, String capacityTag) { - return element.getTagAsInt( + return element.parseIntOrBoolean( capacityTag, v -> issueStore.add(new InvalidVehicleParkingCapacity(element, v)) ); diff --git a/application/src/main/java/org/opentripplanner/inspector/vector/edge/EdgePropertyMapper.java b/application/src/main/java/org/opentripplanner/inspector/vector/edge/EdgePropertyMapper.java index d6e2d7250ea..30763edca9e 100644 --- a/application/src/main/java/org/opentripplanner/inspector/vector/edge/EdgePropertyMapper.java +++ b/application/src/main/java/org/opentripplanner/inspector/vector/edge/EdgePropertyMapper.java @@ -32,7 +32,8 @@ private static List mapStreetEdge(StreetEdge se) { var props = Lists.newArrayList( kv("permission", streetPermissionAsString(se.getPermission())), kv("bicycleSafetyFactor", roundTo2Decimals(se.getBicycleSafetyFactor())), - kv("noThruTraffic", noThruTrafficAsString(se)) + kv("noThruTraffic", noThruTrafficAsString(se)), + kv("wheelchairAccessible", se.isWheelchairAccessible()) ); if (se.hasBogusName()) { props.addFirst(kv("name", "%s (generated)".formatted(se.getName().toString()))); diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmWithTags.java b/application/src/main/java/org/opentripplanner/osm/model/OsmWithTags.java index 3c9a3f537ee..67f737e4c79 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmWithTags.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmWithTags.java @@ -49,6 +49,7 @@ public class OsmWithTags { private static final Set LEVEL_TAGS = Set.of("level", "layer"); private static final Set DEFAULT_LEVEL = Set.of("0"); + private static final Consumer NO_OP = i -> {}; /* To save memory this is only created when an entity actually has tags. */ private Map tags; @@ -220,6 +221,33 @@ public OptionalInt getTagAsInt(String tag, Consumer errorHandler) { return OptionalInt.empty(); } + /** + * Some tags are allowed to have values like 55, "true" or "false". + *

+ * "true", "yes" is returned as 1. + *

+ * "false", "no" is returned as 0 + *

+ * Everything else is returned as an emtpy optional. + */ + public OptionalInt parseIntOrBoolean(String tag, Consumer errorHandler) { + var maybeInt = getTagAsInt(tag, NO_OP); + if (maybeInt.isPresent()) { + return maybeInt; + } else { + if (isTagTrue(tag)) { + return OptionalInt.of(1); + } else if (isTagFalse(tag)) { + return OptionalInt.of(0); + } else if (hasTag(tag)) { + errorHandler.accept(getTag(tag)); + return OptionalInt.empty(); + } else { + return OptionalInt.empty(); + } + } + } + /** * Checks is a tag contains the specified value. */ diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/ParkingProcessorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/ParkingProcessorTest.java new file mode 100644 index 00000000000..bfe5992399b --- /dev/null +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/ParkingProcessorTest.java @@ -0,0 +1,60 @@ +package org.opentripplanner.graph_builder.module.osm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner._support.geometry.Coordinates; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.osm.wayproperty.specifier.WayTestData; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.street.model._data.StreetModelForTest; +import org.opentripplanner.street.model.vertex.IntersectionVertex; + +class ParkingProcessorTest { + + private static final IntersectionVertex INTERSECTION_VERTEX = StreetModelForTest.intersectionVertex( + 1, + 1 + ); + private static final ParkingProcessor PROCESSOR = new ParkingProcessor( + new Graph(), + DataImportIssueStore.NOOP, + (n, w) -> INTERSECTION_VERTEX + ); + + @Test + void noWheelchairParking() { + var entity = WayTestData.parkAndRide(); + var parking = PROCESSOR.createVehicleParkingObjectFromOsmEntity( + true, + Coordinates.BERLIN, + entity, + I18NString.of("parking"), + List.of() + ); + + assertFalse(parking.hasWheelchairAccessibleCarPlaces()); + assertNull(parking.getCapacity().getWheelchairAccessibleCarSpaces()); + } + + @Test + void wheelchairParking() { + var entity = WayTestData.parkAndRide(); + entity.addTag("capacity:disabled", "yes"); + var parking = PROCESSOR.createVehicleParkingObjectFromOsmEntity( + true, + Coordinates.BERLIN, + entity, + I18NString.of("parking"), + List.of() + ); + + assertTrue(parking.hasWheelchairAccessibleCarPlaces()); + assertEquals(1, parking.getCapacity().getWheelchairAccessibleCarSpaces()); + } +} diff --git a/application/src/test/java/org/opentripplanner/osm/model/OsmWithTagsTest.java b/application/src/test/java/org/opentripplanner/osm/model/OsmWithTagsTest.java index 2f81cc55e3f..a89f8612040 100644 --- a/application/src/test/java/org/opentripplanner/osm/model/OsmWithTagsTest.java +++ b/application/src/test/java/org/opentripplanner/osm/model/OsmWithTagsTest.java @@ -8,8 +8,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.OptionalInt; import java.util.Set; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.opentripplanner.osm.wayproperty.specifier.WayTestData; public class OsmWithTagsTest { @@ -272,4 +276,26 @@ void fallbackName() { var namedTunnel = WayTestData.carTunnel(); assertFalse(namedTunnel.hasNoName()); } + + private static List parseIntOrBooleanCases() { + return List.of( + Arguments.of("true", OptionalInt.of(1)), + Arguments.of("yes", OptionalInt.of(1)), + Arguments.of("no", OptionalInt.of(0)), + Arguments.of("false", OptionalInt.of(0)), + Arguments.of("0", OptionalInt.of(0)), + Arguments.of("12", OptionalInt.of(12)), + Arguments.of("", OptionalInt.empty()) + ); + } + + @ParameterizedTest + @MethodSource("parseIntOrBooleanCases") + void parseIntOrBoolean(String value, OptionalInt expected) { + var way = new OsmWithTags(); + var key = "capacity:disabled"; + way.addTag(key, value); + var maybeInt = way.parseIntOrBoolean(key, i -> {}); + assertEquals(expected, maybeInt); + } } diff --git a/application/src/test/java/org/opentripplanner/osm/wayproperty/specifier/WayTestData.java b/application/src/test/java/org/opentripplanner/osm/wayproperty/specifier/WayTestData.java index e075955f6c4..6d468b42db0 100644 --- a/application/src/test/java/org/opentripplanner/osm/wayproperty/specifier/WayTestData.java +++ b/application/src/test/java/org/opentripplanner/osm/wayproperty/specifier/WayTestData.java @@ -224,4 +224,12 @@ public static OsmWithTags indoor(String value) { way.addTag("indoor", value); return way; } + + public static OsmWithTags parkAndRide() { + var way = new OsmWithTags(); + way.addTag("amenity", "parking"); + way.addTag("park_ride", "yes"); + way.addTag("capacity", "10"); + return way; + } } 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 2a25a3722f9..27d708e1b1d 100644 --- a/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json +++ b/application/src/test/resources/org/opentripplanner/apis/vectortiles/style.json @@ -27,6 +27,104 @@ "group" : "Other" } }, + { + "id" : "wheelchair-accessible", + "source" : "vectorSource", + "source-layer" : "edges", + "type" : "line", + "minzoom" : 6, + "maxzoom" : 23, + "paint" : { + "line-color" : "#136b04", + "line-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.2, + 23, + 8.0 + ], + "line-offset" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.4, + 23, + 7.0 + ] + }, + "filter" : [ + "==", + "wheelchairAccessible", + true + ], + "layout" : { + "line-cap" : "round", + "visibility" : "none" + }, + "metadata" : { + "group" : "Wheelchair accessibility" + } + }, + { + "id" : "wheelchair-inaccessible", + "source" : "vectorSource", + "source-layer" : "edges", + "type" : "line", + "minzoom" : 6, + "maxzoom" : 23, + "paint" : { + "line-color" : "#fc0f2a", + "line-width" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.2, + 23, + 8.0 + ], + "line-offset" : [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 13, + 0.4, + 23, + 7.0 + ] + }, + "filter" : [ + "==", + "wheelchairAccessible", + false + ], + "layout" : { + "line-cap" : "round", + "visibility" : "none" + }, + "metadata" : { + "group" : "Wheelchair accessibility" + } + }, { "id" : "edge", "type" : "line", diff --git a/client/src/components/MapView/GeometryPropertyPopup.tsx b/client/src/components/MapView/GeometryPropertyPopup.tsx index 968dfdcde76..10245a8e0cf 100644 --- a/client/src/components/MapView/GeometryPropertyPopup.tsx +++ b/client/src/components/MapView/GeometryPropertyPopup.tsx @@ -23,7 +23,7 @@ export function GeometryPropertyPopup({ {Object.entries(properties).map(([key, value]) => ( {key} - {value} + {String(value)} ))}