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

Elevation handling improvements #4033

Merged
merged 15 commits into from
Mar 30, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions docs/BuildConfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
258 changes: 256 additions & 2 deletions src/client/js/otp/modules/planner/ItinerariesWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ otp.widgets.ItinerariesWidget =
var itin = $(this).data('itin');
this_.module.drawItinerary(itin);
this_.activeIndex = $(this).data('index');

// Wait for the SVG element to be resized, before re-rendering
setTimeout(function () {
this_.renderElevationGraphContent(itin, this_.activeIndex);
}, 0);
});

$('<div id="'+divId+'-'+i+'"></div>')
Expand All @@ -167,15 +172,16 @@ otp.widgets.ItinerariesWidget =
this.renderHeaderContent(itineraries[i], i, header);
}*/
this.renderHeaders();
this_.itinsAccord.accordion("resize");
this.renderElevationGraphContent(this.itineraries[this.activeIndex], this.activeIndex);
this.itinsAccord.accordion("resize");

this.$().resize(_.throttle(function(){
this_.itinsAccord.accordion("resize");
this_.renderHeaders();
this_.renderElevationGraphContent(this_.itineraries[this_.activeIndex], this_.activeIndex);
}, 100, {leading: false}));

this.$().draggable({ cancel: "#"+divId });

},

clear : function() {
Expand Down Expand Up @@ -301,6 +307,244 @@ otp.widgets.ItinerariesWidget =

},

