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;