Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better escalator duration control: specific duration from OSM duration tag, default speed from build-config.json #6268

Merged
merged 17 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,9 +17,19 @@
class EscalatorProcessor {

private final Map<Long, IntersectionVertex> intersectionNodes;
private final DataImportIssueStore issueStore;

public EscalatorProcessor(Map<Long, IntersectionVertex> 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<Long, IntersectionVertex> intersectionNodes,
DataImportIssueStore issueStore
) {
this.intersectionNodes = intersectionNodes;
this.issueStore = issueStore;
}

public void buildEscalatorEdge(OsmWay escalatorWay, double length) {
Expand All @@ -27,30 +41,58 @@ public void buildEscalatorEdge(OsmWay escalatorWay, double length) {
.boxed()
.toList();

Optional<Duration> duration = escalatorWay.getDuration(v ->
issueStore.add(
Issue.issue(
"InvalidDuration",
"Duration for osm node %d is not a valid duration: '%s'; it's replaced with 'Optional.empty()' (unknown).",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could use a link to OSM instead of just referencing the id (nodes have url() method). Also, the "it's replaced with 'Optional.empty()'" is too technical for the issue report. Instead you could just say something like "the value is ignored".

escalatorWay.getId(),
v
)
)
);
if (duration.isPresent()) {
double speed = length / duration.get().toSeconds();
if (speed < SLOW_ESCALATOR_ERROR_CUTOFF || speed > FAST_ESCALATOR_ERROR_CUTOFF) {
duration = Optional.empty();
tkalvas marked this conversation as resolved.
Show resolved Hide resolved
issueStore.add(
Issue.issue(
"InvalidDuration",
"Duration for osm node {} makes implied speed {} be outside acceptable range.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use URL here as well.

escalatorWay.getId(),
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)
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ protected Collection<KeyValue> map(Edge input) {
List<KeyValue> 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())
tkalvas marked this conversation as resolved.
Show resolved Hide resolved
);
default -> List.of();
};
return ListUtils.combine(baseProps, properties);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

import gnu.trove.list.TLongList;
import gnu.trove.list.array.TLongArrayList;
import java.time.Duration;
import java.time.format.DateTimeParseException;
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;
import org.opentripplanner.utils.time.DurationUtils;
tkalvas marked this conversation as resolved.
Show resolved Hide resolved

public class OsmWay extends OsmWithTags {

Expand Down Expand Up @@ -130,6 +135,10 @@ public boolean isEscalator() {
return (isTag("highway", "steps") && isOneOfTags("conveying", ESCALATOR_CONVEYING_TAGS));
}

public Optional<Duration> getDuration(Consumer<String> errorHandler) {
return getTagAsDuration("duration", errorHandler);
}

public boolean isForwardEscalator() {
return isEscalator() && "forward".equals(this.getTag("conveying"));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -221,6 +223,88 @@ public OptionalInt getTagAsInt(String tag, Consumer<String> errorHandler) {
return OptionalInt.empty();
}

/**
* Parse an OSM duration tag, which is one of:
tkalvas marked this conversation as resolved.
Show resolved Hide resolved
* mm
* hh:mm
* hh:mm:ss
* and where the leading value is not limited to any maximum.
* @param duration string in format mm, hh:mm, or hh:mm:ss
tkalvas marked this conversation as resolved.
Show resolved Hide resolved
* @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 0: // case "m"
tkalvas marked this conversation as resolved.
Show resolved Hide resolved
minutes = Long.parseLong(duration);
if (minutes >= 0) {
return Duration.ofMinutes(minutes);
}
break;
case 1: // case "h:mm"
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;
default: // case "h:mm:ss"
//case 2:
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 clock duration", duration, 0);
tkalvas marked this conversation as resolved.
Show resolved Hide resolved
}

public Optional<Duration> getTagAsDuration(String tag, Consumer<String> errorHandler) {
optionsome marked this conversation as resolved.
Show resolved Hide resolved
String value = getTag(tag);
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".
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public final class WalkPreferences implements Serializable {
private final double safetyFactor;

private final double escalatorReluctance;
private final double escalatorSpeed;

private WalkPreferences() {
this.speed = 1.33;
Expand All @@ -39,6 +40,7 @@ private WalkPreferences() {
this.stairsTimeFactor = 3.0;
this.safetyFactor = 1.0;
this.escalatorReluctance = 1.5;
this.escalatorSpeed = 0.45;
}

private WalkPreferences(Builder builder) {
Expand All @@ -49,6 +51,7 @@ private WalkPreferences(Builder builder) {
this.stairsTimeFactor = Units.reluctance(builder.stairsTimeFactor);
this.safetyFactor = Units.reluctance(builder.safetyFactor);
this.escalatorReluctance = Units.reluctance(builder.escalatorReluctance);
this.escalatorSpeed = Units.speed(builder.escalatorSpeed);
}

public static Builder of() {
Expand Down Expand Up @@ -108,6 +111,14 @@ public double safetyFactor() {
return safetyFactor;
}

public double escalatorReluctance() {
return escalatorReluctance;
}

public double escalatorSpeed() {
return escalatorSpeed;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand All @@ -120,7 +131,8 @@ public boolean equals(Object o) {
doubleEquals(that.stairsReluctance, stairsReluctance) &&
doubleEquals(that.stairsTimeFactor, stairsTimeFactor) &&
doubleEquals(that.safetyFactor, safetyFactor) &&
doubleEquals(that.escalatorReluctance, escalatorReluctance)
doubleEquals(that.escalatorReluctance, escalatorReluctance) &&
doubleEquals(that.escalatorSpeed, escalatorSpeed)
);
}

Expand All @@ -133,7 +145,8 @@ public int hashCode() {
stairsReluctance,
stairsTimeFactor,
safetyFactor,
escalatorReluctance
escalatorReluctance,
escalatorSpeed
);
}

Expand All @@ -148,13 +161,10 @@ public String toString() {
.addNum("stairsTimeFactor", stairsTimeFactor, DEFAULT.stairsTimeFactor)
.addNum("safetyFactor", safetyFactor, DEFAULT.safetyFactor)
.addNum("escalatorReluctance", escalatorReluctance, DEFAULT.escalatorReluctance)
.addNum("escalatorSpeed", escalatorSpeed, DEFAULT.escalatorSpeed)
.toString();
}

public double escalatorReluctance() {
return escalatorReluctance;
}

public static class Builder {

private final WalkPreferences original;
Expand All @@ -166,6 +176,7 @@ public static class Builder {
private double safetyFactor;

private double escalatorReluctance;
private double escalatorSpeed;

public Builder(WalkPreferences original) {
this.original = original;
Expand All @@ -176,6 +187,7 @@ public Builder(WalkPreferences original) {
this.stairsTimeFactor = original.stairsTimeFactor;
this.safetyFactor = original.safetyFactor;
this.escalatorReluctance = original.escalatorReluctance;
this.escalatorSpeed = original.escalatorSpeed;
}

public WalkPreferences original() {
Expand Down Expand Up @@ -251,6 +263,15 @@ public Builder withEscalatorReluctance(double escalatorReluctance) {
return this;
}

public double escalatorSpeed() {
return escalatorSpeed;
}

public Builder withEscalatorSpeed(double escalatorSpeed) {
this.escalatorSpeed = escalatorSpeed;
return this;
}

public Builder apply(Consumer<Builder> body) {
body.accept(this);
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -817,6 +818,14 @@ private static void mapWalkPreferences(NodeAdapter root, WalkPreferences.Builder
"A multiplier for how bad being in an escalator is compared to being in transit for equal lengths of time"
)
.asDouble(dft.escalatorReluctance())
)
.withEscalatorSpeed(
c
.of("escalatorSpeed")
.since(V2_7)
.summary("How fast does an escalator move horizontally?")
.description("Horizontal speed of escalator in m/s.")
.asDouble(dft.escalatorSpeed())
);
}
}
Loading
Loading