renderElevationGraphContent : function(itin, index) {
if (!itin) {
return;
}

var graph = $("#" + this.id + '-itinElevationGraph-' + index);
var width = graph.width();
var graphSegments = [];
var transitParts = 0;
var onStreetLength = 0;
var hasElevation = false;
var minElevation = Number.MAX_VALUE;
var maxElevation = Number.MIN_VALUE;
for (var l = 0; l < itin.itinData.legs.length; l++) {
var leg = itin.itinData.legs[l];
if (otp.util.Itin.isTransit(leg.mode)) {
graphSegments.push({
leg: leg,
mode: leg.mode,
textColor: otp.util.Itin.getLegTextColor(leg),
backgroundColor: otp.util.Itin.getLegBackgroundColor(leg),
transit: true,
distance: onStreetLength,
transitParts: transitParts,
});
transitParts++;
} else {
var graphPoints = [];
if (leg.legElevation) {
var regex = /([0-9.]+),([0-9.]+|NaN)/g;
var m;
while (m = regex.exec(leg.legElevation)) {
var elevation = Number.parseFloat(m[2]);
if (!Number.isNaN(elevation)) {
minElevation = Math.min(minElevation, elevation);
maxElevation = Math.max(maxElevation, elevation);
}
graphPoints.push({
elevation: elevation,
distance: onStreetLength + Number.parseFloat(m[1])
});
}
hasElevation = true;
}

graphSegments.push({
leg: leg,
transit: false,
mode: leg.mode,
textColor: otp.util.Itin.getLegTextColor(leg),
backgroundColor: otp.util.Itin.getLegBackgroundColor(leg),
fromDistance: onStreetLength,
toDistance: onStreetLength + leg.distance,
graphPoints: graphPoints,
transitParts: transitParts,
})
onStreetLength += leg.distance;
}
}

var elevationBuffer = (maxElevation - minElevation) * 0.1;
minElevation = Math.floor((minElevation - elevationBuffer) / 10) * 10;
maxElevation = Math.ceil((maxElevation + elevationBuffer) / 10) * 10;

graph.empty();
graph.css({display: hasElevation ? 'inline' : 'none'});

if (hasElevation) {
// jquery has problems handling alternate namespaces, so elements are created explicitly
var svg = function (elementName, attributes, content) {
var element = document.createElementNS('http://www.w3.org/2000/svg', elementName)
for (var attr in attributes) if (attributes.hasOwnProperty(attr)) {
element.setAttribute(attr, attributes[attr]);
}
element.textContent = content || "";
return element;
};
var graphX = function (transitParts, distance) {
return labelWidth + Math.round(distance * pm + transitParts * iconWidth);
};
var drawMode = function (point, fromX, toX) {
graph.append(svg('rect', {
class: "mode",
x: fromX + 1,
y: 119,
width: toX - fromX - 2,
height: iconWidth - 2,
rx: 2,
fill: point.backgroundColor,
}));

if (toX - fromX >= iconWidth) {
graph.append(svg('rect', {
class: "mode",
x: fromX + 1,
y: 119,
width: toX - fromX - 2,
height: iconWidth - 2,
rx: 2,
fill: point.textColor,
style: "mask: url(" + otp.config.resourcePath
+ 'images/mode/'
+ point.mode.toLowerCase() + '.png'
+ ") center no-repeat",
}));
}
};

var labelWidth = 50;
var iconWidth = 22;
var endPadding = 10;
var streetPixels = width - labelWidth - endPadding - (iconWidth * transitParts);
var pm = streetPixels / onStreetLength;

_(graphSegments).each(function (segment) {
if (segment.transit) {
var pointX = graphX(segment.transitParts, segment.distance);
drawMode(segment, pointX, pointX + iconWidth);
} else {
var firstPointX = graphX(segment.transitParts, segment.fromDistance);
var lastPointX = graphX(segment.transitParts, segment.toDistance);

drawMode(segment, firstPointX, lastPointX);

function finishGraphSegment() {
if (firstPoint && lastPoint) {
path += ' L ' + lastPoint.x + ',115';
path += ' L ' + firstPoint.x + ',115 Z';

graph.append(svg('path', {
style: 'elevation',
d: path,
fill: segment.backgroundColor,
}));

firstPoint = null;
lastPoint = null;
path = '';
}
}

var path = '', firstPoint, lastPoint;
_(segment.graphPoints).each(function (graphPoint) {
graphPoint.x = graphX(segment.transitParts, graphPoint.distance);
if (Number.isNaN(graphPoint.elevation)) {
finishGraphSegment();
} else {
graphPoint.y = Math.round(10 + 105 * (1 - (graphPoint.elevation - minElevation) / (maxElevation - minElevation)));
if (firstPoint) {
path += ' L';
lastPoint = graphPoint;
} else {
firstPoint = lastPoint = graphPoint;
path = ' M';
}

path += ' ' + graphPoint.x + ',' + graphPoint.y;
}
});

finishGraphSegment();
}
});

// Render step backgrounds
var stepTransitParts = 0;
var stepTotalDistance = 0;
var stepCounter = 0;
_(itin.itinData.legs).each(function (leg) {
if (otp.util.Itin.isTransit(leg.mode)) {
stepTransitParts++;
} else {
var lastX = graphX(stepTransitParts, stepTotalDistance);
_(leg.steps).each(function (step) {
stepTotalDistance += step.distance;

step.graphX1 = lastX;
step.graphX2 = graphX(stepTransitParts, stepTotalDistance);
step.graphY1 = 10;
step.graphY2 = 115;

graph.append(step.graphElement = svg('rect', {
class: "step",
x: step.graphX1,
y: step.graphY1,
width: Math.max(1, step.graphX2 - step.graphX1),
height: step.graphY2 - step.graphY1,
}));

lastX = step.graphX2;
});
}
});

graph.append(svg('line', {
class: "line",
x1: labelWidth - 1, y1: 10,
x2: labelWidth - 1, y2: 115
}));
graph.append(svg('line', {
class: "line",
x1: labelWidth - 1, y1: 10,
x2: width - endPadding, y2: 10,
"stroke-dasharray": "5,5"
}));
graph.append(svg('line', {
class: "line",
x1: labelWidth - 1, y1: 65,
x2: width - endPadding, y2: 65,
"stroke-dasharray": "5,5"
}));
graph.append(svg('line', {
class: "line",
x1: labelWidth - 1, y1: 115,
x2: width - endPadding, y2: 115,
"stroke-dasharray": "5,5"
}));
graph.append(svg('text', {
class: "label",
"text-anchor": "end",
x: 45,
y: 15
}, otp.util.Itin.distanceString(maxElevation)));
graph.append(svg('text', {
class: "label",
"text-anchor": "end",
x: 45,
y: 65
}, otp.util.Itin.distanceString(Math.round(minElevation + (maxElevation - minElevation) / 2))));
graph.append(svg('text', {
class: "label",
"text-anchor": "end",
x: 45,
y: 115
}, otp.util.Itin.distanceString(minElevation)));
}
},

municoderResultId : 0,

// returns jQuery object
Expand Down Expand Up @@ -431,6 +675,8 @@ otp.widgets.ItinerariesWidget =
//TRANSLATORS: End: Time and date (Shown after path itinerary)
itinDiv.append("<div class='otp-itinEndRow'><b>" + _tr("End") + "</b>: "+itin.getEndTimeStr()+"</div>");

