Add “Maximum Speed” to analysis sidebar

It shows the distribution of maximum speeds for all ways on the
current route (if that data is available, otherwise it’s summed up
under “unknown”).

`maxspeed:forward` and `maxspeed:backward` is respected in conjunction
with `reversedirection`.

Hovering/clicking table rows to highlight matching segments on the
route work the identical to the other analysis tables.

Additionally, all tags in the analysis tab (way type, surface,
smoothness) are translateable now. The values were added to `en.json`.

Some HTML is rendered with template literals now, instead of
concatenating strings.

Variable declarations were changed from `var` to `const`/`let`.
This commit is contained in:
Marcus Jaschen 2024-10-19 14:41:29 +02:00 committed by Gautier P
parent 76f31aeb2b
commit a285751416
3 changed files with 177 additions and 95 deletions

View file

@ -350,10 +350,6 @@ table.dataTable.track-analysis-table tfoot td {
padding-top: 4px; padding-top: 4px;
} }
.track-analysis-title {
text-transform: capitalize;
}
.track-analysis-distance { .track-analysis-distance {
text-align: right; text-align: right;
} }

View file

@ -75,14 +75,14 @@ BR.TrackAnalysis = L.Class.extend({
/** /**
* Everytime the track changes this method is called: * Everytime the track changes this method is called:
* *
* - calculate statistics (way type, surface, smoothness) * - calculate statistics (way type, max speed, surface, smoothness)
* for the whole track * for the whole track
* - renders statistics tables * - renders statistics tables
* - create event listeners which allow to hover/click a * - create event listeners which allow to hover/click a
* table row for highlighting matching track segments * table row for highlighting matching track segments
* *
* @param {Polyline} polyline * @param {Polyline} polyline
* @param {Array} segments * @param {Array} segments route segments between waypoints
*/ */
update(polyline, segments) { update(polyline, segments) {
if (!this.active) { if (!this.active) {
@ -105,7 +105,7 @@ BR.TrackAnalysis = L.Class.extend({
this.trackPolyline = polyline; this.trackPolyline = polyline;
this.trackEdges = new BR.TrackEdges(segments); this.trackEdges = new BR.TrackEdges(segments);
var analysis = this.calcStats(polyline, segments); const analysis = this.calcStats(polyline, segments);
this.render(analysis); this.render(analysis);
@ -132,6 +132,7 @@ BR.TrackAnalysis = L.Class.extend({
calcStats(polyline, segments) { calcStats(polyline, segments) {
const analysis = { const analysis = {
highway: {}, highway: {},
maxspeed: {},
surface: {}, surface: {},
smoothness: {}, smoothness: {},
}; };
@ -175,14 +176,19 @@ BR.TrackAnalysis = L.Class.extend({
segments[segmentIndex].feature.properties.messages[messageIndex][3] segments[segmentIndex].feature.properties.messages[messageIndex][3]
); );
break; break;
case 'maxspeed':
case 'surface': case 'surface':
case 'smoothness': case 'smoothness':
if (typeof analysis[tagName][wayTagParts[1]] === 'undefined') { if (typeof analysis[tagName][wayTagParts[1]] === 'undefined') {
analysis[tagName][wayTagParts[1]] = { let formattedName = i18next.t([
formatted_name: i18next.t(
'sidebar.analysis.data.' + tagName + '.' + wayTagParts[1], 'sidebar.analysis.data.' + tagName + '.' + wayTagParts[1],
wayTagParts[1] wayTagParts[1],
), ]);
if (tagName.indexOf('maxspeed') === 0) {
formattedName += ' km/h';
}
analysis[tagName][wayTagParts[1]] = {
formatted_name: formattedName,
name: wayTagParts[1], name: wayTagParts[1],
subtype: '', subtype: '',
distance: 0.0, distance: 0.0,
@ -209,6 +215,10 @@ BR.TrackAnalysis = L.Class.extend({
* are dropped. If no specialized surface/smoothness tag is found, the default value * are dropped. If no specialized surface/smoothness tag is found, the default value
* is returned, i.e. `smoothness` or `surface`. * is returned, i.e. `smoothness` or `surface`.
* *
* Also, maxspeed comes in different variations, e.g. `maxspeed`, `maxspeed:forward`,
* `maxspeed:backward`. Depending on the existence of the `reversedirection` field
* we can select the correct value.
*
* @param wayTags tags + values for a way segment * @param wayTags tags + values for a way segment
* @param routingType currently only 'cycling' is supported, can be extended in the future (walking, driving, etc.) * @param routingType currently only 'cycling' is supported, can be extended in the future (walking, driving, etc.)
* @returns {*[]} * @returns {*[]}
@ -242,6 +252,19 @@ BR.TrackAnalysis = L.Class.extend({
continue; continue;
} }
if (tagName === 'maxspeed:forward' && !wayTags.includes('reversedirection=yes')) {
normalizedWayTags['maxspeed'] = tagValue;
continue;
}
if (tagName === 'maxspeed:backward' && wayTags.includes('reversedirection=yes')) {
normalizedWayTags['maxspeed'] = tagValue;
continue;
}
if (tagName === 'maxspeed') {
normalizedWayTags[tagName] = tagValue;
continue;
}
normalizedWayTags[tagName] = tagValue; normalizedWayTags[tagName] = tagValue;
} }
@ -279,10 +302,10 @@ BR.TrackAnalysis = L.Class.extend({
* @returns {Object} * @returns {Object}
*/ */
sortAnalysisData(analysis) { sortAnalysisData(analysis) {
var analysisSortable = {}; const analysisSortable = {};
var result = {}; const result = {};
for (var type in analysis) { for (const type in analysis) {
if (!analysis.hasOwnProperty(type)) { if (!analysis.hasOwnProperty(type)) {
continue; continue;
} }
@ -290,18 +313,24 @@ BR.TrackAnalysis = L.Class.extend({
result[type] = {}; result[type] = {};
analysisSortable[type] = []; analysisSortable[type] = [];
for (var name in analysis[type]) { for (const name in analysis[type]) {
if (!analysis[type].hasOwnProperty(name)) { if (!analysis[type].hasOwnProperty(name)) {
continue; continue;
} }
analysisSortable[type].push(analysis[type][name]); analysisSortable[type].push(analysis[type][name]);
} }
if (type === 'maxspeed') {
analysisSortable[type].sort(function (a, b) {
return parseInt(a.name) - parseInt(b.name);
});
} else {
analysisSortable[type].sort(function (a, b) { analysisSortable[type].sort(function (a, b) {
return b.distance - a.distance; return b.distance - a.distance;
}); });
}
for (var j = 0; j < analysisSortable[type].length; j++) { for (let j = 0; j < analysisSortable[type].length; j++) {
result[type][analysisSortable[type][j].formatted_name] = analysisSortable[type][j]; result[type][analysisSortable[type][j].formatted_name] = analysisSortable[type][j];
} }
} }
@ -317,8 +346,8 @@ BR.TrackAnalysis = L.Class.extend({
* @returns {string} * @returns {string}
*/ */
getTrackType(wayTags) { getTrackType(wayTags) {
for (var i = 0; i < wayTags.length; i++) { for (let i = 0; i < wayTags.length; i++) {
var wayTagParts = wayTags[i].split('='); const wayTagParts = wayTags[i].split('=');
if (wayTagParts[0] === 'tracktype') { if (wayTagParts[0] === 'tracktype') {
return wayTagParts[1]; return wayTagParts[1];
} }
@ -331,21 +360,19 @@ BR.TrackAnalysis = L.Class.extend({
* @param {Object} analysis * @param {Object} analysis
*/ */
render(analysis) { render(analysis) {
var $content = $('#track_statistics'); const $content = $('#track_statistics');
$content.html(''); $content.html('');
$content.append( $content.append($(`<h4 class="track-analysis-heading">${i18next.t('sidebar.analysis.header.highway')}</h4>`));
$('<h4 class="track-analysis-heading">' + i18next.t('sidebar.analysis.header.highway') + '</h4>')
);
$content.append(this.renderTable('highway', analysis.highway)); $content.append(this.renderTable('highway', analysis.highway));
$content.append( $content.append($(`<h4 class="track-analysis-heading">${i18next.t('sidebar.analysis.header.surface')}</h4>`));
$('<h4 class="track-analysis-heading">' + i18next.t('sidebar.analysis.header.surface') + '</h4>')
);
$content.append(this.renderTable('surface', analysis.surface)); $content.append(this.renderTable('surface', analysis.surface));
$content.append( $content.append(
$('<h4 class="track-analysis-heading">' + i18next.t('sidebar.analysis.header.smoothness') + '</h4>') $(`<h4 class="track-analysis-heading">${i18next.t('sidebar.analysis.header.smoothness')}</h4>`)
); );
$content.append(this.renderTable('smoothness', analysis.smoothness)); $content.append(this.renderTable('smoothness', analysis.smoothness));
$content.append($(`<h4 class="track-analysis-heading">${i18next.t('sidebar.analysis.header.maxspeed')}</h4>`));
$content.append(this.renderTable('maxspeed', analysis.maxspeed));
}, },
/** /**
@ -356,67 +383,45 @@ BR.TrackAnalysis = L.Class.extend({
* @returns {jQuery} * @returns {jQuery}
*/ */
renderTable(type, data) { renderTable(type, data) {
var index; let index;
var $table = $( const $table = $(`<table data-type="${type}" class="mini stripe dataTable track-analysis-table"></table>`);
'<table data-type="' + type + '" class="mini cell-border stripe dataTable track-analysis-table"></table>' const $thead = $('<thead></thead>');
);
var $thead = $('<thead></thead>');
$thead.append( $thead.append(
$('<tr>') $('<tr>')
.append( .append(
'<th class="track-analysis-header-category">' + `<th class="track-analysis-header-category">${i18next.t('sidebar.analysis.table.category')}</th>`
i18next.t('sidebar.analysis.table.category') +
'</th>'
) )
.append( .append(
$( $(`<th class="track-analysis-header-distance">${i18next.t('sidebar.analysis.table.length')}</th>`)
'<th class="track-analysis-header-distance">' +
i18next.t('sidebar.analysis.table.length') +
'</th>'
)
) )
); );
$table.append($thead); $table.append($thead);
var $tbody = $('<tbody></tbody>'); const $tbody = $('<tbody></tbody>');
var totalDistance = 0.0; let totalDistance = 0.0;
for (index in data) { for (index in data) {
if (!data.hasOwnProperty(index)) { if (!data.hasOwnProperty(index)) {
continue; continue;
} }
var $row = $( const $row = $(`<tr data-name="${data[index].name}" \
'<tr data-name="' + data-subtype="${data[index].subtype}" \
data[index].name + data-distance="${data[index].distance}"></tr>`);
'" data-subtype="' + $row.append(`<td class="track-analysis-title">${data[index].formatted_name}</td>`);
data[index].subtype + $row.append(`<td class="track-analysis-distance">${this.formatDistance(data[index].distance)} km</td>`);
'" data-distance="' +
data[index].distance +
'"></tr>'
);
$row.append('<td class="track-analysis-title">' + data[index].formatted_name + '</td>');
$row.append(
'<td class="track-analysis-distance">' + this.formatDistance(data[index].distance) + ' km</td>'
);
$tbody.append($row); $tbody.append($row);
totalDistance += data[index].distance; totalDistance += data[index].distance;
} }
if (totalDistance < this.totalRouteDistance) { if (totalDistance < this.totalRouteDistance) {
$tbody.append( $tbody.append(
$( $(`<tr data-name="internal-unknown" data-distance="${this.totalRouteDistance - totalDistance}"></tr>`)
'<tr data-name="internal-unknown" data-distance="' + .append($(`<td class="track-analysis-title">${i18next.t('sidebar.analysis.table.unknown')}</td>`))
(this.totalRouteDistance - totalDistance) +
'"></tr>'
)
.append(
$('<td class="track-analysis-title">' + i18next.t('sidebar.analysis.table.unknown') + '</td>')
)
.append( .append(
$( $(
'<td class="track-analysis-distance">' + `<td class="track-analysis-distance">${this.formatDistance(
this.formatDistance(this.totalRouteDistance - totalDistance) + this.totalRouteDistance - totalDistance
' km</td>' )} km</td>`
) )
) )
); );
@ -427,12 +432,12 @@ BR.TrackAnalysis = L.Class.extend({
$table.append( $table.append(
$('<tfoot></tfoot>') $('<tfoot></tfoot>')
.append('<tr></tr>') .append('<tr></tr>')
.append($('<td>' + i18next.t('sidebar.analysis.table.total_known') + '</td>')) .append($(`<td>${i18next.t('sidebar.analysis.table.total_known')}</td>`))
.append( .append(
$( $(
'<td class="track-analysis-distance track-analysis-distance-total">' + `<td class="track-analysis-distance track-analysis-distance-total">${this.formatDistance(
this.formatDistance(totalDistance) + totalDistance
' km</td>' )} km</td>`
) )
) )
); );
@ -451,13 +456,13 @@ BR.TrackAnalysis = L.Class.extend({
}, },
handleHover(event) { handleHover(event) {
var $tableRow = $(event.currentTarget); const $tableRow = $(event.currentTarget);
var $table = $tableRow.parents('table').first(); const $table = $tableRow.parents('table').first();
var dataType = $table.data('type'); const dataType = $table.data('type');
var dataName = $tableRow.data('name'); const dataName = $tableRow.data('name');
var trackType = $tableRow.data('subtype'); const trackType = $tableRow.data('subtype');
var polylinesForDataType = this.getPolylinesForDataType(dataType, dataName, trackType); const polylinesForDataType = this.getPolylinesForDataType(dataType, dataName, trackType);
this.highlightedSegments = L.layerGroup(polylinesForDataType).addTo(this.map); this.highlightedSegments = L.layerGroup(polylinesForDataType).addTo(this.map);
}, },
@ -467,11 +472,11 @@ BR.TrackAnalysis = L.Class.extend({
}, },
toggleSelected(event) { toggleSelected(event) {
var tableRow = event.currentTarget; const tableRow = event.currentTarget;
var $table = $(tableRow).parents('table').first(); const $table = $(tableRow).parents('table').first();
var dataType = $table.data('type'); const dataType = $table.data('type');
var dataName = $(tableRow).data('name'); const dataName = $(tableRow).data('name');
var trackType = $(tableRow).data('subtype'); const trackType = $(tableRow).data('subtype');
if (tableRow.classList.toggle('selected')) { if (tableRow.classList.toggle('selected')) {
if (this.highlightedSegment) { if (this.highlightedSegment) {
@ -505,13 +510,13 @@ BR.TrackAnalysis = L.Class.extend({
* @returns {Polyline[]} * @returns {Polyline[]}
*/ */
getPolylinesForDataType(dataType, dataName, trackType) { getPolylinesForDataType(dataType, dataName, trackType) {
var polylines = []; const polylines = [];
var trackLatLngs = this.trackPolyline.getLatLngs(); const trackLatLngs = this.trackPolyline.getLatLngs();
for (var i = 0; i < this.trackEdges.edges.length; i++) { for (let i = 0; i < this.trackEdges.edges.length; i++) {
if (this.wayTagsMatchesData(trackLatLngs[this.trackEdges.edges[i]], dataType, dataName, trackType)) { if (this.wayTagsMatchesData(trackLatLngs[this.trackEdges.edges[i]], dataType, dataName, trackType)) {
var matchedEdgeIndexStart = i > 0 ? this.trackEdges.edges[i - 1] : 0; const matchedEdgeIndexStart = i > 0 ? this.trackEdges.edges[i - 1] : 0;
var matchedEdgeIndexEnd = this.trackEdges.edges[i] + 1; const matchedEdgeIndexEnd = this.trackEdges.edges[i] + 1;
polylines.push( polylines.push(
L.polyline( L.polyline(
trackLatLngs.slice(matchedEdgeIndexStart, matchedEdgeIndexEnd), trackLatLngs.slice(matchedEdgeIndexStart, matchedEdgeIndexEnd),
@ -559,19 +564,44 @@ BR.TrackAnalysis = L.Class.extend({
return this.singleWayTagMatchesData('surface', parsed, dataName); return this.singleWayTagMatchesData('surface', parsed, dataName);
case 'smoothness': case 'smoothness':
return this.singleWayTagMatchesData('smoothness', parsed, dataName); return this.singleWayTagMatchesData('smoothness', parsed, dataName);
case 'maxspeed':
return this.singleWayTagMatchesData('maxspeed', parsed, dataName);
} }
return false; return false;
}, },
singleWayTagMatchesData(category, parsedData, lookupValue) { singleWayTagMatchesData(category, parsedData, lookupValue) {
var foundValue = null; if (typeof lookupValue === 'number') {
lookupValue = lookupValue.toString();
for (var iterationKey in parsedData) {
if (iterationKey.indexOf(category) !== -1) {
foundValue = parsedData[iterationKey];
break;
} }
let foundValue = null;
// We need to handle `maxspeed:forward` and `maxspeed:backward` separately
// from all other tags, because we need to consider the `reversedirection`
// tag.
// Test URL: http://localhost:3000/#map=15/52.2292/13.6204/standard&lonlats=13.61948,52.231611;13.611327,52.227431
if (
category === 'maxspeed' &&
parsedData.hasOwnProperty('maxspeed:forward') &&
!parsedData.hasOwnProperty('reversedirection')
) {
foundValue = parsedData['maxspeed:forward'];
}
if (
category === 'maxspeed' &&
parsedData.hasOwnProperty('maxspeed:backward') &&
parsedData.hasOwnProperty('reversedirection') &&
parsedData.reversedirection === 'yes'
) {
foundValue = parsedData['maxspeed:backward'];
}
// if the special handling for `maxspeed` didn't find a result,
// check wayTags for matching property:
if (foundValue === null && parsedData.hasOwnProperty(category)) {
foundValue = parsedData[category];
} }
if (lookupValue === 'internal-unknown' && foundValue === null) { if (lookupValue === 'internal-unknown' && foundValue === null) {

View file

@ -285,10 +285,66 @@
}, },
"sidebar": { "sidebar": {
"analysis": { "analysis": {
"data": {
"highway": {
"footway": "Footway",
"path": "Path",
"residential": "Residential",
"cycleway": "Cycleway",
"track": "Track",
"service": "Service",
"tertiary": "Tertiary",
"secondary": "Secondary",
"primary": "Primary",
"trunk": "Trunk",
"motorway": "Motorway",
"motorway_link": "Motorway Link",
"primary_link": "Primary Link",
"secondary_link": "Secondary Link",
"tertiary_link": "Tertiary Link",
"trunk_link": "Trunk Link",
"living_street": "Living Street",
"pedestrian": "Pedestrian",
"road": "Road",
"bridleway": "Bridleway",
"steps": "Steps",
"sidewalk": "Sidewalk",
"crossing": "Crossing",
"unclassified": "Unclassified"
},
"surface": {
"asphalt": "Asphalt",
"cobblestone": "Cobblestone",
"compacted": "Compacted",
"dirt": "Dirt",
"fine_gravel": "Fine Gravel",
"grass": "Grass",
"gravel": "Gravel",
"ground": "Ground",
"paved": "Paved",
"sand": "Sand",
"unpaved": "Unpaved",
"wood": "Wood",
"concrete": "Concrete",
"paving_stones": "Paving Stones",
"sett": "Sett"
},
"smoothness": {
"excellent": "Excellent",
"good": "Good",
"intermediate": "Intermediate",
"bad": "Bad",
"very_bad": "Very Bad",
"horrible": "Horrible",
"very_horrible": "Very Horrible",
"impassable": "Impassable"
}
},
"header": { "header": {
"highway": "Highway", "highway": "Highway",
"smoothness": "Smoothness", "smoothness": "Smoothness",
"surface": "Surface" "surface": "Surface",
"maxspeed": "Maximum Speed"
}, },
"table": { "table": {
"category": "Category", "category": "Category",