/** * Provides track analysis functionality. * * Takes the detailed way tags from brouter-server's response * and creates tables with distributions of way types, surfaces, * and smoothness values. * * On hovering/click a table row the corresponding track segments * are highlighted on the map. * * @type {L.Class} */ BR.TrackAnalysis = L.Class.extend({ /** * @type {Object} */ options: { overlayStyle: { color: 'yellow', opacity: 0.8, weight: 8, // show above quality coding (pane defined in RoutingPathQuality.js) pane: 'routingQualityPane', }, }, /** * The total distance of the whole track, recalculate on each `update()` call. * * @type {float} */ totalRouteDistance: 0.0, /** * @param {Map} map * @param {object} options */ initialize: function (map, options) { this.map = map; L.setOptions(this, options); }, /** * @type {?BR.TrackEdges} */ trackEdges: null, /** * @type {?L.Polyline} */ trackPolyline: null, /** * true when tab is shown, false when hidden * * @type {boolean} */ active: false, /** * Called by BR.Sidebar when tab is activated */ show: function () { this.active = true; this.options.requestUpdate(this); }, /** * Called by BR.Sidebar when tab is deactivated */ hide: function () { this.active = false; }, /** * Everytime the track changes this method is called: * * - calculate statistics (way type, surface, smoothness) * for the whole track * - renders statistics tables * - create event listeners which allow to hover/click a * table row for highlighting matching track segments * * @param {Polyline} polyline * @param {Array} segments */ update: function (polyline, segments) { if (!this.active) { return; } if (segments.length === 0) { $('#track_statistics').html(''); return; } this.trackPolyline = polyline; this.trackEdges = new BR.TrackEdges(segments); var analysis = this.calcStats(polyline, segments); this.render(analysis); $('.track-analysis-table tr').hover(L.bind(this.handleHover, this), L.bind(this.handleHoverOut, this)); $('.track-analysis-table tbody').on('click', 'tr', L.bind(this.toggleSelected, this)); }, /** * This method does the heavy-lifting of statistics calculation. * * What happens here? * * - loop over all route segments * - for each segment loop over all contained points * - parse and analyze the `waytags` field between two consecutive points * - group the values for each examined category (highway, surface, smoothness) and sum up the distances * - special handling for tracks: create an entry for each tracktype (and one if the tracktype is unknown) * - sort the result by distance descending * * @param polyline * @param segments * @returns {Object} */ calcStats: function (polyline, segments) { const analysis = { highway: {}, surface: {}, smoothness: {}, }; this.totalRouteDistance = 0.0; for (let segmentIndex = 0; segments && segmentIndex < segments.length; segmentIndex++) { for ( let messageIndex = 1; messageIndex < segments[segmentIndex].feature.properties.messages.length; messageIndex++ ) { this.totalRouteDistance += parseFloat( segments[segmentIndex].feature.properties.messages[messageIndex][3] ); let wayTags = segments[segmentIndex].feature.properties.messages[messageIndex][9].split(' '); wayTags = this.normalizeWayTags(wayTags, 'cycling'); for (let wayTagIndex = 0; wayTagIndex < wayTags.length; wayTagIndex++) { let wayTagParts = wayTags[wayTagIndex].split('='); let tagName = wayTagParts[0]; switch (tagName) { case 'highway': let highwayType = wayTagParts[1]; let trackType = ''; if (highwayType === 'track') { trackType = this.getTrackType(wayTags); highwayType = 'Track ' + trackType; } if (typeof analysis.highway[highwayType] === 'undefined') { analysis.highway[highwayType] = { formatted_name: i18next.t( 'sidebar.analysis.data.highway.' + highwayType, highwayType ), name: wayTagParts[1], subtype: trackType, distance: 0.0, }; } analysis.highway[highwayType].distance += parseFloat( segments[segmentIndex].feature.properties.messages[messageIndex][3] ); break; case 'surface': case 'smoothness': if (typeof analysis[tagName][wayTagParts[1]] === 'undefined') { analysis[tagName][wayTagParts[1]] = { formatted_name: i18next.t( 'sidebar.analysis.data.' + tagName + '.' + wayTagParts[1], wayTagParts[1] ), name: wayTagParts[1], subtype: '', distance: 0.0, }; } analysis[tagName][wayTagParts[1]].distance += parseFloat( segments[segmentIndex].feature.properties.messages[messageIndex][3] ); break; } } } } return this.sortAnalysisData(analysis); }, /** * Normalize the tag name. * * Motivation: The `surface` and `smoothness` tags come in different variations, * e.g. `surface`, `cycleway:surface` etc. We're only interested * in the tag which matches the given routing type. All other variations * are dropped. If no specialized surface/smoothness tag is found, the default value * is returned, i.e. `smoothness` or `surface`. * * @param wayTags tags + values for a way segment * @param routingType currently only 'cycling' is supported, can be extended in the future (walking, driving, etc.) * @returns {*[]} */ normalizeWayTags: function (wayTags, routingType) { let normalizedWayTags = []; let surfaceTags = {}; let smoothnessTags = {}; for (let wayTagIndex = 0; wayTagIndex < wayTags.length; wayTagIndex++) { let wayTagParts = wayTags[wayTagIndex].split('='); const tagName = wayTagParts[0]; const tagValue = wayTagParts[1]; if (tagName === 'surface') { surfaceTags['default'] = tagValue; continue; } if (tagName.indexOf(':surface') !== -1) { let tagNameParts = tagName.split(':'); surfaceTags[tagNameParts[0]] = tagValue; continue; } if (tagName === 'smoothness') { smoothnessTags['default'] = tagValue; continue; } if (tagName.indexOf(':smoothness') !== -1) { let tagNameParts = tagName.split(':'); smoothnessTags[tagNameParts[0]] = tagValue; continue; } normalizedWayTags[tagName] = tagValue; } switch (routingType) { case 'cycling': if (typeof surfaceTags['cycleway'] === 'string') { normalizedWayTags['surface'] = surfaceTags['cycleway']; } if (typeof smoothnessTags['cycleway'] === 'string') { normalizedWayTags['smoothness'] = smoothnessTags['cycleway']; } break; default: if (typeof surfaceTags['default'] === 'string') { normalizedWayTags['surface'] = surfaceTags['default']; } if (typeof smoothnessTags['default'] === 'string') { normalizedWayTags['smoothness'] = smoothnessTags['default']; } } return normalizedWayTags; }, /** * Transform analysis data for each type into an array, sort it * by distance descending and convert it back to an object. * * @param {Object} analysis * * @returns {Object} */ sortAnalysisData: function (analysis) { var analysisSortable = {}; var result = {}; for (var type in analysis) { if (!analysis.hasOwnProperty(type)) { continue; } result[type] = {}; analysisSortable[type] = []; for (var name in analysis[type]) { if (!analysis[type].hasOwnProperty(name)) { continue; } analysisSortable[type].push(analysis[type][name]); } analysisSortable[type].sort(function (a, b) { return b.distance - a.distance; }); for (var j = 0; j < analysisSortable[type].length; j++) { result[type][analysisSortable[type][j].formatted_name] = analysisSortable[type][j]; } } return result; }, /** * Extract the tracktype from a waytags string. * If no tracktype is found 'unknown' is returned. * * @param {string[]} wayTags * @returns {string} */ getTrackType: function (wayTags) { for (var i = 0; i < wayTags.length; i++) { var wayTagParts = wayTags[i].split('='); if (wayTagParts[0] === 'tracktype') { return wayTagParts[1]; } } return 'unknown'; }, /** * @param {Object} analysis */ render: function (analysis) { var $content = $('#track_statistics'); $content.html(''); $content.append( $('