itinDiv.append("<svg id='" + this.id + "-itinElevationGraph-" + index + "' class='otp-itinElevationGraph' width='100%' height='145px' xmlns='http://www.w3.org/2000/svg'></svg>");

// add trip summary

var tripSummary = $('<div class="otp-itinTripSummary" />')
Expand Down Expand Up @@ -465,6 +711,11 @@ otp.widgets.ItinerariesWidget =
otp.util.Itin.distanceString(carDistance) + '</div>')
}

tripSummary.append('<div class="otp-itinTripSummaryLabel">' + _tr("Elevation Gained") + '</div><div class="otp-itinTripSummaryText">' +
otp.util.Itin.distanceString(itin.itinData.elevationGained) + '</div>')
tripSummary.append('<div class="otp-itinTripSummaryLabel">' + _tr("Elevation Lost") + '</div><div class="otp-itinTripSummaryText">' +
otp.util.Itin.distanceString(itin.itinData.elevationLost) + '</div>')

if(itin.hasTransit) {
//TRANSLATORS: how many public transit transfers in a trip
tripSummary.append('<div class="otp-itinTripSummaryLabel">' + _tr("Transfers") + '</div><div class="otp-itinTripSummaryText">'+itin.itinData.transfers+'</div>')
Expand Down Expand Up @@ -748,12 +999,15 @@ otp.widgets.ItinerariesWidget =
}).hover(function(evt) {
var step = $(this).data("step");
$(this).css('background', '#f0f0f0');
$(step.graphElement).css({display: 'inline'});
var popup = L.popup()
.setLatLng(new L.LatLng(step.lat, step.lon))
.setContent($(this).data("stepText"))
.openOn(this_.module.webapp.map.lmap);
}, function(evt) {
var step = $(this).data("step");
$(this).css('background', '#e8e8e8');
$(step.graphElement).css({display: 'none'});
this_.module.webapp.map.lmap.closePopup();
});
}
Expand Down
15 changes: 15 additions & 0 deletions src/client/js/otp/modules/planner/planner-style.css
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,18 @@ h3.sysNotice {
.otp-itin-print-step {
margin-top: .5em;
}

.otp-itinElevationGraph .line {
stroke: black;
}

.otp-itinElevationGraph .label {
font-size: 10px;
}

.otp-itinElevationGraph .step {
fill: #fcefa160;
stroke: #fcefa1;
stroke-width: 1px;
display: none;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ public static String mapElevation(List<P2<Double>> pairs) {
for (P2<Double> pair : pairs) {
str.append(Math.round(pair.first));
str.append(",");
str.append(Math.round(pair.second * 10.0) / 10.0);
if (Double.isNaN(pair.second)) {
str.append("NaN");
} else {
str.append(Math.round(pair.second * 10.0) / 10.0);
}
str.append(",");
}
if (str.length() > 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.opentripplanner.api.mapping;

import static org.opentripplanner.api.mapping.ElevationMapper.mapElevation;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
Expand Down Expand Up @@ -114,6 +116,7 @@ else if (domain.getPathwayId() != null) {
api.intermediateStops = placeMapper.mapStopArrivals(domain.getIntermediateStops());
}
api.legGeometry = PolylineEncoder.createEncodings(domain.getLegGeometry());
api.legElevation = mapElevation(domain.getLegElevation());
api.steps = walkStepMapper.mapWalkSteps(domain.getWalkSteps());
api.alerts = concatenateAlerts(
streetNoteMaperMapper.mapToApi(domain.getStreetNotes()),
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/org/opentripplanner/api/model/ApiLeg.java
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ public class ApiLeg {
*/
public EncodedPolylineBean legGeometry;

/**
* The elevation profile as a comma-separated list of x,y values. x is the distance from the start of the leg, y is the elevation at this
* distance.
*/
public String legElevation;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we use some more efficient data structure for this?

Copy link
Member

Choose a reason for hiding this comment

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

Isn't that what the WalkStep also uses?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is, but for example the same encoded polylines could be used as for the legGeometry.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Encoded polyline can't be used due to the different ranges the numbers have, so the existing string seems like the best solution, and so can stay the same.


/**
* A series of turn by turn instructions used for walking, biking and driving.
*/
Expand Down
Loading