diff --git a/docs/Changelog.md b/docs/Changelog.md index 212b5a6886a..25b89362c3a 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -30,6 +30,7 @@ - Fix for traveling back in time when optimize transfers [#3491](https://github.com/opentripplanner/OpenTripPlanner/pull/3491) - Transit reluctance per transit mode [#3440](https://github.com/opentripplanner/OpenTripPlanner/issues/3440) - Allow the removal of P+R results consisting only of driving of walking [#3515](https://github.com/opentripplanner/OpenTripPlanner/pull/3515) +- Add reluctance to "walk" over certain surfaces (for wheelchairs) [#3524](https://github.com/opentripplanner/OpenTripPlanner/issues/3524) ## 2.0.0 (2020-11-27) diff --git a/src/main/java/org/opentripplanner/api/common/RoutingResource.java b/src/main/java/org/opentripplanner/api/common/RoutingResource.java index 4377b1faee1..71fc2bb0ada 100644 --- a/src/main/java/org/opentripplanner/api/common/RoutingResource.java +++ b/src/main/java/org/opentripplanner/api/common/RoutingResource.java @@ -191,6 +191,15 @@ public abstract class RoutingResource { @QueryParam("waitAtBeginningFactor") protected Double waitAtBeginningFactor; + /** + * How should different road/surface types be preferred? Each "surfaceReluctance" would be a + * comma-separated pair, of "surface_key" * (as detailed at + * https://wiki.openstreetmap.org/wiki/Key:surface) and the reluctance factor (>1). Each + * "surfaceReluctance" pair should be separated by a semi-colon (;). + */ + @QueryParam("surfaceReluctances") + protected String surfaceReluctances; + /** The user's walking speed in meters/second. Defaults to approximately 3 MPH. */ @QueryParam("walkSpeed") protected Double walkSpeed; @@ -700,6 +709,9 @@ protected RoutingRequest buildRequest() throws ParameterException { if (waitAtBeginningFactor != null) request.setWaitAtBeginningFactor(waitAtBeginningFactor); + if (surfaceReluctances != null) + request.setSurfaceReluctances(surfaceReluctances); + if (walkSpeed != null) request.walkSpeed = walkSpeed; diff --git a/src/main/java/org/opentripplanner/api/resource/PlannerResource.java b/src/main/java/org/opentripplanner/api/resource/PlannerResource.java index 7b056031dfa..279b9a2a688 100644 --- a/src/main/java/org/opentripplanner/api/resource/PlannerResource.java +++ b/src/main/java/org/opentripplanner/api/resource/PlannerResource.java @@ -1,7 +1,9 @@ package org.opentripplanner.api.resource; +import javax.ws.rs.WebApplicationException; import org.glassfish.grizzly.http.server.Request; import org.opentripplanner.api.common.Message; +import org.opentripplanner.api.common.ParameterException; import org.opentripplanner.api.common.RoutingResource; import org.opentripplanner.api.mapping.PlannerErrorMapper; import org.opentripplanner.api.mapping.TripPlanMapper; @@ -92,6 +94,12 @@ public TripPlannerResponse plan(@Context UriInfo uriInfo, @Context Request grizz response.debugOutput = res.getDebugAggregator().finishedRendering(); } + catch (ParameterException ex) { + // probably shouldn't log it, since it is a user/parameter error, just report it back + PlannerError error = new PlannerError(); + error.setMsg(ex.message); + response.setError(error); + } catch (Exception e) { LOG.error("System error", e); PlannerError error = new PlannerError(); diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/OpenStreetMapModule.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/OpenStreetMapModule.java index 7b1e3466a6a..e237c841948 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/OpenStreetMapModule.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/OpenStreetMapModule.java @@ -3,6 +3,7 @@ import com.google.common.collect.Iterables; import gnu.trove.iterator.TLongIterator; import gnu.trove.list.TLongList; +import java.util.Optional; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; @@ -1174,6 +1175,7 @@ private StreetEdge getEdgeForStreet(OsmVertex startEndpoint, OsmVertex endEndpoi street.setHasBogusName(true); } street.setStairs(steps); + Optional.ofNullable(way.getSurface()).ifPresent(street::setSurface); /* TODO: This should probably generalized somehow? */ if (!ignoreWheelchairAccessibility diff --git a/src/main/java/org/opentripplanner/openstreetmap/model/OSMWay.java b/src/main/java/org/opentripplanner/openstreetmap/model/OSMWay.java index aaebb534e1b..bdaff1f6781 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/model/OSMWay.java +++ b/src/main/java/org/opentripplanner/openstreetmap/model/OSMWay.java @@ -3,9 +3,6 @@ import gnu.trove.list.TLongList; import gnu.trove.list.array.TLongArrayList; -import java.util.ArrayList; -import java.util.List; - public class OSMWay extends OSMWithTags { private TLongList nodes = new TLongArrayList(); @@ -128,4 +125,15 @@ public boolean isOpposableCycleway() { || (cyclewayLeft != null && cyclewayLeft.startsWith("opposite")) || (cyclewayRight != null && cyclewayRight.startsWith("opposite")); } + + /** + * The possible surface values' documentation can be seen here. And an enumeration of + * all existing values can be + * found here. + * @return The value of the {@code surface} tag of this way. + */ + public String getSurface() { + return getTag("surface"); + } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptor/transit/mappers/RaptorRequestMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptor/transit/mappers/RaptorRequestMapper.java index bebe82605a6..0b12427776a 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptor/transit/mappers/RaptorRequestMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptor/transit/mappers/RaptorRequestMapper.java @@ -66,6 +66,7 @@ public static RaptorRequest mapRequest( } builder.mcCostFactors() + .surfaceReluctanceFactors(request.surfaceReluctances) .waitReluctanceFactor(request.waitReluctance); if(request.modes.accessMode == StreetMode.WALK) { diff --git a/src/main/java/org/opentripplanner/routing/api/request/RoutingRequest.java b/src/main/java/org/opentripplanner/routing/api/request/RoutingRequest.java index d64e1b92dcc..015814ef4dd 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/RoutingRequest.java +++ b/src/main/java/org/opentripplanner/routing/api/request/RoutingRequest.java @@ -1,5 +1,7 @@ package org.opentripplanner.routing.api.request; +import gnu.trove.map.TObjectDoubleMap; +import gnu.trove.map.hash.TObjectDoubleHashMap; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.Serializable; @@ -427,6 +429,8 @@ public class RoutingRequest implements AutoCloseable, Cloneable, Serializable { @Deprecated public double waitAtBeginningFactor = 0.4; + public TObjectDoubleMap surfaceReluctances = new TObjectDoubleHashMap<>(0); + /** * This prevents unnecessary transfers by adding a cost for boarding a vehicle. This is in * addition to the cost of the transfer(walking) and waiting-time. It is also in addition to @@ -1328,6 +1332,30 @@ public void setWaitAtBeginningFactor(double waitAtBeginningFactor) { } } + public void setSurfaceReluctances(String surfaceReluctances) throws ParameterException { + if (surfaceReluctances == null) { + return; + } + String[] reluctanceSegments = surfaceReluctances.split(";"); + this.surfaceReluctances = new TObjectDoubleHashMap<>(reluctanceSegments.length); + for (String reluctanceWithSurface : reluctanceSegments) { + String[] surfaceAndReluctance = reluctanceWithSurface.split(","); + if (surfaceAndReluctance.length != 2) { + throw new ParameterException(Message.BOGUS_PARAMETER); + } + String surface = surfaceAndReluctance[0]; + try { + double reluctance = Double.parseDouble(surfaceAndReluctance[1]); + if (reluctance < 1) { + throw new ParameterException(Message.BOGUS_PARAMETER); + } + this.surfaceReluctances.put(surface, reluctance); + } catch (NumberFormatException ex) { + throw new ParameterException(Message.BOGUS_PARAMETER); + } + } + } + public Set getBannedRoutes(Collection routes) { Set bannedRoutes = new HashSet<>(); for (Route route : routes) { diff --git a/src/main/java/org/opentripplanner/routing/edgetype/StreetEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/StreetEdge.java index 4d2b593cf29..9ab6bc2c8b1 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/StreetEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/StreetEdge.java @@ -117,6 +117,8 @@ public class StreetEdge extends Edge implements Cloneable, CarPickupableEdge { /** The angle at the start of the edge geometry. Internal representation like that of inAngle. */ private byte outAngle; + private String surface; + public StreetEdge(StreetVertex v1, StreetVertex v2, LineString geometry, I18NString name, double length, StreetTraversalPermission permission, boolean back) { @@ -381,6 +383,13 @@ private StateEditor doTraverse(State s0, RoutingRequest options, TraverseMode tr // TODO: this is being applied even when biking or driving. weight *= options.walkReluctance; } + for (String surfaceToCheck : options.surfaceReluctances.keys(new String[0])) { + if (surfaceToCheck.equals(this.surface)) { + weight *= options.surfaceReluctances.get(surfaceToCheck); + // only up to one surface can match + break; + } + } StateEditor s1 = s0.edit(this); s1.setBackMode(traverseMode); @@ -736,6 +745,14 @@ public void setSlopeOverride(boolean slopeOverride) { flags = BitSetUtils.set(flags, SLOPEOVERRIDE_FLAG_INDEX, slopeOverride); } + public String getSurface() { + return surface; + } + + public void setSurface(String surface) { + this.surface = surface; + } + /** * Return the azimuth of the first segment in this edge in integer degrees clockwise from South. * TODO change everything to clockwise from North diff --git a/src/main/java/org/opentripplanner/transit/raptor/api/request/McCostParams.java b/src/main/java/org/opentripplanner/transit/raptor/api/request/McCostParams.java index 37419cbddef..5cf800630b2 100644 --- a/src/main/java/org/opentripplanner/transit/raptor/api/request/McCostParams.java +++ b/src/main/java/org/opentripplanner/transit/raptor/api/request/McCostParams.java @@ -1,6 +1,9 @@ package org.opentripplanner.transit.raptor.api.request; +import gnu.trove.TCollections; +import gnu.trove.map.TObjectDoubleMap; +import gnu.trove.map.hash.TObjectDoubleHashMap; import javax.annotation.Nullable; import org.opentripplanner.routing.api.request.RoutingRequest; @@ -20,6 +23,7 @@ public class McCostParams { private final double[] transitReluctanceFactors; private final double walkReluctanceFactor; private final double waitReluctanceFactor; + private final TObjectDoubleMap surfaceReluctanceFactors; /** * Default constructor defines default values. These defaults are @@ -31,6 +35,7 @@ private McCostParams() { this.transitReluctanceFactors = null; this.walkReluctanceFactor = 4.0; this.waitReluctanceFactor = 1.0; + this.surfaceReluctanceFactors = new TObjectDoubleHashMap<>(0); } McCostParams(McCostParamsBuilder builder) { @@ -39,6 +44,7 @@ private McCostParams() { this.transitReluctanceFactors = builder.transitReluctanceFactors(); this.walkReluctanceFactor = builder.walkReluctanceFactor(); this.waitReluctanceFactor = builder.waitReluctanceFactor(); + this.surfaceReluctanceFactors = TCollections.unmodifiableMap(builder.surfaceReluctanceFactors()); } public int boardCost() { @@ -77,6 +83,10 @@ public double waitReluctanceFactor() { return waitReluctanceFactor; } + public TObjectDoubleMap surfaceReluctanceFactors() { + return surfaceReluctanceFactors; + } + @Override public String toString() { return "McCostParams{" + @@ -84,6 +94,7 @@ public String toString() { ", transferCost=" + transferCost + ", transferReluctanceFactor=" + walkReluctanceFactor + ", waitReluctanceFactor=" + waitReluctanceFactor + + ", surfaceReluctanceFactors=" + surfaceReluctanceFactors + '}'; } @@ -94,12 +105,13 @@ public boolean equals(Object o) { McCostParams that = (McCostParams) o; return boardCost == that.boardCost && transferCost == that.transferCost && + surfaceReluctanceFactors.equals(that.surfaceReluctanceFactors) && Double.compare(that.walkReluctanceFactor, walkReluctanceFactor) == 0 && Double.compare(that.waitReluctanceFactor, waitReluctanceFactor) == 0; } @Override public int hashCode() { - return Objects.hash(boardCost, transferCost, walkReluctanceFactor, waitReluctanceFactor); + return Objects.hash(boardCost, transferCost, walkReluctanceFactor, waitReluctanceFactor, surfaceReluctanceFactors); } } diff --git a/src/main/java/org/opentripplanner/transit/raptor/api/request/McCostParamsBuilder.java b/src/main/java/org/opentripplanner/transit/raptor/api/request/McCostParamsBuilder.java index 86786dde168..f0a114a46c7 100644 --- a/src/main/java/org/opentripplanner/transit/raptor/api/request/McCostParamsBuilder.java +++ b/src/main/java/org/opentripplanner/transit/raptor/api/request/McCostParamsBuilder.java @@ -1,6 +1,8 @@ package org.opentripplanner.transit.raptor.api.request; +import gnu.trove.map.TObjectDoubleMap; + /** * Mutable version of the {@link McCostParams}. */ @@ -11,6 +13,7 @@ public class McCostParamsBuilder { private double[] transitReluctanceFactors; private double walkReluctanceFactor; private double waitReluctanceFactor; + private TObjectDoubleMap surfaceReluctanceFactors; McCostParamsBuilder(McCostParams defaults) { @@ -19,6 +22,7 @@ public class McCostParamsBuilder { this.transitReluctanceFactors = defaults.transitReluctanceFactors(); this.walkReluctanceFactor = defaults.walkReluctanceFactor(); this.waitReluctanceFactor = defaults.waitReluctanceFactor(); + this.surfaceReluctanceFactors = defaults.surfaceReluctanceFactors(); } public int boardCost() { @@ -65,4 +69,13 @@ public McCostParamsBuilder waitReluctanceFactor(double waitReluctanceFactor) { this.waitReluctanceFactor = waitReluctanceFactor; return this; } + + public TObjectDoubleMap surfaceReluctanceFactors() { + return surfaceReluctanceFactors; + } + + public McCostParamsBuilder surfaceReluctanceFactors(TObjectDoubleMap surfaceReluctanceFactors) { + this.surfaceReluctanceFactors = surfaceReluctanceFactors; + return this; + } } diff --git a/src/test/java/org/opentripplanner/routing/core/RoutingRequestTest.java b/src/test/java/org/opentripplanner/routing/core/RoutingRequestTest.java index caf58cab74d..27eb232265a 100644 --- a/src/test/java/org/opentripplanner/routing/core/RoutingRequestTest.java +++ b/src/test/java/org/opentripplanner/routing/core/RoutingRequestTest.java @@ -1,6 +1,7 @@ package org.opentripplanner.routing.core; import org.junit.Test; +import org.opentripplanner.api.common.ParameterException; import org.opentripplanner.model.Agency; import org.opentripplanner.model.FeedScopedId; import org.opentripplanner.model.GenericLocation; @@ -12,6 +13,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.opentripplanner.routing.core.TraverseMode.CAR; public class RoutingRequestTest { @@ -27,36 +29,37 @@ private GenericLocation randomLocation() { @Test public void testRequest() { - RoutingRequest request = new RoutingRequest(); - - request.addMode(CAR); - assertTrue(request.streetSubRequestModes.getCar()); - request.removeMode(CAR); - assertFalse(request.streetSubRequestModes.getCar()); - - request.setStreetSubRequestModes(new TraverseModeSet(TraverseMode.BICYCLE,TraverseMode.WALK)); - assertFalse(request.streetSubRequestModes.getCar()); - assertTrue(request.streetSubRequestModes.getBicycle()); - assertTrue(request.streetSubRequestModes.getWalk()); + try (RoutingRequest request = new RoutingRequest()) { + request.addMode(CAR); + assertTrue(request.streetSubRequestModes.getCar()); + request.removeMode(CAR); + assertFalse(request.streetSubRequestModes.getCar()); + + request.setStreetSubRequestModes(new TraverseModeSet(TraverseMode.BICYCLE,TraverseMode.WALK)); + assertFalse(request.streetSubRequestModes.getCar()); + assertTrue(request.streetSubRequestModes.getBicycle()); + assertTrue(request.streetSubRequestModes.getWalk()); + } } @Test public void testIntermediatePlaces() { - RoutingRequest req = new RoutingRequest(); - assertFalse(req.hasIntermediatePlaces()); + try (RoutingRequest req = new RoutingRequest()) { + assertFalse(req.hasIntermediatePlaces()); + + req.clearIntermediatePlaces(); + assertFalse(req.hasIntermediatePlaces()); - req.clearIntermediatePlaces(); - assertFalse(req.hasIntermediatePlaces()); + req.addIntermediatePlace(randomLocation()); + assertTrue(req.hasIntermediatePlaces()); - req.addIntermediatePlace(randomLocation()); - assertTrue(req.hasIntermediatePlaces()); - - req.clearIntermediatePlaces(); - assertFalse(req.hasIntermediatePlaces()); + req.clearIntermediatePlaces(); + assertFalse(req.hasIntermediatePlaces()); - req.addIntermediatePlace(randomLocation()); - req.addIntermediatePlace(randomLocation()); - assertTrue(req.hasIntermediatePlaces()); + req.addIntermediatePlace(randomLocation()); + req.addIntermediatePlace(randomLocation()); + assertTrue(req.hasIntermediatePlaces()); + } } @Test @@ -98,6 +101,44 @@ public void testPreferencesPenaltyForRoute() { } } + @Test + public void testSurfaceReluctances() { + try (RoutingRequest req = new RoutingRequest()) { + assertTrue(req.surfaceReluctances.isEmpty()); + + try { + req.setSurfaceReluctances("asphalt,1.2;cobblestone,5"); + } catch (Exception e) { + fail(); + } + + assertFalse(req.surfaceReluctances.isEmpty()); + assertEquals(2, req.surfaceReluctances.size()); + assertTrue(req.surfaceReluctances.containsKey("asphalt")); + assertTrue(req.surfaceReluctances.containsValue(1.2)); + assertTrue(req.surfaceReluctances.containsKey("cobblestone")); + assertTrue(req.surfaceReluctances.containsValue(5.0)); + + try { + req.setSurfaceReluctances("asphalt;cobblestone,5"); + fail(); + } catch (ParameterException expected) { + // we expect it to throw the exception + } + try { + req.setSurfaceReluctances("asphalt,0.5;cobblestone,5"); + fail(); + } catch (ParameterException expected) { + // we expect it to throw the exception + } + try { + req.setSurfaceReluctances("asphalt,foo;cobblestone,5"); + fail(); + } catch (ParameterException expected) { + // we expect it to throw the exception + } + } + } private static class RoutePenaltyTC { final boolean prefAgency;