diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/EscalatorProcessor.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/EscalatorProcessor.java index 75e0965d82f..40695e7c67c 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/EscalatorProcessor.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/EscalatorProcessor.java @@ -1,8 +1,12 @@ package org.opentripplanner.graph_builder.module.osm; +import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.graph_builder.issue.api.Issue; import org.opentripplanner.osm.model.OsmWay; import org.opentripplanner.street.model.edge.EscalatorEdge; import org.opentripplanner.street.model.vertex.IntersectionVertex; @@ -13,9 +17,19 @@ class EscalatorProcessor { private final Map intersectionNodes; + private final DataImportIssueStore issueStore; - public EscalatorProcessor(Map intersectionNodes) { + // If an escalator is tagged as moving less than 5 cm/s, or more than 5 m/s, + // assume it's an error and ignore it. + private static final double SLOW_ESCALATOR_ERROR_CUTOFF = 0.05; + private static final double FAST_ESCALATOR_ERROR_CUTOFF = 5.0; + + public EscalatorProcessor( + Map intersectionNodes, + DataImportIssueStore issueStore + ) { this.intersectionNodes = intersectionNodes; + this.issueStore = issueStore; } public void buildEscalatorEdge(OsmWay escalatorWay, double length) { @@ -27,30 +41,58 @@ public void buildEscalatorEdge(OsmWay escalatorWay, double length) { .boxed() .toList(); + Optional duration = escalatorWay.getDuration(v -> + issueStore.add( + Issue.issue( + "InvalidDuration", + "Duration for osm node {} is not a valid duration: '{}'; the value is ignored.", + escalatorWay.url(), + v + ) + ) + ); + if (duration.isPresent()) { + double speed = length / duration.get().toSeconds(); + if (speed < SLOW_ESCALATOR_ERROR_CUTOFF || speed > FAST_ESCALATOR_ERROR_CUTOFF) { + duration = Optional.empty(); + issueStore.add( + Issue.issue( + "InvalidDuration", + "Duration for osm node {} makes implied speed {} be outside acceptable range.", + escalatorWay.url(), + speed + ) + ); + } + } for (int i = 0; i < nodes.size() - 1; i++) { if (escalatorWay.isForwardEscalator()) { EscalatorEdge.createEscalatorEdge( intersectionNodes.get(nodes.get(i)), intersectionNodes.get(nodes.get(i + 1)), - length + length, + duration.orElse(null) ); } else if (escalatorWay.isBackwardEscalator()) { EscalatorEdge.createEscalatorEdge( intersectionNodes.get(nodes.get(i + 1)), intersectionNodes.get(nodes.get(i)), - length + length, + duration.orElse(null) ); } else { EscalatorEdge.createEscalatorEdge( intersectionNodes.get(nodes.get(i)), intersectionNodes.get(nodes.get(i + 1)), - length + length, + duration.orElse(null) ); EscalatorEdge.createEscalatorEdge( intersectionNodes.get(nodes.get(i + 1)), intersectionNodes.get(nodes.get(i)), - length + length, + duration.orElse(null) ); } } diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java index 08d23087a45..e4a08b40177 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java @@ -247,7 +247,10 @@ private void buildBasicGraph() { long wayCount = osmdb.getWays().size(); ProgressTracker progress = ProgressTracker.track("Build street graph", 5_000, wayCount); LOG.info(progress.startMessage()); - var escalatorProcessor = new EscalatorProcessor(vertexGenerator.intersectionNodes()); + var escalatorProcessor = new EscalatorProcessor( + vertexGenerator.intersectionNodes(), + issueStore + ); WAY:for (OsmWay way : osmdb.getWays()) { WayProperties wayData = way.getOsmProvider().getWayPropertySet().getDataForWay(way); 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 30763edca9e..83b677a62ce 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 @@ -22,7 +22,10 @@ protected Collection map(Edge input) { List properties = switch (input) { case StreetEdge e -> mapStreetEdge(e); - case EscalatorEdge e -> List.of(kv("distance", e.getDistanceMeters())); + case EscalatorEdge e -> List.of( + kv("distance", e.getDistanceMeters()), + kv("duration", e.getDuration().map(d -> d.toString()).orElse(null)) + ); default -> List.of(); }; return ListUtils.combine(baseProps, properties); diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmWay.java b/application/src/main/java/org/opentripplanner/osm/model/OsmWay.java index 7b5fbe56748..a620545f521 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmWay.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmWay.java @@ -2,7 +2,10 @@ import gnu.trove.list.TLongList; import gnu.trove.list.array.TLongArrayList; +import java.time.Duration; +import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import org.opentripplanner.graph_builder.module.osm.StreetTraversalPermissionPair; import org.opentripplanner.street.model.StreetTraversalPermission; @@ -130,6 +133,10 @@ public boolean isEscalator() { return (isTag("highway", "steps") && isOneOfTags("conveying", ESCALATOR_CONVEYING_TAGS)); } + public Optional getDuration(Consumer errorHandler) { + return getTagValueAsDuration("duration", errorHandler); + } + public boolean isForwardEscalator() { return isEscalator() && "forward".equals(this.getTag("conveying")); } 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 67f737e4c79..3f47d4454bd 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmWithTags.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmWithTags.java @@ -1,5 +1,7 @@ package org.opentripplanner.osm.model; +import java.time.Duration; +import java.time.format.DateTimeParseException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -221,6 +223,101 @@ public OptionalInt getTagAsInt(String tag, Consumer errorHandler) { return OptionalInt.empty(); } + /** + * Parse an OSM duration tag, which is one of: + * mm + * hh:mm + * hh:mm:ss + * and where the leading value is not limited to any maximum. + * See OSM wiki definition + * of duration. + * + * @param duration string in format mm, hh:mm, or hh:mm:ss + * @return Duration + * @throws DateTimeParseException on bad input + */ + public static Duration parseOsmDuration(String duration) { + // Unfortunately DateFormatParserBuilder doesn't quite do enough for this case. + // It has the capability for expressing optional parts, so it could express hh(:mm(:ss)?)? + // but it cannot express (hh:)?mm(:ss)? where the existence of (:ss) implies the existence + // of (hh:). Even if it did, it would not be able to handle the cases where hours are + // greater than 23 or (if there is no hours part at all) minutes are greater than 59, which + // are both allowed by the spec and exist in OSM data. Durations are not LocalTimes after + // all, in parsing a LocalTime it makes sense and is correct that hours cannot be more than + // 23 or minutes more than 59, but in durations if you have capped the largest unit, it is + // reasonable for the amount of the largest unit to be as large as it needs to be. + int colonCount = (int) duration.chars().filter(ch -> ch == ':').count(); + if (colonCount <= 2) { + try { + int i, j; + long hours, minutes, seconds; + // The first :-separated element can be any width, and has no maximum. It still has + // to be non-negative. The following elements must be 2 characters wide, non-negative, + // and less than 60. + switch (colonCount) { + // case "m" + case 0: + minutes = Long.parseLong(duration); + if (minutes >= 0) { + return Duration.ofMinutes(minutes); + } + break; + // case "h:mm" + case 1: + i = duration.indexOf(':'); + hours = Long.parseLong(duration.substring(0, i)); + minutes = Long.parseLong(duration.substring(i + 1)); + if (duration.length() - i == 3 && hours >= 0 && minutes >= 0 && minutes < 60) { + return Duration.ofHours(hours).plusMinutes(minutes); + } + break; + // case "h:mm:ss" + default: + i = duration.indexOf(':'); + j = duration.indexOf(':', i + 1); + hours = Long.parseLong(duration.substring(0, i)); + minutes = Long.parseLong(duration.substring(i + 1, j)); + seconds = Long.parseLong(duration.substring(j + 1)); + if ( + j - i == 3 && + duration.length() - j == 3 && + hours >= 0 && + minutes >= 0 && + minutes < 60 && + seconds >= 0 && + seconds < 60 + ) { + return Duration.ofHours(hours).plusMinutes(minutes).plusSeconds(seconds); + } + break; + } + } catch (NumberFormatException e) { + // fallthrough + } + } + throw new DateTimeParseException("Bad OSM duration", duration, 0); + } + + /** + * Gets a tag's value, assumes it is an OSM wiki specified duration, parses and returns it. + * If parsing fails, calls the error handler. + * + * @param key + * @param errorHandler + * @return parsed Duration, or empty + */ + public Optional getTagValueAsDuration(String key, Consumer errorHandler) { + String value = getTag(key); + if (value != null) { + try { + return Optional.of(parseOsmDuration(value)); + } catch (DateTimeParseException e) { + errorHandler.accept(value); + } + } + return Optional.empty(); + } + /** * Some tags are allowed to have values like 55, "true" or "false". *

diff --git a/application/src/main/java/org/opentripplanner/routing/api/request/preference/EscalatorPreferences.java b/application/src/main/java/org/opentripplanner/routing/api/request/preference/EscalatorPreferences.java new file mode 100644 index 00000000000..3c150b43417 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/routing/api/request/preference/EscalatorPreferences.java @@ -0,0 +1,114 @@ +package org.opentripplanner.routing.api.request.preference; + +import static org.opentripplanner.utils.lang.DoubleUtils.doubleEquals; + +import java.io.Serializable; +import java.util.Objects; +import java.util.function.Consumer; +import org.opentripplanner.utils.tostring.ToStringBuilder; + +public class EscalatorPreferences implements Serializable { + + public static final EscalatorPreferences DEFAULT = new EscalatorPreferences(); + + private final double reluctance; + private final double speed; + + /* Using the angle of 30 degrees and a speed of 0.5 m/s gives a horizontal component + * of approx. 0.43 m/s. This is typical of short escalators like those in shopping + * malls. */ + private static final double HORIZONTAL_SPEED = 0.45; + + private EscalatorPreferences() { + this.reluctance = 1.5; + this.speed = HORIZONTAL_SPEED; + } + + private EscalatorPreferences(Builder builder) { + reluctance = builder.reluctance; + speed = builder.speed; + } + + public static Builder of() { + return new Builder(DEFAULT); + } + + public Builder copyOf() { + return new Builder(this); + } + + public double reluctance() { + return reluctance; + } + + public double speed() { + return speed; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EscalatorPreferences that = (EscalatorPreferences) o; + return (doubleEquals(that.reluctance, reluctance) && doubleEquals(that.speed, speed)); + } + + @Override + public int hashCode() { + return Objects.hash(speed, reluctance); + } + + @Override + public String toString() { + return ToStringBuilder + .of(EscalatorPreferences.class) + .addNum("speed", speed, DEFAULT.speed) + .addNum("reluctance", reluctance, DEFAULT.reluctance) + .toString(); + } + + public static class Builder { + + private final EscalatorPreferences original; + private double reluctance; + private double speed; + + public Builder(EscalatorPreferences original) { + this.original = original; + this.reluctance = original.reluctance; + this.speed = original.speed; + } + + public EscalatorPreferences original() { + return original; + } + + public double speed() { + return speed; + } + + public Builder withSpeed(double speed) { + this.speed = speed; + return this; + } + + public double reluctance() { + return reluctance; + } + + public Builder withReluctance(double reluctance) { + this.reluctance = reluctance; + return this; + } + + public Builder apply(Consumer body) { + body.accept(this); + return this; + } + + public EscalatorPreferences build() { + var newObj = new EscalatorPreferences(this); + return original.equals(newObj) ? original : newObj; + } + } +} diff --git a/application/src/main/java/org/opentripplanner/routing/api/request/preference/WalkPreferences.java b/application/src/main/java/org/opentripplanner/routing/api/request/preference/WalkPreferences.java index 4a5969049ba..5bc0a120d32 100644 --- a/application/src/main/java/org/opentripplanner/routing/api/request/preference/WalkPreferences.java +++ b/application/src/main/java/org/opentripplanner/routing/api/request/preference/WalkPreferences.java @@ -1,6 +1,7 @@ package org.opentripplanner.routing.api.request.preference; import static org.opentripplanner.utils.lang.DoubleUtils.doubleEquals; +import static org.opentripplanner.utils.lang.ObjectUtils.ifNotNull; import java.io.Serializable; import java.util.Objects; @@ -29,7 +30,7 @@ public final class WalkPreferences implements Serializable { private final double stairsTimeFactor; private final double safetyFactor; - private final double escalatorReluctance; + private final EscalatorPreferences escalator; private WalkPreferences() { this.speed = 1.33; @@ -38,7 +39,7 @@ private WalkPreferences() { this.stairsReluctance = 2.0; this.stairsTimeFactor = 3.0; this.safetyFactor = 1.0; - this.escalatorReluctance = 1.5; + this.escalator = EscalatorPreferences.DEFAULT; } private WalkPreferences(Builder builder) { @@ -48,7 +49,7 @@ private WalkPreferences(Builder builder) { this.stairsReluctance = Units.reluctance(builder.stairsReluctance); this.stairsTimeFactor = Units.reluctance(builder.stairsTimeFactor); this.safetyFactor = Units.reluctance(builder.safetyFactor); - this.escalatorReluctance = Units.reluctance(builder.escalatorReluctance); + this.escalator = builder.escalator; } public static Builder of() { @@ -108,6 +109,10 @@ public double safetyFactor() { return safetyFactor; } + public EscalatorPreferences escalator() { + return escalator; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -120,7 +125,7 @@ public boolean equals(Object o) { doubleEquals(that.stairsReluctance, stairsReluctance) && doubleEquals(that.stairsTimeFactor, stairsTimeFactor) && doubleEquals(that.safetyFactor, safetyFactor) && - doubleEquals(that.escalatorReluctance, escalatorReluctance) + escalator.equals(that.escalator) ); } @@ -133,7 +138,7 @@ public int hashCode() { stairsReluctance, stairsTimeFactor, safetyFactor, - escalatorReluctance + escalator ); } @@ -147,14 +152,10 @@ public String toString() { .addNum("stairsReluctance", stairsReluctance, DEFAULT.stairsReluctance) .addNum("stairsTimeFactor", stairsTimeFactor, DEFAULT.stairsTimeFactor) .addNum("safetyFactor", safetyFactor, DEFAULT.safetyFactor) - .addNum("escalatorReluctance", escalatorReluctance, DEFAULT.escalatorReluctance) + .addObj("escalator", escalator, DEFAULT.escalator) .toString(); } - public double escalatorReluctance() { - return escalatorReluctance; - } - public static class Builder { private final WalkPreferences original; @@ -165,7 +166,7 @@ public static class Builder { private double stairsTimeFactor; private double safetyFactor; - private double escalatorReluctance; + private EscalatorPreferences escalator; public Builder(WalkPreferences original) { this.original = original; @@ -175,7 +176,7 @@ public Builder(WalkPreferences original) { this.stairsReluctance = original.stairsReluctance; this.stairsTimeFactor = original.stairsTimeFactor; this.safetyFactor = original.safetyFactor; - this.escalatorReluctance = original.escalatorReluctance; + this.escalator = original.escalator; } public WalkPreferences original() { @@ -242,12 +243,12 @@ public Builder withSafetyFactor(double safetyFactor) { return this; } - public double escalatorReluctance() { - return escalatorReluctance; + public EscalatorPreferences escalator() { + return escalator; } - public Builder withEscalatorReluctance(double escalatorReluctance) { - this.escalatorReluctance = escalatorReluctance; + public Builder withEscalator(Consumer body) { + this.escalator = ifNotNull(this.escalator, original.escalator).copyOf().apply(body).build(); return this; } diff --git a/application/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java b/application/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java index d05aee96ccf..454ab29a68c 100644 --- a/application/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java +++ b/application/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java @@ -6,6 +6,7 @@ import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_3; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_4; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_5; +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_7; import static org.opentripplanner.standalone.config.routerequest.ItineraryFiltersConfig.mapItineraryFilterParams; import static org.opentripplanner.standalone.config.routerequest.TransferConfig.mapTransferPreferences; import static org.opentripplanner.standalone.config.routerequest.TriangleOptimizationConfig.mapOptimizationTriangle; @@ -25,6 +26,7 @@ import org.opentripplanner.routing.api.request.preference.AccessEgressPreferences; import org.opentripplanner.routing.api.request.preference.BikePreferences; import org.opentripplanner.routing.api.request.preference.CarPreferences; +import org.opentripplanner.routing.api.request.preference.EscalatorPreferences; import org.opentripplanner.routing.api.request.preference.RoutingPreferences; import org.opentripplanner.routing.api.request.preference.ScooterPreferences; import org.opentripplanner.routing.api.request.preference.StreetPreferences; @@ -736,6 +738,32 @@ private static void mapSystemPreferences(NodeAdapter c, SystemPreferences.Builde } } + private static void mapEscalatorPreferences( + NodeAdapter root, + EscalatorPreferences.Builder escalator + ) { + var dft = escalator.original(); + NodeAdapter c = root.of("escalator").since(V2_7).summary("Escalator preferences.").asObject(); + escalator + .withReluctance( + c + .of("reluctance") + .since(V2_4) + .summary( + "A multiplier for how bad being in an escalator is compared to being in transit for equal lengths of time" + ) + .asDouble(dft.reluctance()) + ) + .withSpeed( + c + .of("speed") + .since(V2_7) + .summary("How fast does an escalator move horizontally?") + .description("Horizontal speed of escalator in m/s.") + .asDouble(dft.speed()) + ); + } + private static void mapWalkPreferences(NodeAdapter root, WalkPreferences.Builder walk) { var dft = walk.original(); NodeAdapter c = root.of("walk").since(V2_5).summary("Walking preferences.").asObject(); @@ -809,14 +837,6 @@ private static void mapWalkPreferences(NodeAdapter root, WalkPreferences.Builder ) .asDouble(dft.safetyFactor()) ) - .withEscalatorReluctance( - c - .of("escalatorReluctance") - .since(V2_4) - .summary( - "A multiplier for how bad being in an escalator is compared to being in transit for equal lengths of time" - ) - .asDouble(dft.escalatorReluctance()) - ); + .withEscalator(escalator -> mapEscalatorPreferences(c, escalator)); } } diff --git a/application/src/main/java/org/opentripplanner/street/model/edge/EscalatorEdge.java b/application/src/main/java/org/opentripplanner/street/model/edge/EscalatorEdge.java index 20fae657c78..d44b67568d6 100644 --- a/application/src/main/java/org/opentripplanner/street/model/edge/EscalatorEdge.java +++ b/application/src/main/java/org/opentripplanner/street/model/edge/EscalatorEdge.java @@ -1,5 +1,8 @@ package org.opentripplanner.street.model.edge; +import java.time.Duration; +import java.util.Optional; +import javax.annotation.Nullable; import org.locationtech.jts.geom.LineString; import org.opentripplanner.framework.geometry.GeometryUtils; import org.opentripplanner.framework.i18n.I18NString; @@ -11,16 +14,14 @@ /** Represents an escalator. An escalator edge can only be traversed by walking */ public class EscalatorEdge extends Edge { - /* A quick internet search gives escalator speed range of 0.3-0.6 m/s and angle of 30 degrees. - * Using the angle of 30 degrees and a speed of 0.5 m/s gives a horizontal component - * of approx. 0.43 m/s */ - private static final double HORIZONTAL_SPEED = 0.45; private static final LocalizedString NAME = new LocalizedString("name.escalator"); private final double length; + private final Duration duration; - private EscalatorEdge(Vertex v1, Vertex v2, double length) { + private EscalatorEdge(Vertex v1, Vertex v2, double length, Duration duration) { super(v1, v2); this.length = length; + this.duration = duration; } @Override @@ -28,8 +29,13 @@ public State[] traverse(State s0) { // Only allow traversal by walking if (s0.currentMode() == TraverseMode.WALK && !s0.getRequest().wheelchair()) { var s1 = s0.edit(this); - var time = getDistanceMeters() / HORIZONTAL_SPEED; - s1.incrementWeight(s0.getPreferences().walk().escalatorReluctance() * time); + double time; + if (duration == null) { + time = getDistanceMeters() / s0.getPreferences().walk().escalator().speed(); + } else { + time = duration.toSeconds(); + } + s1.incrementWeight(s0.getPreferences().walk().escalator().reluctance() * time); s1.incrementTimeInSeconds((int) Math.round(time)); s1.incrementWalkDistance(getDistanceMeters()); return s1.makeStateArray(); @@ -46,12 +52,25 @@ public double getDistanceMeters() { return length; } + /** + * Parsed content of duration tag in OSM, if any. Not a calculated value. + * @return Duration, or empty + */ + public Optional getDuration() { + return Optional.ofNullable(duration); + } + @Override public I18NString getName() { return NAME; } - public static EscalatorEdge createEscalatorEdge(Vertex from, Vertex to, double length) { - return connectToGraph(new EscalatorEdge(from, to, length)); + public static EscalatorEdge createEscalatorEdge( + Vertex from, + Vertex to, + double length, + @Nullable Duration duration + ) { + return connectToGraph(new EscalatorEdge(from, to, length, duration)); } } 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 a89f8612040..84b74b8f655 100644 --- a/application/src/test/java/org/opentripplanner/osm/model/OsmWithTagsTest.java +++ b/application/src/test/java/org/opentripplanner/osm/model/OsmWithTagsTest.java @@ -5,9 +5,11 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.OptionalInt; import java.util.Set; import org.junit.jupiter.api.Test; @@ -298,4 +300,32 @@ void parseIntOrBoolean(String value, OptionalInt expected) { var maybeInt = way.parseIntOrBoolean(key, i -> {}); assertEquals(expected, maybeInt); } + + private static List parseTagAsDurationCases() { + return List.of( + Arguments.of("00:11", Optional.of(Duration.ofMinutes(11))), + Arguments.of("11", Optional.of(Duration.ofMinutes(11))), + Arguments.of("1:22:33", Optional.of(Duration.ofHours(1).plusMinutes(22).plusSeconds(33))), + Arguments.of("82", Optional.of(Duration.ofMinutes(82))), + Arguments.of("25:00", Optional.of(Duration.ofHours(25))), + Arguments.of("25:00:00", Optional.of(Duration.ofHours(25))), + Arguments.of("22:60", Optional.empty()), + Arguments.of("10:61:40", Optional.empty()), + Arguments.of("10:59:60", Optional.empty()), + Arguments.of("1:12:34", Optional.of(Duration.ofHours(1).plusMinutes(12).plusSeconds(34))), + Arguments.of("1:2:34", Optional.empty()), + Arguments.of("1:12:3", Optional.empty()), + Arguments.of("1:2", Optional.empty()) + ); + } + + @ParameterizedTest + @MethodSource("parseTagAsDurationCases") + void parseTagAsDuration(String value, Optional expected) { + var way = new OsmWithTags(); + var key = "duration"; + way.addTag(key, value); + var duration = way.getTagValueAsDuration(key, i -> {}); + assertEquals(expected, duration); + } } diff --git a/application/src/test/java/org/opentripplanner/routing/api/request/preference/WalkPreferencesTest.java b/application/src/test/java/org/opentripplanner/routing/api/request/preference/WalkPreferencesTest.java index a61a0558bb1..785b130ca7a 100644 --- a/application/src/test/java/org/opentripplanner/routing/api/request/preference/WalkPreferencesTest.java +++ b/application/src/test/java/org/opentripplanner/routing/api/request/preference/WalkPreferencesTest.java @@ -88,7 +88,7 @@ void testEqualsAndHashCodeWithNewlyConstructedPreferences() { .withReluctance(sameReluctance) .withStairsReluctance(sameStairsReluctance) .withSafetyFactor(sameSafetyFactor) - .withEscalatorReluctance(sameEscalatorReluctance) + .withEscalator(escalator -> escalator.withReluctance(sameEscalatorReluctance)) .withBoardCost(sameBoardCost) .build(); var secondEqual = WalkPreferences @@ -97,7 +97,7 @@ void testEqualsAndHashCodeWithNewlyConstructedPreferences() { .withReluctance(sameReluctance) .withStairsReluctance(sameStairsReluctance) .withSafetyFactor(sameSafetyFactor) - .withEscalatorReluctance(sameEscalatorReluctance) + .withEscalator(escalator -> escalator.withReluctance(sameEscalatorReluctance)) .withBoardCost(sameBoardCost) .build(); assertEqualsAndHashCode(firstEqual, secondEqual); @@ -110,7 +110,7 @@ void testEqualsAndHashCodeWithNewlyConstructedPreferences() { .withReluctance(sameReluctance) .withStairsReluctance(sameStairsReluctance) .withSafetyFactor(sameSafetyFactor) - .withEscalatorReluctance(sameEscalatorReluctance) + .withEscalator(escalator -> escalator.withReluctance(sameEscalatorReluctance)) .withBoardCost(sameBoardCost) .build(); assertNotEqualsAndHashCode(firstEqual, differentSpeedPreferences); @@ -123,7 +123,7 @@ void testEqualsAndHashCodeWithNewlyConstructedPreferences() { .withReluctance(notSameReluctance) .withStairsReluctance(sameStairsReluctance) .withSafetyFactor(sameSafetyFactor) - .withEscalatorReluctance(sameEscalatorReluctance) + .withEscalator(escalator -> escalator.withReluctance(sameEscalatorReluctance)) .withBoardCost(sameBoardCost) .build(); assertNotEqualsAndHashCode(firstEqual, differentReluctancePreferences); @@ -136,7 +136,7 @@ void testEqualsAndHashCodeWithNewlyConstructedPreferences() { .withReluctance(sameReluctance) .withStairsReluctance(notSameStairsReluctance) .withSafetyFactor(sameSafetyFactor) - .withEscalatorReluctance(sameEscalatorReluctance) + .withEscalator(escalator -> escalator.withReluctance(sameEscalatorReluctance)) .withBoardCost(sameBoardCost) .build(); assertNotEqualsAndHashCode(firstEqual, differentStairsReluctancePreferences); @@ -149,7 +149,7 @@ void testEqualsAndHashCodeWithNewlyConstructedPreferences() { .withReluctance(sameReluctance) .withStairsReluctance(sameStairsReluctance) .withSafetyFactor(notSameSafetyFactor) - .withEscalatorReluctance(sameEscalatorReluctance) + .withEscalator(escalator -> escalator.withReluctance(sameEscalatorReluctance)) .withBoardCost(sameBoardCost) .build(); assertNotEqualsAndHashCode(firstEqual, differentSafetyFactorPreferences); @@ -162,7 +162,7 @@ void testEqualsAndHashCodeWithNewlyConstructedPreferences() { .withReluctance(sameReluctance) .withStairsReluctance(sameStairsReluctance) .withSafetyFactor(sameSafetyFactor) - .withEscalatorReluctance(notSameEscalatorReluctance) + .withEscalator(escalator -> escalator.withReluctance(notSameEscalatorReluctance)) .withBoardCost(sameBoardCost) .build(); assertNotEqualsAndHashCode(firstEqual, differentEscalatorReluctancePreferences); @@ -175,7 +175,7 @@ void testEqualsAndHashCodeWithNewlyConstructedPreferences() { .withReluctance(sameReluctance) .withStairsReluctance(sameStairsReluctance) .withSafetyFactor(sameSafetyFactor) - .withEscalatorReluctance(sameEscalatorReluctance) + .withEscalator(escalator -> escalator.withReluctance(sameEscalatorReluctance)) .withBoardCost(notSameBoardCost) .build(); assertNotEqualsAndHashCode(firstEqual, differentBoardCostPreferences); diff --git a/application/src/test/java/org/opentripplanner/street/model/edge/EscalatorEdgeTest.java b/application/src/test/java/org/opentripplanner/street/model/edge/EscalatorEdgeTest.java index 1cfff635c45..23ae7a567f8 100644 --- a/application/src/test/java/org/opentripplanner/street/model/edge/EscalatorEdgeTest.java +++ b/application/src/test/java/org/opentripplanner/street/model/edge/EscalatorEdgeTest.java @@ -3,6 +3,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import java.time.Duration; import java.util.Locale; import java.util.stream.Stream; import org.junit.jupiter.api.Test; @@ -27,20 +28,31 @@ static Stream args() { @ParameterizedTest(name = "escalatorReluctance of {0} should lead to traversal costs of {1}") @MethodSource("args") void testWalking(double escalatorReluctance, double expectedWeight) { - var edge = EscalatorEdge.createEscalatorEdge(from, to, 45); + var edge = EscalatorEdge.createEscalatorEdge(from, to, 45, null); var req = StreetSearchRequest .of() - .withPreferences(p -> p.withWalk(w -> w.withEscalatorReluctance(escalatorReluctance))) + .withPreferences(p -> + p.withWalk(w -> w.withEscalator(escalator -> escalator.withReluctance(escalatorReluctance))) + ) .withMode(StreetMode.WALK); var res = edge.traverse(new State(from, req.build()))[0]; - assertEquals(res.weight, expectedWeight); - assertEquals(res.getTimeDeltaSeconds(), 100); + assertEquals(expectedWeight, res.weight); + assertEquals(100, res.getTimeDeltaSeconds()); + } + + @Test + void testDuration() { + // If duration is given, length does not affect timeDeltaSeconds, only duration does. + var edge = EscalatorEdge.createEscalatorEdge(from, to, 45, Duration.ofSeconds(60)); + var req = StreetSearchRequest.of().withMode(StreetMode.WALK); + var res = edge.traverse(new State(from, req.build()))[0]; + assertEquals(60, res.getTimeDeltaSeconds()); } @Test void testCycling() { - var edge = EscalatorEdge.createEscalatorEdge(from, to, 10); + var edge = EscalatorEdge.createEscalatorEdge(from, to, 10, null); var req = StreetSearchRequest.of().withMode(StreetMode.BIKE); var res = edge.traverse(new State(from, req.build())); assertThat(res).isEmpty(); @@ -48,7 +60,7 @@ void testCycling() { @Test void testWheelchair() { - var edge = EscalatorEdge.createEscalatorEdge(from, to, 10); + var edge = EscalatorEdge.createEscalatorEdge(from, to, 10, null); var req = StreetSearchRequest.of().withMode(StreetMode.WALK).withWheelchair(true); var res = edge.traverse(new State(from, req.build())); assertThat(res).isEmpty(); @@ -56,14 +68,14 @@ void testWheelchair() { @Test void name() { - var edge = EscalatorEdge.createEscalatorEdge(from, to, 10); + var edge = EscalatorEdge.createEscalatorEdge(from, to, 10, null); assertEquals("Rolltreppe", edge.getName().toString(Locale.GERMANY)); assertEquals("escalator", edge.getName().toString(Locale.ENGLISH)); } @Test void geometry() { - var edge = EscalatorEdge.createEscalatorEdge(from, to, 10); + var edge = EscalatorEdge.createEscalatorEdge(from, to, 10, null); assertThat(edge.getGeometry().getCoordinates()).isNotEmpty(); } } diff --git a/application/src/test/resources/standalone/config/router-config.json b/application/src/test/resources/standalone/config/router-config.json index f1eac1cb41e..e3f5b0caad2 100644 --- a/application/src/test/resources/standalone/config/router-config.json +++ b/application/src/test/resources/standalone/config/router-config.json @@ -75,7 +75,10 @@ "reluctance": 4.0, "stairsReluctance": 1.65, "boardCost": 600, - "escalatorReluctance": 1.5 + "escalator": { + "reluctance": 1.5, + "speed": 0.45 + } }, "waitReluctance": 1.0, "otherThanPreferredRoutesPenalty": 300, diff --git a/doc/user/RouteRequest.md b/doc/user/RouteRequest.md index ea3d0d12c74..5b428ff9175 100644 --- a/doc/user/RouteRequest.md +++ b/doc/user/RouteRequest.md @@ -155,12 +155,14 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe |    [routes](#rd_unpreferred_routes) | `feed-scoped-id[]` | The ids of the routes that incur an extra cost when being used. Format: `FeedId:RouteId` | *Optional* | | 2.2 | | walk | `object` | Walking preferences. | *Optional* | | 2.5 | |    boardCost | `integer` | Prevents unnecessary transfers by adding a cost for boarding a vehicle. This is the cost that is used when boarding while walking. | *Optional* | `600` | 2.0 | -|    escalatorReluctance | `double` | A multiplier for how bad being in an escalator is compared to being in transit for equal lengths of time | *Optional* | `1.5` | 2.4 | |    [reluctance](#rd_walk_reluctance) | `double` | A multiplier for how bad walking is, compared to being in transit for equal lengths of time. | *Optional* | `2.0` | 2.0 | |    [safetyFactor](#rd_walk_safetyFactor) | `double` | Factor for how much the walk safety is considered in routing. | *Optional* | `1.0` | 2.2 | |    speed | `double` | The user's walking speed in meters/second. | *Optional* | `1.33` | 2.0 | |    stairsReluctance | `double` | Used instead of walkReluctance for stairs. | *Optional* | `2.0` | 2.0 | |    [stairsTimeFactor](#rd_walk_stairsTimeFactor) | `double` | How much more time does it take to walk a flight of stairs compared to walking a similar horizontal length. | *Optional* | `3.0` | 2.1 | +|    escalator | `object` | Escalator preferences. | *Optional* | | 2.7 | +|       reluctance | `double` | A multiplier for how bad being in an escalator is compared to being in transit for equal lengths of time | *Optional* | `1.5` | 2.4 | +|       [speed](#rd_walk_escalator_speed) | `double` | How fast does an escalator move horizontally? | *Optional* | `0.45` | 2.7 | | wheelchairAccessibility | `object` | See [Wheelchair Accessibility](Accessibility.md) | *Optional* | | 2.2 | |    enabled | `boolean` | Enable wheelchair accessibility. | *Optional* | `false` | 2.0 | |    inaccessibleStreetReluctance | `double` | The factor to multiply the cost of traversing a street edge that is not wheelchair-accessible. | *Optional* | `25.0` | 2.2 | @@ -1105,6 +1107,15 @@ Default value is based on: Fujiyama, T., & Tyler, N. (2010). Predicting the walk speed of pedestrians on stairs. Transportation Planning and Technology, 33(2), 177–202. +

speed

+ +**Since version:** `2.7` ∙ **Type:** `double` ∙ **Cardinality:** `Optional` ∙ **Default value:** `0.45` +**Path:** /routingDefaults/walk/escalator + +How fast does an escalator move horizontally? + +Horizontal speed of escalator in m/s. +

maxSlope

**Since version:** `2.0` ∙ **Type:** `double` ∙ **Cardinality:** `Optional` ∙ **Default value:** `0.083` @@ -1214,7 +1225,10 @@ include stairs as a last result. "reluctance" : 4.0, "stairsReluctance" : 1.65, "boardCost" : 600, - "escalatorReluctance" : 1.5 + "escalator" : { + "reluctance" : 1.5, + "speed" : 0.45 + } }, "waitReluctance" : 1.0, "otherThanPreferredRoutesPenalty" : 300, diff --git a/doc/user/RouterConfiguration.md b/doc/user/RouterConfiguration.md index 6dbd1174397..7dae97fd74c 100644 --- a/doc/user/RouterConfiguration.md +++ b/doc/user/RouterConfiguration.md @@ -528,7 +528,10 @@ Used to group requests when monitoring OTP. "reluctance" : 4.0, "stairsReluctance" : 1.65, "boardCost" : 600, - "escalatorReluctance" : 1.5 + "escalator" : { + "reluctance" : 1.5, + "speed" : 0.45 + } }, "waitReluctance" : 1.0, "otherThanPreferredRoutesPenalty" : 300,