From 9345d6fdaba5c008893b5a728167cbd03f38de3d Mon Sep 17 00:00:00 2001 From: Zsombor Welker Date: Thu, 3 Feb 2022 19:12:20 +0100 Subject: [PATCH] Parameterize elevation propagation The existing logic is modified so that is deterministic and tested. The new logic interpolates the elevation for each vertex that is within maxElevationPropagationDistance on the street network using the two closest vertices with elevation. --- docs/BuildConfiguration.md | 1 + .../graph_builder/GraphBuilder.java | 1 + .../module/ned/ElevationModule.java | 247 ++-------------- .../module/ned/MissingElevationHandler.java | 276 ++++++++++++++++++ .../mapping/StatesToWalkStepsMapper.java | 6 +- .../opentripplanner/routing/graph/Vertex.java | 32 +- .../standalone/config/BuildConfig.java | 7 + .../ned/MissingElevationHandlerTest.java | 225 ++++++++++++++ .../__snapshots__/ElevationSnapshotTest.snap | 177 +++++------ 9 files changed, 633 insertions(+), 339 deletions(-) create mode 100644 src/main/java/org/opentripplanner/graph_builder/module/ned/MissingElevationHandler.java create mode 100644 src/test/java/org/opentripplanner/graph_builder/module/ned/MissingElevationHandlerTest.java diff --git a/docs/BuildConfiguration.md b/docs/BuildConfiguration.md index f5df3b56d20..fc6a6a29f38 100644 --- a/docs/BuildConfiguration.md +++ b/docs/BuildConfiguration.md @@ -19,6 +19,7 @@ This table lists all the JSON properties that can be defined in a `build-config. | `matchBusRoutesToStreets` | Based on GTFS shape data, guess which OSM streets each bus runs on to improve stop linking | boolean | false | | | `maxAreaNodes` | Visibility calculations for an area will not be done if there are more nodes than this limit | integer | 500 | | | `maxDataImportIssuesPerFile` | If number of data import issues is larger then specified maximum number of issues the report will be split in multiple files | int | 1,000 | | +| `maxElevationPropagationMeters` | The maximum distance to propagate elevation to vertices which have no elevation. | int | 2,000 | see [Elevation Data](#elevation-data) | | `maxInterlineDistance` | Maximal distance between stops in meters that will connect consecutive trips that are made with same vehicle | int | 200 | units: meters | | `maxStopToShapeSnapDistance` | This field is used for mapping route's geometry shapes. It determines max distance between shape points and their stop sequence. If the mapper can not find any stops within this radius it will default to simple stop-to-stop geometry instead. | double | 150 | units: meters | | `maxTransferDurationSeconds` | Transfers up to this duration in seconds will be pre-calculated and included in the Graph | double | 1800 | units: seconds | diff --git a/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java b/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java index 2ed09432c24..57d764165ef 100644 --- a/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java +++ b/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java @@ -204,6 +204,7 @@ public static GraphBuilder create( config.writeCachedElevations, config.elevationUnitMultiplier, config.distanceBetweenElevationSamples, + config.maxElevationPropagationMeters, config.includeEllipsoidToGeoidDifference, config.multiThreadElevationCalculations ) diff --git a/src/main/java/org/opentripplanner/graph_builder/module/ned/ElevationModule.java b/src/main/java/org/opentripplanner/graph_builder/module/ned/ElevationModule.java index 6b70df3baf9..7f20e2e11f5 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/ned/ElevationModule.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/ned/ElevationModule.java @@ -13,9 +13,9 @@ import java.time.Duration; import java.time.Instant; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import org.geotools.geometry.DirectPosition2D; @@ -27,11 +27,9 @@ import org.opengis.referencing.operation.TransformException; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; -import org.opentripplanner.common.pqueue.BinHeap; import org.opentripplanner.graph_builder.DataImportIssueStore; import org.opentripplanner.graph_builder.issues.ElevationFlattened; import org.opentripplanner.graph_builder.issues.ElevationProfileFailure; -import org.opentripplanner.graph_builder.issues.ElevationPropagationLimit; import org.opentripplanner.graph_builder.issues.Graphwide; import org.opentripplanner.graph_builder.module.extra_elevation_data.ElevationPoint; import org.opentripplanner.graph_builder.services.GraphBuilderModule; @@ -70,6 +68,7 @@ public class ElevationModule implements GraphBuilderModule { private final boolean writeCachedElevations; /* The file of cached elevations */ private final File cachedElevationsFile; + private final double maxElevationPropagationMeters; /* Whether or not to include geoid difference values in individual elevation calculations */ private final boolean includeEllipsoidToGeoidDifference; /* @@ -125,6 +124,7 @@ public ElevationModule(ElevationGridCoverageFactory factory) { false, 1, 10, + 2000, true, false ); @@ -137,6 +137,7 @@ public ElevationModule( boolean writeCachedElevations, double elevationUnitMultiplier, double distanceBetweenSamplesM, + double maxElevationPropagationMeters, boolean includeEllipsoidToGeoidDifference, boolean multiThreadElevationCalculations ) { @@ -145,6 +146,7 @@ public ElevationModule( this.readCachedElevations = readCachedElevations; this.writeCachedElevations = writeCachedElevations; this.elevationUnitMultiplier = elevationUnitMultiplier; + this.maxElevationPropagationMeters = maxElevationPropagationMeters; this.includeEllipsoidToGeoidDifference = includeEllipsoidToGeoidDifference; this.multiThreadElevationCalculations = multiThreadElevationCalculations; this.distanceBetweenSamplesM = distanceBetweenSamplesM; @@ -250,13 +252,18 @@ public void buildGraph( out.writeObject(newCachedElevations); out.close(); } catch (IOException e) { - LOG.error(e.getMessage()); - issueStore.add(new Graphwide("Failed to write cached elevation file!")); + issueStore.add(new Graphwide("Failed to write cached elevation file: " + e.getMessage())); } } + @SuppressWarnings("unchecked") - HashMap extraElevation = (HashMap) extra.get(ElevationPoint.class); - assignMissingElevations(graph, edgesWithCalculatedElevations, extraElevation); + Map extraElevation = (Map) extra.get(ElevationPoint.class); + var elevationsForVertices = collectKnownElevationsForVertices(extraElevation, edgesWithCalculatedElevations); + + new MissingElevationHandler( + issueStore, elevationsForVertices, maxElevationPropagationMeters + ) + .run(); updateElevationMetadata(graph); @@ -271,244 +278,50 @@ private void updateElevationMetadata(Graph graph) { } } - static class ElevationRepairState { - /* This uses an intuitionist approach to elevation inspection */ - public StreetEdge backEdge; - - public ElevationRepairState backState; - - public Vertex vertex; - - public double distance; - - public double initialElevation; - - public ElevationRepairState(StreetEdge backEdge, ElevationRepairState backState, - Vertex vertex, double distance, double initialElevation) { - this.backEdge = backEdge; - this.backState = backState; - this.vertex = vertex; - this.distance = distance; - this.initialElevation = initialElevation; - } - } - - /** - * Assign missing elevations by interpolating from nearby points with known - * elevation; also handle osm ele tags - */ - private void assignMissingElevations(Graph graph, List edgesWithElevation, HashMap knownElevations) { - - LOG.debug("Assigning missing elevations"); - - BinHeap pq = new BinHeap<>(); - - // elevation for each vertex (known or interpolated) + private Map collectKnownElevationsForVertices( + Map knownElevations, + List edgesWithElevation + ) { // knownElevations will be null if there are no ElevationPoints in the data // for instance, with the Shapefile loader.) - HashMap elevations; - if (knownElevations != null) - elevations = (HashMap) knownElevations.clone(); - else - elevations = new HashMap<>(); - - // If including the EllipsoidToGeoidDifference, subtract these from the known elevations found in OpenStreetMap - // data. + var elevations = knownElevations != null + ? new HashMap<>(knownElevations) + : new HashMap(); + + // If including the EllipsoidToGeoidDifference, subtract these from the known elevations + // found in OpenStreetMap data. if (includeEllipsoidToGeoidDifference) { elevations.forEach((vertex, elevation) -> { try { elevations.put( - vertex, elevation - getApproximateEllipsoidToGeoidDifference(vertex.getY(), vertex.getX()) + vertex, elevation - getApproximateEllipsoidToGeoidDifference(vertex.getY(), vertex.getX()) ); } catch (TransformException e) { LOG.error( - "Error processing elevation for known elevation at vertex: {} due to error: {}", - vertex, - e + "Error processing elevation for known elevation at vertex: {} due to error: {}", + vertex, + e ); } }); } - HashSet closed = new HashSet<>(); - - // initialize queue with all vertices which already have known elevation + // add all vertices which known elevation for (StreetEdge e : edgesWithElevation) { PackedCoordinateSequence profile = e.getElevationProfile(); if (!elevations.containsKey(e.getFromVertex())) { double firstElevation = profile.getOrdinate(0, 1); - ElevationRepairState state = new ElevationRepairState( - null, - null, - e.getFromVertex(), - 0, - firstElevation - ); - pq.insert(state, 0); elevations.put(e.getFromVertex(), firstElevation); } if (!elevations.containsKey(e.getToVertex())) { double lastElevation = profile.getOrdinate(profile.size() - 1, 1); - ElevationRepairState state = new ElevationRepairState( - null, - null, - e.getToVertex(), - 0, - lastElevation - ); - pq.insert(state, 0); elevations.put(e.getToVertex(), lastElevation); } } - // Grow an SPT outward from vertices with known elevations into regions where the - // elevation is not known. when a branch hits a region with known elevation, follow the - // back pointers through the region of unknown elevation, setting elevations via interpolation. - while (!pq.empty()) { - ElevationRepairState state = pq.extract_min(); - - if (closed.contains(state.vertex)) continue; - closed.add(state.vertex); - - ElevationRepairState curState = state; - Vertex initialVertex = null; - while (curState != null) { - initialVertex = curState.vertex; - curState = curState.backState; - } - - double bestDistance = Double.MAX_VALUE; - double bestElevation = 0; - for (Edge e : state.vertex.getOutgoing()) { - if (!(e instanceof StreetEdge)) { - continue; - } - StreetEdge edge = (StreetEdge) e; - Vertex tov = e.getToVertex(); - if (tov == initialVertex) - continue; - - Double elevation = elevations.get(tov); - if (elevation != null) { - double distance = e.getDistanceMeters(); - if (distance < bestDistance) { - bestDistance = distance; - bestElevation = elevation; - } - } else { - // continue - ElevationRepairState newState = new ElevationRepairState( - edge, - state, - tov, - e.getDistanceMeters() + state.distance, - state.initialElevation - ); - pq.insert(newState, e.getDistanceMeters() + state.distance); - } - } // end loop over outgoing edges - - for (Edge e : state.vertex.getIncoming()) { - if (!(e instanceof StreetEdge)) { - continue; - } - StreetEdge edge = (StreetEdge) e; - Vertex fromv = e.getFromVertex(); - if (fromv == initialVertex) - continue; - Double elevation = elevations.get(fromv); - if (elevation != null) { - double distance = e.getDistanceMeters(); - if (distance < bestDistance) { - bestDistance = distance; - bestElevation = elevation; - } - } else { - // continue - ElevationRepairState newState = new ElevationRepairState( - edge, - state, - fromv, - e.getDistanceMeters() + state.distance, - state.initialElevation - ); - pq.insert(newState, e.getDistanceMeters() + state.distance); - } - } // end loop over incoming edges - - //limit elevation propagation to at max 2km; this prevents an infinite loop - //in the case of islands missing elevation (and some other cases) - if (bestDistance == Double.MAX_VALUE && state.distance > 2000) { - issueStore.add(new ElevationPropagationLimit(state.vertex)); - bestDistance = state.distance; - bestElevation = state.initialElevation; - } - if (bestDistance != Double.MAX_VALUE) { - // we have found a second vertex with elevation, so we can interpolate the elevation - // for this point - double totalDistance = bestDistance + state.distance; - // trace backwards, setting states as we go - while (true) { - // watch out for division by 0 here, which will propagate NaNs - // all the way out to edge lengths - if (totalDistance == 0) - elevations.put(state.vertex, bestElevation); - else { - double elevation = (bestElevation * state.distance + - state.initialElevation * bestDistance) / totalDistance; - elevations.put(state.vertex, elevation); - } - if (state.backState == null) - break; - bestDistance += state.backEdge.getDistanceMeters(); - state = state.backState; - if (elevations.containsKey(state.vertex)) - break; - } - - } - } // end loop over states - - // do actual assignments - for (Vertex v : graph.getVertices()) { - Double fromElevation = elevations.get(v); - for (Edge e : v.getOutgoing()) { - if (e instanceof StreetEdge) { - StreetEdge edge = ((StreetEdge) e); - - Double toElevation = elevations.get(edge.getToVertex()); - - if (fromElevation == null || toElevation == null) { - if (!edge.isElevationFlattened() && !edge.isSlopeOverride()) { - issueStore.add(new ElevationProfileFailure(edge, "Failed to propagate elevation data")); - } - continue; - } - - if (edge.getElevationProfile() != null && edge.getElevationProfile().size() > 2) { - continue; - } - - Coordinate[] coords = new Coordinate[2]; - coords[0] = new Coordinate(0, fromElevation); - coords[1] = new Coordinate(edge.getDistanceMeters(), toElevation); - - PackedCoordinateSequence profile = new PackedCoordinateSequence.Double(coords); - - try { - StreetElevationExtension.addToEdge(edge, profile, true); - - if (edge.isElevationFlattened()) { - issueStore.add(new ElevationFlattened(edge)); - } - } catch (Exception ex) { - issueStore.add(new ElevationProfileFailure(edge, ex.getMessage())); - } - } - } - } + return elevations; } /** diff --git a/src/main/java/org/opentripplanner/graph_builder/module/ned/MissingElevationHandler.java b/src/main/java/org/opentripplanner/graph_builder/module/ned/MissingElevationHandler.java new file mode 100644 index 00000000000..415650890da --- /dev/null +++ b/src/main/java/org/opentripplanner/graph_builder/module/ned/MissingElevationHandler.java @@ -0,0 +1,276 @@ +package org.opentripplanner.graph_builder.module.ned; + +import java.util.HashMap; +import java.util.Map; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.impl.PackedCoordinateSequence; +import org.opentripplanner.common.pqueue.BinHeap; +import org.opentripplanner.graph_builder.DataImportIssueStore; +import org.opentripplanner.graph_builder.issues.ElevationFlattened; +import org.opentripplanner.graph_builder.issues.ElevationProfileFailure; +import org.opentripplanner.routing.edgetype.StreetEdge; +import org.opentripplanner.routing.edgetype.StreetElevationExtension; +import org.opentripplanner.routing.graph.Edge; +import org.opentripplanner.routing.graph.Vertex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Assigns elevation to edges which don't yet have elevation set, which may be for example tunnels, + * bridges or islands. + * + * Elevation may be missing from a {@link StreetEdge} for two reasons: + * 1. the source DEM files contained no data for the whole geometry + * 2. {@link StreetEdge#isSlopeOverride()} is set + * + * The elevation for missing edges is set through its vertices, with the elevation for the from/to + * vertices being used to set the elevation profile. + * 0. The source elevations are determined for vertices using edges with an existing elevation + * profile, along with values from the {@code ele} tag + * 1. All vertices within {@code maxElevationPropagationMeters} of vertices with elevation + * without elevation are visited + * 2. Foreach vertex without elevation the first two paths from a vertex with elevation are used + * to interpolate elevations + * 3. If a vertex only had a single path, then the last known elevation is used + * 4. Once elevations for vertices are interpolated they are used to set the elevation profile + * for the incoming / outgoing StreetEdges + */ +class MissingElevationHandler { + + private static final Logger LOG = LoggerFactory.getLogger(MissingElevationHandler.class); + + + private final DataImportIssueStore issueStore; + private final Map existingElevationForVertices; + private final double maxElevationPropagationMeters; + + public MissingElevationHandler( + DataImportIssueStore issueStore, + Map existingElevationsForVertices, + double maxElevationPropagationMeters + ) { + this.issueStore = issueStore; + this.existingElevationForVertices = existingElevationsForVertices; + this.maxElevationPropagationMeters = maxElevationPropagationMeters; + } + + /** + * Assign missing elevations by interpolating from nearby points with known + * elevation; also handle osm ele tags + */ + void run() { + + LOG.debug("Assigning missing elevations"); + + // elevation for each vertex (known or interpolated) + var pq = createPriorityQueue(existingElevationForVertices); + var elevations = new HashMap<>(existingElevationForVertices); + + // Grow an SPT outward from vertices with known elevations into regions where the + // elevation is not known. When branches meet, follow the back pointers through the region + // of unknown elevation, setting elevations via interpolation. + propagateElevationToNearbyVertices(pq, elevations); + + // Assign elevations to street edges based on the vertices + elevations.keySet().forEach(vertex -> { + vertex.getIncomingStreetEdges().forEach(edge -> assignElevationToEdgeIfPossible(elevations, edge)); + vertex.getOutgoingStreetEdges().forEach(edge -> assignElevationToEdgeIfPossible(elevations, edge)); + }); + } + + private BinHeap createPriorityQueue(Map elevations) { + var pq = new BinHeap(); + + elevations.forEach(( + (vertex, elevation) -> { + vertex.getIncoming().forEach(edge -> { + if (edge.getDistanceMeters() < maxElevationPropagationMeters) { + pq.insert(new ElevationRepairState( + vertex, elevation, edge.getFromVertex(), + edge.getDistanceMeters() + ), edge.getDistanceMeters()); + } + }); + + vertex.getOutgoing().forEach(edge -> { + if (edge.getDistanceMeters() < maxElevationPropagationMeters) { + pq.insert(new ElevationRepairState( + vertex, elevation, edge.getToVertex(), + edge.getDistanceMeters() + ), edge.getDistanceMeters()); + } + }); + } + )); + + return pq; + } + + private void propagateElevationToNearbyVertices(BinHeap pq, Map elevations) { + // Stores vertices without elevation which were visited by a single path from a vertices with elevation + var pending = new HashMap(); + + while (!pq.empty()) { + ElevationRepairState currentState = pq.extract_min(); + if (elevations.containsKey(currentState.currentVertex)) { + continue; + } + + if (pending.containsKey(currentState.currentVertex)) { + var otherState = pending.get(currentState.currentVertex); + if (otherState.initialVertex != currentState.currentVertex) { + interpolateElevationsAlongBackPath(otherState, currentState, elevations, pending); + interpolateElevationsAlongBackPath(currentState, otherState, elevations, pending); + } else { + continue; + } + } else { + pending.put(currentState.currentVertex, currentState); + } + + for (Edge e : currentState.currentVertex.getIncoming()) { + var nsVertex = e.getFromVertex(); + var nsDistance = currentState.distance + e.getDistanceMeters(); + if (elevations.containsKey(nsVertex) || nsDistance > maxElevationPropagationMeters) { + continue; + } + + pq.insert(new ElevationRepairState(currentState, nsVertex, nsDistance), nsDistance); + } + + for (Edge e : currentState.currentVertex.getOutgoing()) { + var nsVertex = e.getToVertex(); + var nsDistance = currentState.distance + e.getDistanceMeters(); + if (elevations.containsKey(nsVertex) || nsDistance > maxElevationPropagationMeters) { + continue; + } + + pq.insert(new ElevationRepairState(currentState, nsVertex, nsDistance), nsDistance); + } + } + + // Copy elevation to all pending vertices (where only one path with an initial elevation was found) + pending.forEach((vertex, elevationRepairState) -> { + if (!elevations.containsKey(vertex)) { + elevations.put(vertex, lastKnowElevationForState(elevationRepairState, elevations)); + } + }); + } + + private Double lastKnowElevationForState( + ElevationRepairState elevationRepairState, + Map elevations + ) { + var backState = elevationRepairState.previousState; + while (backState != null) { + if (elevations.containsKey(backState.currentVertex)) { + return elevations.get(backState.currentVertex); + } + backState = backState.previousState; + } + + return elevationRepairState.initialElevation; + } + + private void interpolateElevationsAlongBackPath( + ElevationRepairState stateToBackTrack, + ElevationRepairState alternateState, + Map elevations, + Map pending + ) { + var elevationDiff = alternateState.initialElevation - stateToBackTrack.initialElevation; + var totalDistance = stateToBackTrack.distance + alternateState.distance; + + var currentState = stateToBackTrack; + while (currentState != null) { + if (!elevations.containsKey(currentState.currentVertex)) { + var elevation = currentState.initialElevation + + elevationDiff * (currentState.distance / totalDistance); + elevation = Math.round(elevation * 10) / 10.d; + + elevations.put(currentState.currentVertex, elevation); + pending.remove(currentState.currentVertex); + } + + currentState = currentState.previousState; + } + } + + private void assignElevationToEdgeIfPossible(Map elevations, StreetEdge edge) { + if (edge.getElevationProfile() != null) { + return; + } + + Double fromElevation = elevations.get(edge.getFromVertex()); + Double toElevation = elevations.get(edge.getToVertex()); + + if (fromElevation == null || toElevation == null) { + if (!edge.isElevationFlattened() && !edge.isSlopeOverride()) { + issueStore.add(new ElevationProfileFailure(edge, + "Failed to propagate elevation data" + )); + } + return; + } + + Coordinate[] coords = new Coordinate[]{ + new Coordinate(0, fromElevation), + new Coordinate(edge.getDistanceMeters(), toElevation) + }; + + PackedCoordinateSequence profile = new PackedCoordinateSequence.Double(coords); + + try { + StreetElevationExtension.addToEdge(edge, profile, true); + + if (edge.isElevationFlattened()) { + issueStore.add(new ElevationFlattened(edge)); + } + } catch (Exception ex) { + issueStore.add(new ElevationProfileFailure(edge, ex.getMessage())); + } + } + + private static class ElevationRepairState { + final ElevationRepairState previousState;; + final Vertex initialVertex; + final Double initialElevation; + final Vertex currentVertex; + final Double distance; + + ElevationRepairState( + ElevationRepairState previousState, + Vertex currentVertex, + Double distance + ) { + this.previousState = previousState; + this.initialVertex = previousState.initialVertex; + this.initialElevation = previousState.initialElevation; + this.currentVertex = currentVertex; + this.distance = distance; + } + + ElevationRepairState( + Vertex initialVertex, + Double initialElevation, + Vertex currentVertex, + Double distance + ) { + this.previousState = null; + this.initialVertex = initialVertex; + this.initialElevation = initialElevation; + this.currentVertex = currentVertex; + this.distance = distance; + } + + @Override + public String toString() { + return "ElevationRepairState{" + + "initialVertex=" + initialVertex + + ", initialElevation=" + initialElevation + + ", currentVertex=" + currentVertex + + ", distance=" + distance + + '}'; + } + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java index 9f241edccc9..b8914170331 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/StatesToWalkStepsMapper.java @@ -330,7 +330,7 @@ private boolean isPossibleToTurnToOtherStreet( if (edge instanceof StreetEdge) { // the next edges will be PlainStreetEdges, we hope double angleDiff = getAbsoluteAngleDiff(thisAngle, lastAngle); - for (Edge alternative : backState.getVertex().getOutgoingStreetEdges()) { + for (StreetEdge alternative : backState.getVertex().getOutgoingStreetEdges()) { if (isTurnToOtherStreet(streetName, angleDiff, alternative)) { return true; } @@ -341,8 +341,8 @@ private boolean isPossibleToTurnToOtherStreet( // FIXME: this code might be wrong with the removal of the edge-based graph State twoStatesBack = backState.getBackState(); Vertex backVertex = twoStatesBack.getVertex(); - for (Edge alternative : backVertex.getOutgoingStreetEdges()) { - for (Edge innerAlternative : alternative.getToVertex().getOutgoingStreetEdges()) { + for (StreetEdge alternative : backVertex.getOutgoingStreetEdges()) { + for (StreetEdge innerAlternative : alternative.getToVertex().getOutgoingStreetEdges()) { if (isTurnToOtherStreet(streetName, angleDiff, innerAlternative)) { return true; } diff --git a/src/main/java/org/opentripplanner/routing/graph/Vertex.java b/src/main/java/org/opentripplanner/routing/graph/Vertex.java index 96d31c2d893..e7916cdcc47 100644 --- a/src/main/java/org/opentripplanner/routing/graph/Vertex.java +++ b/src/main/java/org/opentripplanner/routing/graph/Vertex.java @@ -1,14 +1,5 @@ package org.opentripplanner.routing.graph; -import org.locationtech.jts.geom.Coordinate; -import org.opentripplanner.common.geometry.DirectionUtils; -import org.opentripplanner.model.StationElement; -import org.opentripplanner.routing.edgetype.StreetEdge; -import org.opentripplanner.util.I18NString; -import org.opentripplanner.util.NonLocalizedString; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; @@ -17,6 +8,14 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import org.locationtech.jts.geom.Coordinate; +import org.opentripplanner.common.geometry.DirectionUtils; +import org.opentripplanner.model.StationElement; +import org.opentripplanner.routing.edgetype.StreetEdge; +import org.opentripplanner.util.I18NString; +import org.opentripplanner.util.NonLocalizedString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A vertex in the graph. Each vertex has a longitude/latitude location, as well as a set of @@ -252,8 +251,19 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE /* UTILITY METHODS FOR SEARCHING, GRAPH BUILDING, AND GENERATING WALKSTEPS */ - public List getOutgoingStreetEdges() { - List result = new ArrayList<>(); + public List getIncomingStreetEdges() { + List result = new ArrayList<>(); + for (Edge out : this.getIncoming()) { + if (!(out instanceof StreetEdge)) { + continue; + } + result.add((StreetEdge) out); + } + return result; + } + + public List getOutgoingStreetEdges() { + List result = new ArrayList<>(); for (Edge out : this.getOutgoing()) { if (!(out instanceof StreetEdge)) { continue; diff --git a/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java b/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java index bee7f40d407..076e90665f1 100644 --- a/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java @@ -214,6 +214,12 @@ public class BuildConfig { */ public double distanceBetweenElevationSamples; + /** + * The maximum distance to propagate elevation to vertices which have no elevation. By default, this + * is 2000 meters. + */ + public double maxElevationPropagationMeters; + /** * When set to true (it is by default), the elevation module will attempt to read this file in order to reuse * calculations of elevation data for various coordinate sequences instead of recalculating them all over again. @@ -363,6 +369,7 @@ public BuildConfig(JsonNode node, String source, boolean logUnusedParams) { transitServiceEnd = c.asDateOrRelativePeriod( "transitServiceEnd", "P3Y"); writeCachedElevations = c.asBoolean("writeCachedElevations", false); maxAreaNodes = c.asInt("maxAreaNodes", 500); + maxElevationPropagationMeters = c.asInt("maxElevationPropagationMeters", 2000); // List of complex parameters fareServiceFactory = DefaultFareServiceFactory.fromConfig(c.asRawNode("fares")); diff --git a/src/test/java/org/opentripplanner/graph_builder/module/ned/MissingElevationHandlerTest.java b/src/test/java/org/opentripplanner/graph_builder/module/ned/MissingElevationHandlerTest.java new file mode 100644 index 00000000000..3a61b34192b --- /dev/null +++ b/src/test/java/org/opentripplanner/graph_builder/module/ned/MissingElevationHandlerTest.java @@ -0,0 +1,225 @@ +package org.opentripplanner.graph_builder.module.ned; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.ArrayList; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.impl.PackedCoordinateSequence; +import org.mockito.Mockito; +import org.opentripplanner.graph_builder.DataImportIssueStore; +import org.opentripplanner.routing.edgetype.StreetEdge; +import org.opentripplanner.routing.edgetype.StreetElevationExtension; +import org.opentripplanner.routing.edgetype.StreetTraversalPermission; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.graph.Vertex; +import org.opentripplanner.routing.vertextype.IntersectionVertex; +import org.opentripplanner.util.LocalizedStringFormat; + +class MissingElevationHandlerTest { + + private static final DataImportIssueStore issueStore = Mockito.mock(DataImportIssueStore.class); + + private Graph graph; + + private StreetEdge AB, BC, CA, AB2, AD, ED, AF, FG, GB, FH, CJ, JI, AI, BJ; + + private Map elevations; + + @BeforeEach + void setUp() { + IntersectionVertex A, B, C, D, E, F, G, H, I, J; + + graph = new Graph(); + A = vertex("A"); + B = vertex("B"); + C = vertex("C"); + + AB = edge(A, B, 50); + BC = edge(B, C, 50); + CA = edge(C, A, 50); + + // + AB2 = edge(A, B, 100); + + // A - D - E leaf + D = vertex("D"); + E = vertex("E"); + + AD = edge(A, D, 100); + ED = edge(E, D, 200); + + // loop from A - F - G - B + F = vertex("F"); + G = vertex("G"); + + AF = edge(A, F, 100); + FG = edge(F, G, 100); + GB = edge(G, B, 150); + + // leaf from F, which should use the interpolated elevation from F + H = vertex("H"); + FH = edge(F, H, 100); + + // complex path with multiple elevation points (A, B, C) dependent on edge lengths + I = vertex("I"); + J = vertex("J"); + + CJ = edge(C, J, 10); + JI = edge(J, I, 10); + AI = edge(A, I, 25); + BJ = edge(B, J, 30); + + elevations = Map.of(A, 100d, B, 200d, C, 300d); + + assignElevation(AB, elevations); + assignElevation(BC, elevations); + assignElevation(CA, elevations); + } + + @Test + void zeroPropagationDistance() { + var subject = new MissingElevationHandler(issueStore, elevations, 0); + + subject.run(); + + assertElevation(AB, 0, 100, 50, 200); + assertElevation(BC, 0, 200, 50, 300); + assertElevation(CA, 0, 300, 50, 100); + // The elevation for both A (original) and B (original) is known, so AB2 is assigned an elevation + assertElevation(AB2, 0, 100, 100, 200); + assertNullElevation(AD); + assertNullElevation(ED); + assertNullElevation(AF); + assertNullElevation(FG); + assertNullElevation(GB); + assertNullElevation(FH); + assertNullElevation(CJ); + assertNullElevation(JI); + assertNullElevation(AI); + assertNullElevation(BJ); + } + + @Test + void smallPropagationDistance() { + var subject = new MissingElevationHandler(issueStore, elevations, 20); + + subject.run(); + + assertElevation(AB, 0, 100, 50, 200); + assertElevation(BC, 0, 200, 50, 300); + assertElevation(CA, 0, 300, 50, 100); + // The elevation for both A (original) and B (original) is known, so AB2 is assigned an elevation + assertElevation(AB2, 0, 100, 100, 200); + assertNullElevation(AD); + assertNullElevation(ED); + assertNullElevation(AF); + assertNullElevation(FG); + assertNullElevation(GB); + assertNullElevation(FH); + assertElevation(CJ, 0, 300, 10, 300); + assertElevation(JI, 0, 300, 10, 300); + // The elevation for both A (original) and I (pending) is known, so AI is assigned an elevation + assertElevation(AI, 0, 100, 25, 300); + // The elevation for both B (original) and J (pending) is known, so BJ is assigned an elevation + assertElevation(BJ, 0, 200, 30, 300); + } + + @Test + void partialPropagationDistance() { + var subject = new MissingElevationHandler(issueStore, elevations, 150); + + subject.run(); + + assertElevation(AB, 0, 100, 50, 200); + assertElevation(BC, 0, 200, 50, 300); + assertElevation(CA, 0, 300, 50, 100); + // The elevation for both A (original) and B (original) is known, so AB2 is assigned an elevation + assertElevation(AB2, 0, 100, 100, 200); + // E has a pending elevation of 100 + assertElevation(AD, 0, 100, 100, 100); + assertNullElevation(ED); + // F has a pending elevation of 100 + assertElevation(AF, 0, 100, 100, 100); + assertNullElevation(FG); + assertNullElevation(GB); + assertNullElevation(FH); + // J is interpolated as 25/45 of A - C + assertElevation(CJ, 0, 300, 10, 255.6); + // I is interpolated as 35/45 of A - C + assertElevation(JI, 0, 255.6, 10, 211.1); + assertElevation(AI, 0, 100, 25, 211.1); + assertElevation(BJ, 0, 200, 30, 255.6); + } + + @Test + void fullPropagationDistance() { + var subject = new MissingElevationHandler(issueStore, elevations, 300); + + subject.run(); + + assertElevation(AB, 0, 100, 50, 200); + assertElevation(BC, 0, 200, 50, 300); + assertElevation(CA, 0, 300, 50, 100); + // The elevation for both A (original) and B (original) is known, so AB2 is assigned an elevation + assertElevation(AB2, 0, 100, 100, 200); + // E has a pending elevation of 100 + assertElevation(AD, 0, 100, 100, 100); + // E has a pending elevation of 100 + assertElevation(ED, 0, 100, 200, 100); + // F is interpolated as 100/350 of A - B + assertElevation(AF, 0, 100, 100, 128.6); + // G is interpolated as 200/350 of A - B + assertElevation(FG, 0, 128.6, 100, 157.1); + assertElevation(GB, 0, 157.1, 150, 200); + // H has a pending elevation of F + assertElevation(FH, 0, 128.6, 100, 128.6); + // J is interpolated as 25/45 of A - C + assertElevation(CJ, 0, 300, 10, 255.6); + // I is interpolated as 35/45 of A - C + assertElevation(JI, 0, 255.6, 10, 211.1); + assertElevation(AI, 0, 100, 25, 211.1); + assertElevation(BJ, 0, 200, 30, 255.6); + } + + private IntersectionVertex vertex(String A) { + return new IntersectionVertex(graph, A, 0, 0); + } + + private StreetEdge edge(IntersectionVertex from, IntersectionVertex to, double length) { + return new StreetEdge(from, to, null, + new LocalizedStringFormat("%s%s", from.getName(), to.getName()), length, + StreetTraversalPermission.ALL, false + ); + } + + private void assignElevation(StreetEdge edge, Map elevations) { + Double fromElevation = elevations.get(edge.getFromVertex()); + Double toElevation = elevations.get(edge.getToVertex()); + + Coordinate[] coords = new Coordinate[]{ + new Coordinate(0, fromElevation), + new Coordinate(edge.getDistanceMeters(), toElevation) + }; + + PackedCoordinateSequence profile = new PackedCoordinateSequence.Double(coords); + + StreetElevationExtension.addToEdge(edge, profile, true); + } + + private void assertNullElevation(StreetEdge edge) { + assertNull(edge.getElevationProfile()); + } + + private void assertElevation(StreetEdge edge, double... points) { + var expectedCoordinates = new ArrayList<>(); + for (int i = 0; i + 1 < points.length; i += 2) { + expectedCoordinates.add(new Coordinate(points[i], points[i + 1])); + } + + assertArrayEquals(expectedCoordinates.toArray(), edge.getElevationProfile().toCoordinateArray()); + } +} \ No newline at end of file diff --git a/src/test/java/org/opentripplanner/routing/algorithm/mapping/__snapshots__/ElevationSnapshotTest.snap b/src/test/java/org/opentripplanner/routing/algorithm/mapping/__snapshots__/ElevationSnapshotTest.snap index 24c90b475d2..2c57a36b63a 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/mapping/__snapshots__/ElevationSnapshotTest.snap +++ b/src/test/java/org/opentripplanner/routing/algorithm/mapping/__snapshots__/ElevationSnapshotTest.snap @@ -3,8 +3,8 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.accessBikeRe { "arrivedAtDestinationWithRentedBicycle": false, "duration": 1035, - "elevationGained": 5.009754028320298, - "elevationLost": 16.153650588989255, + "elevationGained": 5.009754028320302, + "elevationLost": 16.153650588989247, "endTime": "2009-10-21T23:32:25.000+00:00", "fare": { "details": { @@ -785,8 +785,8 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.accessBikeRe { "arrivedAtDestinationWithRentedBicycle": false, "duration": 1035, - "elevationGained": 5.009754028320298, - "elevationLost": 16.153650588989255, + "elevationGained": 5.009754028320302, + "elevationLost": 16.153650588989247, "endTime": "2009-10-21T23:45:25.000+00:00", "fare": { "details": { @@ -1244,8 +1244,8 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.accessBikeRe { "arrivedAtDestinationWithRentedBicycle": false, "duration": 1035, - "elevationGained": 5.009754028320298, - "elevationLost": 16.153650588989255, + "elevationGained": 5.009754028320302, + "elevationLost": 16.153650588989247, "endTime": "2009-10-21T23:54:25.000+00:00", "fare": { "details": { @@ -2026,8 +2026,8 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.accessBikeRe { "arrivedAtDestinationWithRentedBicycle": false, "duration": 1035, - "elevationGained": 5.009754028320298, - "elevationLost": 16.153650588989255, + "elevationGained": 5.009754028320302, + "elevationLost": 16.153650588989247, "endTime": "2009-10-22T00:03:25.000+00:00", "fare": { "details": { @@ -2490,15 +2490,15 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directBike=[ [ { "arrivedAtDestinationWithRentedBicycle": false, - "duration": 597, - "elevationGained": 0.7000018310546992, - "elevationLost": 7.728395080566415, + "duration": 724, + "elevationGained": 18.900001831054695, + "elevationLost": 30.648395080566416, "endTime": "2009-10-21T23:10:00.000+00:00", "fare": { "details": { }, "fare": { } }, - "generalizedCost": 1106, + "generalizedCost": 1374, "legs": [ { "agencyTimeZoneOffset": -25200000, @@ -2507,15 +2507,15 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directBike=[ "distance": 2635.313, "endTime": "2009-10-21T23:10:00.000+00:00", "from": { - "departure": "2009-10-21T23:00:03.000+00:00", + "departure": "2009-10-21T22:57:56.000+00:00", "lat": 45.52832, "lon": -122.70059, "name": "SW Johnson St. & NW 24th Ave. (P1)", "vertexType": "NORMAL" }, - "generalizedCost": 1106, + "generalizedCost": 1374, "interlineWithPreviousLeg": false, - "legElevation": "0,63.7,1,63.7,1,63.7,11,63.4,21,63.2,31,63.3,41,63.5,51,63.3,61,62.9,71,62.5,81,62.2,91,62.0,101,61.7,111,61.5,121,61.2,131,60.9,141,60.6,151,60.3,160,60.2,160,NaN,2078,NaN,2078,55.5,2112,55.5,2229,55.5,2272,55.5,2272,NaN,2635,NaN", + "legElevation": "0,63.7,1,63.7,1,63.7,11,63.4,21,63.2,31,63.3,41,63.5,51,63.3,61,62.9,71,62.5,81,62.2,91,62.0,101,61.7,111,61.5,121,61.2,131,60.9,141,60.6,151,60.3,160,60.2,240,60.2,1032,60.2,1190,55.5,1430,55.5,1506,40.4,1849,40.4,1919,37.3,1931,37.3,2078,55.5,2503,55.5,2503,NaN,2635,NaN", "legGeometry": { "length": 59, "points": "}f{tGv}{kVC?mCBmCDoCDmCDmCBoCDmCDkCDoCB_CBM@iGEE@MB[H}@@s@@GsKAkCCkCA}BCkE?EAcEoCB{BBS@K?wDD}@@I?IAECKMKOIKyAmBCCQScB`DeAzBOVYj@eCxECF{@eAq@nAcAzBeAbBKDy@Ou@a@k@]c@_@Ws@M{@BsA" @@ -2525,14 +2525,14 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directBike=[ "realTime": false, "rentedBike": false, "route": "", - "startTime": "2009-10-21T23:00:03.000+00:00", + "startTime": "2009-10-21T22:57:56.000+00:00", "steps": [ { "absoluteDirection": "NORTH", "area": false, "bogusName": false, "distance": 1032.385, - "elevation": "0,63.7,1,63.7,3,63.7,13,63.4,23,63.2,33,63.3,43,63.5,53,63.3,63,62.9,73,62.5,83,62.2,83,62.2,93,62.0,103,61.7,113,61.5,123,61.2,133,60.9,143,60.6,153,60.3,161,60.2", + "elevation": "0,63.7,1,63.7,3,63.7,13,63.4,23,63.2,33,63.3,43,63.5,53,63.3,63,62.9,73,62.5,83,62.2,83,62.2,93,62.0,103,61.7,113,61.5,123,61.2,133,60.9,143,60.6,153,60.3,161,60.2,161,60.2,241,60.2,241,60.2,320,60.2,320,60.2,399,60.2,399,60.2,479,60.2,479,60.2,558,60.2,558,60.2,636,60.2,636,60.2,717,60.2,717,60.2,795,60.2,795,60.2,947,60.2,947,60.2,955,60.2,955,60.2,1005,60.2,1005,60.2,1034,60.2", "lat": 45.52831993266165, "lon": -122.70059632295107, "relativeDirection": "DEPART", @@ -2545,7 +2545,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directBike=[ "area": false, "bogusName": false, "distance": 473.912, - "elevation": "", + "elevation": "0,60.2,157,55.5,157,55.5,212,55.5,212,55.5,266,55.5,266,55.5,316,55.5,316,55.5,395,55.5,395,55.5,397,55.5,397,55.5,474,40.4", "lat": 45.5375956, "lon": -122.70093440000001, "relativeDirection": "RIGHT", @@ -2558,7 +2558,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directBike=[ "area": false, "bogusName": false, "distance": 424.41099999999994, - "elevation": "", + "elevation": "0,40.4,80,40.4,80,40.4,160,40.4,160,40.4,268,40.4,268,40.4,314,40.4,314,40.4,343,40.4,343,40.4,412,37.3,412,37.3,424,37.3", "lat": 45.5377086, "lon": -122.69485180000001, "relativeDirection": "LEFT", @@ -2571,7 +2571,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directBike=[ "area": false, "bogusName": false, "distance": 298.61, - "elevation": "147,55.5,181,55.5,181,55.5,299,55.5", + "elevation": "0,37.3,147,55.5,147,55.5,181,55.5,181,55.5,299,55.5", "lat": 45.541291900000004, "lon": -122.69403120000001, "relativeDirection": "LEFT", @@ -2597,7 +2597,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directBike=[ "area": false, "bogusName": true, "distance": 158.348, - "elevation": "", + "elevation": "0,55.5,158,55.5", "lat": 45.543344700000006, "lon": -122.69658910000001, "relativeDirection": "LEFT", @@ -2610,7 +2610,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directBike=[ "area": false, "bogusName": true, "distance": 204.76799999999997, - "elevation": "", + "elevation": "0,55.5,72,55.5", "lat": 45.544288, "lon": -122.69810880000001, "relativeDirection": "RIGHT", @@ -2630,14 +2630,14 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directBike=[ "walkingBike": false } ], - "startTime": "2009-10-21T23:00:03.000+00:00", + "startTime": "2009-10-21T22:57:56.000+00:00", "tooSloped": false, "transfers": 0, "transitTime": 0, "waitingTime": 0, "walkDistance": 2635.313, "walkLimitExceeded": false, - "walkTime": 597 + "walkTime": 724 } ] ] @@ -2648,8 +2648,8 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directBikeRe { "arrivedAtDestinationWithRentedBicycle": false, "duration": 459, - "elevationGained": 12.529360656738277, - "elevationLost": 36.97238433837891, + "elevationGained": 12.889360656738276, + "elevationLost": 38.14238433837891, "endTime": "2009-10-21T23:17:39.000+00:00", "fare": { "details": { }, @@ -2901,22 +2901,22 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directWalk=[ [ { "arrivedAtDestinationWithRentedBicycle": false, - "duration": 1880, - "elevationGained": 0.700001831054756, - "elevationLost": 17.168395080566476, - "endTime": "2009-10-21T23:41:20.000+00:00", + "duration": 1863, + "elevationGained": 0.700001831054692, + "elevationLost": 12.448395080566414, + "endTime": "2009-10-21T23:41:03.000+00:00", "fare": { "details": { }, "fare": { } }, - "generalizedCost": 3660, + "generalizedCost": 3648, "legs": [ { "agencyTimeZoneOffset": -25200000, "arrivalDelay": 0, "departureDelay": 0, - "distance": 2442.081, - "endTime": "2009-10-21T23:41:20.000+00:00", + "distance": 2431.985, + "endTime": "2009-10-21T23:41:03.000+00:00", "from": { "departure": "2009-10-21T23:10:00.000+00:00", "lat": 45.52832, @@ -2924,12 +2924,12 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directWalk=[ "name": "SW Johnson St. & NW 24th Ave. (P1)", "vertexType": "NORMAL" }, - "generalizedCost": 3660, + "generalizedCost": 3648, "interlineWithPreviousLeg": false, - "legElevation": "0,63.7,1,63.7,1,63.7,11,63.4,21,63.2,31,63.3,41,63.5,51,63.3,61,62.9,71,62.5,81,62.2,91,62.0,101,61.7,111,61.5,121,61.2,131,60.9,141,60.6,151,60.3,160,60.2,170,59.8,180,59.5,190,59.2,200,59.1,210,58.8,220,58.6,230,58.4,240,58.0,250,57.7,260,57.4,270,57.0,280,56.6,290,56.2,300,55.9,310,55.7,318,55.5,398,55.5,404,55.5,418,55.5,439,55.5,469,55.5,476,55.5,556,55.5,556,NaN,635,NaN,635,55.5,715,55.5,715,NaN,794,NaN,794,55.5,873,55.5,873,NaN,953,NaN,953,55.5,981,55.5,1092,55.5,1546,55.5,1556,55.5,1571,55.5,1584,55.5,1617,55.5,1645,55.5,1814,55.5,1814,NaN,1884,NaN,1884,55.5,1919,55.5,2036,55.5,2079,55.5,2079,NaN,2442,NaN", + "legElevation": "0,63.7,1,63.7,1,63.7,11,63.4,21,63.2,31,63.3,41,63.5,51,63.3,61,62.9,71,62.5,81,62.2,91,62.0,101,61.7,111,61.5,121,61.2,131,60.9,141,60.6,151,60.3,160,60.2,240,60.2,1466,60.2,1540,55.5,2299,55.5,2299,NaN,2432,NaN", "legGeometry": { - "length": 93, - "points": "}f{tGv}{kVC?mCBmCDGaK?SM?_CDK?Y@c@?m@@I?K?M@sBBM?K?G?W@qA@O?K@sBBO@M?wA@S@E?M?K?uBDK?K?eA@m@@M@K@e@@iED_@@Y@aA?s@@kA@aA@mCBC@MDi@?c@CcBByBBS@K@E??IEGGG?IM@C_@Ce@Gm@EeAMmBGiAGoAWWg@s@EEGKIM]i@k@w@A?QWOVYj@eCxECF{@eAq@nAcAzBeAbBKDy@Ou@a@k@]c@_@Ws@M{@BsA" + "length": 62, + "points": "}f{tGv}{kVC?mCBmCDoCDmCDmCBoCDmCDkCDoCB_CBM@iGEE@MB[H}@@s@@oCDmCBC@eCBK@SBWDeCZ]eFYyDEu@C_@Ce@Gm@EeAMmBGiAGoAWWg@s@EEGKIM]i@k@w@A?QWOVYj@eCxECF{@eAq@nAcAzBeAbBKDy@Ou@a@k@]c@_@Ws@M{@BsA" }, "mode": "WALK", "pathway": false, @@ -2942,8 +2942,8 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directWalk=[ "absoluteDirection": "NORTH", "area": false, "bogusName": false, - "distance": 159.625, - "elevation": "0,63.7,1,63.7,3,63.7,13,63.4,23,63.2,33,63.3,43,63.5,53,63.3,63,62.9,73,62.5,83,62.2,83,62.2,93,62.0,103,61.7,113,61.5,123,61.2,133,60.9,143,60.6,153,60.3,161,60.2", + "distance": 1374.9060000000002, + "elevation": "0,63.7,1,63.7,3,63.7,13,63.4,23,63.2,33,63.3,43,63.5,53,63.3,63,62.9,73,62.5,83,62.2,83,62.2,93,62.0,103,61.7,113,61.5,123,61.2,133,60.9,143,60.6,153,60.3,161,60.2,161,60.2,241,60.2,241,60.2,320,60.2,320,60.2,399,60.2,399,60.2,479,60.2,479,60.2,558,60.2,558,60.2,636,60.2,636,60.2,717,60.2,717,60.2,795,60.2,795,60.2,947,60.2,947,60.2,955,60.2,955,60.2,1005,60.2,1005,60.2,1034,60.2,1034,60.2,1114,60.2,1114,60.2,1195,60.2,1195,60.2,1301,60.2,1301,60.2,1376,60.2", "lat": 45.52831993266165, "lon": -122.70059632295107, "relativeDirection": "DEPART", @@ -2955,49 +2955,10 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directWalk=[ "absoluteDirection": "EAST", "area": false, "bogusName": false, - "distance": 158.449, - "elevation": "0,60.2,10,59.8,20,59.5,30,59.2,40,59.1,50,58.8,60,58.6,70,58.4,80,58.0,90,57.7,100,57.4,110,57.0,120,56.6,130,56.2,140,55.9,150,55.7,158,55.5", - "lat": 45.529755, - "lon": -122.70064880000001, - "relativeDirection": "RIGHT", - "stayOn": false, - "streetName": "Northwest Lovejoy Street", - "walkingBike": false - }, - { - "absoluteDirection": "NORTH", - "area": false, - "bogusName": false, - "distance": 1228.4150000000002, - "elevation": "0,55.5,80,55.5,80,55.5,86,55.5,86,55.5,100,55.5,100,55.5,121,55.5,121,55.5,151,55.5,151,55.5,158,55.5,158,55.5,238,55.5,317,55.5,397,55.5,476,55.5,555,55.5,634,55.5,662,55.5,662,55.5,774,55.5,774,55.5,792,55.5,792,55.5,806,55.5,806,55.5,843,55.5,843,55.5,872,55.5,872,55.5,951,55.5,951,55.5,1033,55.5,1033,55.5,1139,55.5,1139,55.5,1228,55.5", - "lat": 45.529794700000004, - "lon": -122.6986155, - "relativeDirection": "LEFT", - "stayOn": false, - "streetName": "Northwest 23rd Avenue", - "walkingBike": false - }, - { - "absoluteDirection": "NORTHEAST", - "area": false, - "bogusName": true, - "distance": 24.678, - "elevation": "0,55.5,9,55.5,9,55.5,25,55.5", - "lat": 45.540833400000004, - "lon": -122.6990399, - "relativeDirection": "RIGHT", - "stayOn": false, - "streetName": "way 132128478 from 11", - "walkingBike": false - }, - { - "absoluteDirection": "EAST", - "area": false, - "bogusName": false, - "distance": 178.10399999999998, - "elevation": "0,55.5,13,55.5,13,55.5,28,55.5,28,55.5,46,55.5,46,55.5,74,55.5,74,55.5,117,55.5,117,55.5,146,55.5,146,55.5,178,55.5", - "lat": 45.540972100000005, - "lon": -122.6988678, + "distance": 364.269, + "elevation": "0,60.2,91,60.2,91,60.2,165,55.5,165,55.5,186,55.5,186,55.5,199,55.5,199,55.5,214,55.5,214,55.5,232,55.5,232,55.5,260,55.5,260,55.5,303,55.5,303,55.5,332,55.5,332,55.5,364,55.5", + "lat": 45.540664500000005, + "lon": -122.7012174, "relativeDirection": "RIGHT", "stayOn": false, "streetName": "Northwest Nicolai Street", @@ -3008,7 +2969,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directWalk=[ "area": false, "bogusName": false, "distance": 135.077, - "elevation": "0,55.5,65,55.5", + "elevation": "0,55.5,65,55.5,65,55.5,120,55.5,120,55.5,135,55.5", "lat": 45.5412394, "lon": -122.6966132, "relativeDirection": "LEFT", @@ -3047,7 +3008,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directWalk=[ "area": false, "bogusName": true, "distance": 158.348, - "elevation": "", + "elevation": "0,55.5,158,55.5", "lat": 45.543344700000006, "lon": -122.69658910000001, "relativeDirection": "LEFT", @@ -3060,7 +3021,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directWalk=[ "area": false, "bogusName": true, "distance": 204.76799999999997, - "elevation": "", + "elevation": "0,55.5,72,55.5", "lat": 45.544288, "lon": -122.69810880000001, "relativeDirection": "RIGHT", @@ -3070,7 +3031,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directWalk=[ } ], "to": { - "arrival": "2009-10-21T23:41:20.000+00:00", + "arrival": "2009-10-21T23:41:03.000+00:00", "lat": 45.54549, "lon": -122.69659, "name": "Sulzer Pump (P4)", @@ -3085,9 +3046,9 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.directWalk=[ "transfers": 0, "transitTime": 0, "waitingTime": 0, - "walkDistance": 2442.081, + "walkDistance": 2431.985, "walkLimitExceeded": false, - "walkTime": 1880 + "walkTime": 1863 } ] ] @@ -3525,7 +3486,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.transit=[ }, "generalizedCost": 442, "interlineWithPreviousLeg": false, - "legElevation": "0,29.6,0,29.5,10,29.5,18,29.3,18,29.3,28,29.4,38,29.3,48,29.3,58,29.3,68,29.2,78,29.2,86,29.3,86,29.3,98,29.3,104,29.4,114,29.4,124,29.4,134,29.4,144,29.4,154,29.3,164,29.3,170,29.3,177,29.3,187,29.4,197,29.4,207,29.4,237,29.4,250,29.7,260,29.7,270,29.8,280,29.7,288,29.7", + "legElevation": "0,29.6,0,29.5,10,29.5,18,29.3,18,29.3,28,29.4,38,29.3,48,29.3,58,29.3,68,29.2,78,29.2,86,29.3,98,29.3,104,29.4,114,29.4,124,29.4,134,29.4,144,29.4,154,29.3,164,29.3,170,29.3,177,29.3,187,29.4,197,29.4,207,29.4,237,29.4,250,29.7,260,29.7,270,29.8,280,29.7,288,29.7", "legGeometry": { "length": 17, "points": "ssztGh_wkV?`@?LJAlBCH?J?H?vBCJ?H?jBCJA?D@L?lAE?" @@ -3784,7 +3745,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.transit=[ "vertexType": "TRANSIT", "zoneId": "1" }, - "generalizedCost": 818, + "generalizedCost": 819, "interlineWithPreviousLeg": false, "legElevation": "0,82.0,9,82.8,19,83.4,29,84.1,39,85.0,49,85.7,55,86.1,65,85.8,75,85.2,85,83.4,95,82.6,105,82.8,115,82.3,125,82.2,135,82.0,142,81.9,152,81.5,162,81.1,172,80.8,182,80.5,192,80.0,202,79.8,212,79.6,222,79.6,231,79.3,241,79.7,246,79.8,256,79.8,266,79.4,276,78.4,286,77.5,296,76.5,306,75.5,316,74.5,323,74.1,328,73.8,338,73.7,348,73.4,358,73.0,368,72.4,378,72.0,388,71.5,398,71.0,407,70.7,417,70.5,427,70.1,437,69.7,447,69.1,457,68.7,467,68.2,477,67.7,485,67.4,485,67.4,495,67.2,505,66.8,515,66.3,525,65.8,535,65.2,545,64.7,555,64.1,564,63.7", "legGeometry": { @@ -3873,10 +3834,10 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.transit=[ }, { "arrivedAtDestinationWithRentedBicycle": false, - "duration": 1143, - "elevationGained": 4.415277175903327, - "elevationLost": 1.0902490997314551, - "endTime": "2009-10-21T23:37:31.000+00:00", + "duration": 1156, + "elevationGained": 9.135277175903319, + "elevationLost": 1.0902490997314445, + "endTime": "2009-10-21T23:37:44.000+00:00", "fare": { "details": { "regular": [ @@ -3909,7 +3870,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.transit=[ } } }, - "generalizedCost": 2370, + "generalizedCost": 2396, "legs": [ { "agencyTimeZoneOffset": -25200000, @@ -3926,7 +3887,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.transit=[ }, "generalizedCost": 391, "interlineWithPreviousLeg": false, - "legElevation": "0,29.6,0,29.5,10,29.5,18,29.3,18,29.3,28,29.3,38,29.3,48,29.3,58,29.3,68,29.4,78,29.3,88,29.3,96,29.3,106,29.2,116,29.3,126,29.2,136,29.2,146,29.2,156,29.2,166,29.3,176,29.3,176,29.3,183,29.3,195,29.4,205,29.5,215,29.6,225,29.6,235,29.6,250,29.4,253,29.4", + "legElevation": "0,29.6,0,29.5,10,29.5,18,29.3,18,29.3,28,29.3,38,29.3,48,29.3,58,29.3,68,29.4,78,29.3,88,29.3,96,29.3,106,29.2,116,29.3,126,29.2,136,29.2,146,29.2,156,29.2,166,29.3,176,29.3,183,29.3,195,29.4,205,29.5,215,29.6,225,29.6,235,29.6,250,29.4,253,29.4", "legGeometry": { "length": 15, "points": "ssztGh_wkV?`@?LK?uBBK?K@sBBM??D?L@\\?lCC??E" @@ -4160,7 +4121,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.transit=[ "arrivalDelay": 0, "departureDelay": 0, "distance": 580.63, - "endTime": "2009-10-21T23:37:31.000+00:00", + "endTime": "2009-10-21T23:37:44.000+00:00", "from": { "arrival": "2009-10-21T23:29:50.000+00:00", "departure": "2009-10-21T23:29:50.000+00:00", @@ -4172,9 +4133,9 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.transit=[ "vertexType": "TRANSIT", "zoneId": "1" }, - "generalizedCost": 901, + "generalizedCost": 927, "interlineWithPreviousLeg": false, - "legElevation": "0,55.5,14,55.5,25,55.5,25,NaN,421,NaN,421,60.2,431,60.4,441,60.6,451,60.9,461,61.2,471,61.5,481,61.8,491,62.0,499,62.2,509,62.5,519,62.9,529,63.3,539,63.5,549,63.3,559,63.2,569,63.4,579,63.7,579,63.7,581,63.7", + "legElevation": "0,55.5,25,55.5,183,60.2,341,60.2,421,60.2,431,60.4,441,60.6,451,60.9,461,61.2,471,61.5,481,61.8,491,62.0,499,62.2,509,62.5,519,62.9,529,63.3,539,63.5,549,63.3,559,63.2,569,63.4,579,63.7,579,63.7,581,63.7", "legGeometry": { "length": 12, "points": "}~{tGnq{kV?LVAF?J?FtKlCClCEnCElCElCCB?" @@ -4204,7 +4165,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.transit=[ "area": false, "bogusName": false, "distance": 158.45, - "elevation": "", + "elevation": "0,55.5,158,60.2", "lat": 45.531934, "lon": -122.698695, "relativeDirection": "RIGHT", @@ -4217,7 +4178,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.transit=[ "area": false, "bogusName": false, "distance": 397.28499999999997, - "elevation": "238,60.2,248,60.4,258,60.6,268,60.9,278,61.2,288,61.5,298,61.8,308,62.0,316,62.2,316,62.2,326,62.5,336,62.9,346,63.3,356,63.5,366,63.3,376,63.2,386,63.4,396,63.7,396,63.7,397,63.7", + "elevation": "0,60.2,79,60.2,79,60.2,158,60.2,158,60.2,238,60.2,238,60.2,248,60.4,258,60.6,268,60.9,278,61.2,288,61.5,298,61.8,308,62.0,316,62.2,316,62.2,326,62.5,336,62.9,346,63.3,356,63.5,366,63.3,376,63.2,386,63.4,396,63.7,396,63.7,397,63.7", "lat": 45.5318916, "lon": -122.70072830000001, "relativeDirection": "LEFT", @@ -4227,7 +4188,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.transit=[ } ], "to": { - "arrival": "2009-10-21T23:37:31.000+00:00", + "arrival": "2009-10-21T23:37:44.000+00:00", "lat": 45.52832, "lon": -122.70059, "name": "SW Johnson St. & NW 24th Ave. (P1)", @@ -4244,13 +4205,13 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.transit=[ "waitingTime": 0, "walkDistance": 833.505, "walkLimitExceeded": false, - "walkTime": 666 + "walkTime": 679 }, { "arrivedAtDestinationWithRentedBicycle": false, "duration": 1535, - "elevationGained": 9.480002822875981, - "elevationLost": 4.756008529663092, + "elevationGained": 9.480002822875978, + "elevationLost": 4.756008529663088, "endTime": "2009-10-21T23:46:18.000+00:00", "fare": { "details": { @@ -4301,7 +4262,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.transit=[ }, "generalizedCost": 949, "interlineWithPreviousLeg": false, - "legElevation": "0,29.6,0,29.5,10,29.5,18,29.3,18,29.3,28,29.4,38,29.3,48,29.3,58,29.3,68,29.2,78,29.2,86,29.3,86,29.3,98,29.3,104,29.4,114,29.4,124,29.4,134,29.4,144,29.4,154,29.3,164,29.3,170,29.3,177,29.3,187,29.4,197,29.4,207,29.4,237,29.4,250,29.7,263,29.6,273,29.6,283,29.5,293,29.5,303,29.7,314,29.6,324,29.7,334,29.7,344,29.7,354,29.8,367,29.8,375,29.9,385,29.9,395,30.0,405,30.0,415,30.1,425,30.0,438,30.1,438,30.1,447,30.0,447,30.0,457,29.9,467,29.9,477,30.0,487,30.0,497,30.0,507,30.1,514,30.1,526,30.2,536,30.3,546,30.3,556,30.5,566,30.6,576,30.7,586,30.8,599,30.9,605,30.9,619,30.9", + "legElevation": "0,29.6,0,29.5,10,29.5,18,29.3,18,29.3,28,29.4,38,29.3,48,29.3,58,29.3,68,29.2,78,29.2,86,29.3,98,29.3,104,29.4,114,29.4,124,29.4,134,29.4,144,29.4,154,29.3,164,29.3,170,29.3,177,29.3,187,29.4,197,29.4,207,29.4,237,29.4,250,29.7,263,29.6,273,29.6,283,29.5,293,29.5,303,29.7,314,29.6,324,29.7,334,29.7,344,29.7,354,29.8,367,29.8,375,29.9,385,29.9,395,30.0,405,30.0,415,30.1,425,30.0,438,30.1,447,30.0,457,29.9,467,29.9,477,30.0,487,30.0,497,30.0,507,30.1,514,30.1,526,30.2,536,30.3,546,30.3,556,30.5,566,30.6,576,30.7,586,30.8,599,30.9,605,30.9,619,30.9", "legGeometry": { "length": 37, "points": "ssztGh_wkV?`@?LJAlBCH?J?H?vBCJ?H?jBCJAVCJAF@F@b@NPHFBD@fA`@JDJBhBx@NFNJ~Ap@F@LFLFhBx@@?HDBOBOGE" @@ -5129,7 +5090,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.transit=[ }, "generalizedCost": 442, "interlineWithPreviousLeg": false, - "legElevation": "0,29.6,0,29.5,10,29.5,18,29.3,18,29.3,28,29.4,38,29.3,48,29.3,58,29.3,68,29.2,78,29.2,86,29.3,86,29.3,98,29.3,104,29.4,114,29.4,124,29.4,134,29.4,144,29.4,154,29.3,164,29.3,170,29.3,177,29.3,187,29.4,197,29.4,207,29.4,237,29.4,250,29.7,260,29.7,270,29.8,280,29.7,288,29.7", + "legElevation": "0,29.6,0,29.5,10,29.5,18,29.3,18,29.3,28,29.4,38,29.3,48,29.3,58,29.3,68,29.2,78,29.2,86,29.3,98,29.3,104,29.4,114,29.4,124,29.4,134,29.4,144,29.4,154,29.3,164,29.3,170,29.3,177,29.3,187,29.4,197,29.4,207,29.4,237,29.4,250,29.7,260,29.7,270,29.8,280,29.7,288,29.7", "legGeometry": { "length": 17, "points": "ssztGh_wkV?`@?LJAlBCH?J?H?vBCJ?H?jBCJA?D@L?lAE?" @@ -5388,7 +5349,7 @@ org.opentripplanner.routing.algorithm.mapping.ElevationSnapshotTest.transit=[ "vertexType": "TRANSIT", "zoneId": "1" }, - "generalizedCost": 818, + "generalizedCost": 819, "interlineWithPreviousLeg": false, "legElevation": "0,82.0,9,82.8,19,83.4,29,84.1,39,85.0,49,85.7,55,86.1,65,85.8,75,85.2,85,83.4,95,82.6,105,82.8,115,82.3,125,82.2,135,82.0,142,81.9,152,81.5,162,81.1,172,80.8,182,80.5,192,80.0,202,79.8,212,79.6,222,79.6,231,79.3,241,79.7,246,79.8,256,79.8,266,79.4,276,78.4,286,77.5,296,76.5,306,75.5,316,74.5,323,74.1,328,73.8,338,73.7,348,73.4,358,73.0,368,72.4,378,72.0,388,71.5,398,71.0,407,70.7,417,70.5,427,70.1,437,69.7,447,69.1,457,68.7,467,68.2,477,67.7,485,67.4,485,67.4,495,67.2,505,66.8,515,66.3,525,65.8,535,65.2,545,64.7,555,64.1,564,63.7", "legGeometry": {