Add sidebar tab with route analysis data
- adds a new analysis tab in the sidebar - the analysis tab shows length distribution for different way types, surface, and smoothness - table rows can be hovered/clicked to highlight the according segments on the map (similar behaviour as the detailed data table) - localization is implemented for `de` and `en` - the method for finding segment edges was extracted from `js/control/TrackMessages.js` into `js/util/TrackEdges.js` as it's used in the new analysis class too (the Gulp config was changed to reflect that) Notes: I had the idea to use the *DataTable* plugin for rendering the tables but decided against it. The only meaningful way to sort such a table is by the length column and that's already the case. So it's just three plain, old HTML tables, rendered by jQuery. For meaningful statistics the `processUnusedTags` setting has to be enabled in the routing profile. Only in this case the BRouter backend includes all needed tags (`highway`, `surface`, and `smoothness`) for *every* route segment in the response. I’ve enabled that setting for all profiles at my BRouter-web instance at <https://brouter.m11n.de/>.
This commit is contained in:
parent
b181649dff
commit
fc54c65e23
9 changed files with 682 additions and 53 deletions
|
|
@ -249,6 +249,38 @@ input#trackname:focus:invalid {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-analysis-header-distance {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-analysis-table td {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.dataTable.track-analysis-table tfoot td {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.track-analysis-table tr:hover {
|
||||||
|
background-color: rgba(255, 255, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-analysis-title {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-analysis-distance {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-analysis-heading {
|
||||||
|
margin-top: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
/* dashed line animation, derived from Chris Coyier and others
|
/* dashed line animation, derived from Chris Coyier and others
|
||||||
https://css-tricks.com/svg-line-animation-works/
|
https://css-tricks.com/svg-line-animation-works/
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ var paths = {
|
||||||
'js/Map.js',
|
'js/Map.js',
|
||||||
'js/LayersConfig.js',
|
'js/LayersConfig.js',
|
||||||
'js/router/BRouter.js',
|
'js/router/BRouter.js',
|
||||||
|
'js/util/*.js',
|
||||||
'js/plugin/*.js',
|
'js/plugin/*.js',
|
||||||
'js/control/*.js',
|
'js/control/*.js',
|
||||||
'js/index.js'
|
'js/index.js'
|
||||||
|
|
|
||||||
13
index.html
13
index.html
|
|
@ -573,6 +573,11 @@
|
||||||
><i class="fa fa-table"></i
|
><i class="fa fa-table"></i
|
||||||
></a>
|
></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#tab_statistics" role="tab" data-i18n="[title]sidebar.analysis.title" title="Analysis"
|
||||||
|
><i class="fa fa-pie-chart"></i
|
||||||
|
></a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -764,6 +769,14 @@
|
||||||
</h1>
|
</h1>
|
||||||
<div id="itinerary" class="flexcolumn flexgrow"></div>
|
<div id="itinerary" class="flexcolumn flexgrow"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="leaflet-sidebar-pane" id="tab_statistics">
|
||||||
|
<h1 class="leaflet-sidebar-header">
|
||||||
|
<span data-i18n="sidebar.analysis.title">Analysis</span>
|
||||||
|
<span class="leaflet-sidebar-close"><i class="fa fa-caret-right"></i></span>
|
||||||
|
</h1>
|
||||||
|
<div id="track_statistics" class="flexcolumn flexgrow"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
482
js/control/TrackAnalysis.js
Normal file
482
js/control/TrackAnalysis.js
Normal file
|
|
@ -0,0 +1,482 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (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) {
|
||||||
|
var analysis = {
|
||||||
|
highway: {},
|
||||||
|
surface: {},
|
||||||
|
smoothness: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.totalRouteDistance = 0.0;
|
||||||
|
|
||||||
|
for (var segmentIndex = 0; segments && segmentIndex < segments.length; segmentIndex++) {
|
||||||
|
for (
|
||||||
|
var messageIndex = 1;
|
||||||
|
messageIndex < segments[segmentIndex].feature.properties.messages.length;
|
||||||
|
messageIndex++
|
||||||
|
) {
|
||||||
|
this.totalRouteDistance += parseFloat(
|
||||||
|
segments[segmentIndex].feature.properties.messages[messageIndex][3]
|
||||||
|
);
|
||||||
|
var wayTags = segments[segmentIndex].feature.properties.messages[messageIndex][9].split(' ');
|
||||||
|
for (var wayTagIndex = 0; wayTagIndex < wayTags.length; wayTagIndex++) {
|
||||||
|
var wayTagParts = wayTags[wayTagIndex].split('=');
|
||||||
|
switch (wayTagParts[0]) {
|
||||||
|
case 'highway':
|
||||||
|
var highwayType = wayTagParts[1];
|
||||||
|
var 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[wayTagParts[0]][wayTagParts[1]] === 'undefined') {
|
||||||
|
analysis[wayTagParts[0]][wayTagParts[1]] = {
|
||||||
|
formatted_name: i18next.t(
|
||||||
|
'sidebar.analysis.data.' + wayTagParts[0] + '.' + wayTagParts[1],
|
||||||
|
wayTagParts[1]
|
||||||
|
),
|
||||||
|
name: wayTagParts[1],
|
||||||
|
subtype: '',
|
||||||
|
distance: 0.0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
analysis[wayTagParts[0]][wayTagParts[1]].distance += parseFloat(
|
||||||
|
segments[segmentIndex].feature.properties.messages[messageIndex][3]
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sortAnalysisData(analysis);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 a.distance < b.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($('<h4 class="analysis-heading">' + i18next.t('sidebar.analysis.header.highway') + '</h4>'));
|
||||||
|
$content.append(this.renderTable('highway', analysis.highway));
|
||||||
|
$content.append($('<h4 class="analysis-heading">' + i18next.t('sidebar.analysis.header.surface') + '</h4>'));
|
||||||
|
$content.append(this.renderTable('surface', analysis.surface));
|
||||||
|
$content.append($('<h4 class="analysis-heading">' + i18next.t('sidebar.analysis.header.smoothness') + '</h4>'));
|
||||||
|
$content.append(this.renderTable('smoothness', analysis.smoothness));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an analysis table.
|
||||||
|
*
|
||||||
|
* @param {string} type
|
||||||
|
* @param {Array} data
|
||||||
|
* @returns {jQuery}
|
||||||
|
*/
|
||||||
|
renderTable: function(type, data) {
|
||||||
|
var index;
|
||||||
|
var $table = $(
|
||||||
|
'<table data-type="' + type + '" class="mini cell-border stripe dataTable track-analysis-table"></table>'
|
||||||
|
);
|
||||||
|
var $thead = $('<thead></thead>');
|
||||||
|
$thead.append(
|
||||||
|
$('<tr>')
|
||||||
|
.append(
|
||||||
|
'<th class="track-analysis-header-category">' +
|
||||||
|
i18next.t('sidebar.analysis.table.category') +
|
||||||
|
'</th>'
|
||||||
|
)
|
||||||
|
.append(
|
||||||
|
$(
|
||||||
|
'<th class="track-analysis-header-distance">' +
|
||||||
|
i18next.t('sidebar.analysis.table.length') +
|
||||||
|
'</th>'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$table.append($thead);
|
||||||
|
var $tbody = $('<tbody></tbody>');
|
||||||
|
|
||||||
|
var totalDistance = 0.0;
|
||||||
|
|
||||||
|
for (index in data) {
|
||||||
|
if (!data.hasOwnProperty(index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var $row = $(
|
||||||
|
'<tr data-name="' +
|
||||||
|
data[index].name +
|
||||||
|
'" data-subtype="' +
|
||||||
|
data[index].subtype +
|
||||||
|
'" 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);
|
||||||
|
totalDistance += data[index].distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalDistance < this.totalRouteDistance) {
|
||||||
|
$tbody.append(
|
||||||
|
$(
|
||||||
|
'<tr data-name="internal-unknown" data-distance="' +
|
||||||
|
(this.totalRouteDistance - totalDistance) +
|
||||||
|
'"></tr>'
|
||||||
|
)
|
||||||
|
.append(
|
||||||
|
$('<td class="track-analysis-title">' + i18next.t('sidebar.analysis.table.unknown') + '</td>')
|
||||||
|
)
|
||||||
|
.append(
|
||||||
|
$(
|
||||||
|
'<td class="track-analysis-distance">' +
|
||||||
|
this.formatDistance(this.totalRouteDistance - totalDistance) +
|
||||||
|
' km</td>'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table.append($tbody);
|
||||||
|
|
||||||
|
$table.append(
|
||||||
|
$('<tfoot></tfoot>')
|
||||||
|
.append('<tr></tr>')
|
||||||
|
.append($('<td>' + i18next.t('sidebar.analysis.table.total_known') + '</td>'))
|
||||||
|
.append(
|
||||||
|
$(
|
||||||
|
'<td class="track-analysis-distance track-analysis-distance-total">' +
|
||||||
|
this.formatDistance(totalDistance) +
|
||||||
|
' km</td>'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $table;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a distance with two decimal places.
|
||||||
|
*
|
||||||
|
* @param {number} meters
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
formatDistance: function(meters) {
|
||||||
|
return (meters / 1000).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
|
},
|
||||||
|
|
||||||
|
handleHover: function(event) {
|
||||||
|
var $tableRow = $(event.currentTarget);
|
||||||
|
var $table = $tableRow.parents('table').first();
|
||||||
|
var dataType = $table.data('type');
|
||||||
|
var dataName = $tableRow.data('name');
|
||||||
|
var trackType = $tableRow.data('subtype');
|
||||||
|
|
||||||
|
var polylinesForDataType = this.getPolylinesForDataType(dataType, dataName, trackType);
|
||||||
|
|
||||||
|
this.highlightedSegments = L.layerGroup(polylinesForDataType).addTo(this.map);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleHoverOut: function() {
|
||||||
|
this.map.removeLayer(this.highlightedSegments);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleSelected: function(event) {
|
||||||
|
var tableRow = event.currentTarget;
|
||||||
|
var $table = $(tableRow)
|
||||||
|
.parents('table')
|
||||||
|
.first();
|
||||||
|
var dataType = $table.data('type');
|
||||||
|
var dataName = $(tableRow).data('name');
|
||||||
|
var trackType = $(tableRow).data('subtype');
|
||||||
|
|
||||||
|
if (tableRow.classList.toggle('selected')) {
|
||||||
|
if (this.highlightedSegment) {
|
||||||
|
this.map.removeLayer(this.highlightedSegment);
|
||||||
|
this.selectedTableRow.classList.remove('selected');
|
||||||
|
}
|
||||||
|
this.highlightedSegment = L.layerGroup(this.getPolylinesForDataType(dataType, dataName, trackType)).addTo(
|
||||||
|
this.map
|
||||||
|
);
|
||||||
|
this.selectedTableRow = tableRow;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.map.removeLayer(this.highlightedSegment);
|
||||||
|
this.selectedTableRow = null;
|
||||||
|
this.highlightedSegment = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searching each track edge if it matches the requested
|
||||||
|
* arguments (type, name, subtype if type == track). If the
|
||||||
|
* track edge matches the search, create a Leaflet polyline
|
||||||
|
* and add it to the result array.
|
||||||
|
*
|
||||||
|
* @param {string} dataType `highway`, `surface`, `smoothness`
|
||||||
|
* @param {string} dataName `primary`, `track, `asphalt`, etc.
|
||||||
|
* @param {string} trackType the tracktype is passed here (e.g.
|
||||||
|
* `grade3`), but only in the case that `dataName` is `track`
|
||||||
|
*
|
||||||
|
* @returns {Polyline[]}
|
||||||
|
*/
|
||||||
|
getPolylinesForDataType: function(dataType, dataName, trackType) {
|
||||||
|
var polylines = [];
|
||||||
|
var trackLatLngs = this.trackPolyline.getLatLngs();
|
||||||
|
|
||||||
|
for (var i = 0; i < this.trackEdges.edges.length; i++) {
|
||||||
|
if (this.wayTagsMatchesData(trackLatLngs[this.trackEdges.edges[i]], dataType, dataName, trackType)) {
|
||||||
|
var matchedEdgeIndexStart = i > 0 ? this.trackEdges.edges[i - 1] : 0;
|
||||||
|
var matchedEdgeIndexEnd = this.trackEdges.edges[i] + 1;
|
||||||
|
polylines.push(
|
||||||
|
L.polyline(
|
||||||
|
trackLatLngs.slice(matchedEdgeIndexStart, matchedEdgeIndexEnd),
|
||||||
|
this.options.overlayStyle
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return polylines;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Examine the way tags string if it matches the data arguments.
|
||||||
|
* Special handling for implicit defined dataName 'internal-unknown'
|
||||||
|
* which matches if a tag-pair is missing. Special handling for
|
||||||
|
* tracktypes again.
|
||||||
|
*
|
||||||
|
* @param {string} wayTags The way tags as provided by brouter, e.g.
|
||||||
|
* `highway=secondary surface=asphalt smoothness=good`
|
||||||
|
* @param {string} dataType `highway`, `surface`, `smoothness`
|
||||||
|
* @param {string} dataName `primary`, `track, `asphalt`, etc.
|
||||||
|
* @param {string} trackType the tracktype is passed here (e.g.
|
||||||
|
* `grade3`), but only in the case that `dataName` is `track`
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
wayTagsMatchesData: function(wayTags, dataType, dataName, trackType) {
|
||||||
|
var parsed = this.parseWayTags(wayTags);
|
||||||
|
|
||||||
|
switch (dataType) {
|
||||||
|
case 'highway':
|
||||||
|
if (dataName === 'track') {
|
||||||
|
if (trackType === 'unknown' && parsed.highway === 'track' && !parsed.tracktype) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof parsed.tracktype === 'string' && parsed.tracktype === trackType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.highway === dataName;
|
||||||
|
case 'surface':
|
||||||
|
if (dataName === 'internal-unknown' && typeof parsed.surface !== 'string') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof parsed.surface === 'string' && parsed.surface === dataName;
|
||||||
|
case 'smoothness':
|
||||||
|
if (dataName === 'internal-unknown' && typeof parsed.smoothness !== 'string') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof parsed.smoothness === 'string' && parsed.smoothness === dataName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a way tags string into an object, for example:
|
||||||
|
*
|
||||||
|
* 'highway=primary surface=asphalt' => { highway: 'primary', surface: 'asphalt' }
|
||||||
|
*
|
||||||
|
* @param wayTags The way tags as provided by brouter, e.g.
|
||||||
|
* `highway=secondary surface=asphalt smoothness=good`
|
||||||
|
*
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
parseWayTags: function(wayTags) {
|
||||||
|
var result = {};
|
||||||
|
var wayTagPairs = wayTags.feature.wayTags.split(' ');
|
||||||
|
|
||||||
|
for (var j = 0; j < wayTagPairs.length; j++) {
|
||||||
|
var wayTagParts = wayTagPairs[j].split('=');
|
||||||
|
result[wayTagParts[0]] = wayTagParts[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -26,6 +26,16 @@ BR.TrackMessages = L.Class.extend({
|
||||||
InitialCost: { title: 'initial$', className: 'dt-body-right' }
|
InitialCost: { title: 'initial$', className: 'dt-body-right' }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {?BR.TrackEdges}
|
||||||
|
*/
|
||||||
|
trackEdges: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {?L.Polyline}
|
||||||
|
*/
|
||||||
|
trackPolyline: null,
|
||||||
|
|
||||||
initialize: function(map, options) {
|
initialize: function(map, options) {
|
||||||
L.setOptions(this, options);
|
L.setOptions(this, options);
|
||||||
this._map = map;
|
this._map = map;
|
||||||
|
|
@ -49,6 +59,9 @@ BR.TrackMessages = L.Class.extend({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.trackPolyline = polyline;
|
||||||
|
this.trackEdges = new BR.TrackEdges(segments);
|
||||||
|
|
||||||
for (i = 0; segments && i < segments.length; i++) {
|
for (i = 0; segments && i < segments.length; i++) {
|
||||||
messages = segments[i].feature.properties.messages;
|
messages = segments[i].feature.properties.messages;
|
||||||
if (messages) {
|
if (messages) {
|
||||||
|
|
@ -80,7 +93,7 @@ BR.TrackMessages = L.Class.extend({
|
||||||
});
|
});
|
||||||
|
|
||||||
// highlight track segment (graph edge) on row hover
|
// highlight track segment (graph edge) on row hover
|
||||||
this._setEdges(polyline, segments);
|
|
||||||
$('#datatable tbody tr').hover(L.bind(this._handleHover, this), L.bind(this._handleHoverOut, this));
|
$('#datatable tbody tr').hover(L.bind(this._handleHover, this), L.bind(this._handleHoverOut, this));
|
||||||
$('#datatable tbody').on('click', 'tr', L.bind(this._toggleSelected, this));
|
$('#datatable tbody').on('click', 'tr', L.bind(this._toggleSelected, this));
|
||||||
},
|
},
|
||||||
|
|
@ -147,59 +160,11 @@ BR.TrackMessages = L.Class.extend({
|
||||||
return empty;
|
return empty;
|
||||||
},
|
},
|
||||||
|
|
||||||
_getMessageLatLng: function(message) {
|
|
||||||
var lon = message[0] / 1000000,
|
|
||||||
lat = message[1] / 1000000;
|
|
||||||
|
|
||||||
return L.latLng(lat, lon);
|
|
||||||
},
|
|
||||||
|
|
||||||
_setEdges: function(polyline, segments) {
|
|
||||||
var messages,
|
|
||||||
segLatLngs,
|
|
||||||
length,
|
|
||||||
si,
|
|
||||||
mi,
|
|
||||||
latLng,
|
|
||||||
i,
|
|
||||||
segIndex,
|
|
||||||
baseIndex = 0;
|
|
||||||
|
|
||||||
// track latLngs index for end node of edge
|
|
||||||
this._edges = [];
|
|
||||||
this._track = polyline;
|
|
||||||
|
|
||||||
for (si = 0; si < segments.length; si++) {
|
|
||||||
messages = segments[si].feature.properties.messages;
|
|
||||||
segLatLngs = segments[si].getLatLngs();
|
|
||||||
length = segLatLngs.length;
|
|
||||||
segIndex = 0;
|
|
||||||
|
|
||||||
for (mi = 1; mi < messages.length; mi++) {
|
|
||||||
latLng = this._getMessageLatLng(messages[mi]);
|
|
||||||
|
|
||||||
for (i = segIndex; i < length; i++) {
|
|
||||||
if (latLng.equals(segLatLngs[i])) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (i === length) {
|
|
||||||
i = length - 1;
|
|
||||||
if (mi !== messages.length - 1) debugger;
|
|
||||||
}
|
|
||||||
|
|
||||||
segIndex = i + 1;
|
|
||||||
this._edges.push(baseIndex + i);
|
|
||||||
}
|
|
||||||
baseIndex += length;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_getRowEdge: function(tr) {
|
_getRowEdge: function(tr) {
|
||||||
var row = this._table.row($(tr)),
|
var row = this._table.row($(tr)),
|
||||||
trackLatLngs = this._track.getLatLngs(),
|
trackLatLngs = this.trackPolyline.getLatLngs(),
|
||||||
startIndex = row.index() > 0 ? this._edges[row.index() - 1] : 0,
|
startIndex = row.index() > 0 ? this.trackEdges.edges[row.index() - 1] : 0,
|
||||||
endIndex = this._edges[row.index()],
|
endIndex = this.trackEdges.edges[row.index()],
|
||||||
edgeLatLngs = trackLatLngs.slice(startIndex, endIndex + 1);
|
edgeLatLngs = trackLatLngs.slice(startIndex, endIndex + 1);
|
||||||
|
|
||||||
return L.polyline(edgeLatLngs, this.options.edgeStyle);
|
return L.polyline(edgeLatLngs, this.options.edgeStyle);
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
exportRoute,
|
exportRoute,
|
||||||
profile,
|
profile,
|
||||||
trackMessages,
|
trackMessages,
|
||||||
|
trackAnalysis,
|
||||||
sidebar,
|
sidebar,
|
||||||
drawButton,
|
drawButton,
|
||||||
deleteRouteButton,
|
deleteRouteButton,
|
||||||
|
|
@ -198,6 +199,9 @@
|
||||||
trackMessages = new BR.TrackMessages(map, {
|
trackMessages = new BR.TrackMessages(map, {
|
||||||
requestUpdate: requestUpdate
|
requestUpdate: requestUpdate
|
||||||
});
|
});
|
||||||
|
trackAnalysis = new BR.TrackAnalysis(map, {
|
||||||
|
requestUpdate: requestUpdate
|
||||||
|
});
|
||||||
|
|
||||||
routingPathQuality = new BR.RoutingPathQuality(map, layersControl);
|
routingPathQuality = new BR.RoutingPathQuality(map, layersControl);
|
||||||
|
|
||||||
|
|
@ -248,6 +252,7 @@
|
||||||
stats.update(track, segments);
|
stats.update(track, segments);
|
||||||
}
|
}
|
||||||
trackMessages.update(track, segments);
|
trackMessages.update(track, segments);
|
||||||
|
trackAnalysis.update(track, segments);
|
||||||
|
|
||||||
exportRoute.update(latLngs);
|
exportRoute.update(latLngs);
|
||||||
}
|
}
|
||||||
|
|
@ -260,7 +265,8 @@
|
||||||
defaultTabId: BR.conf.transit ? 'tab_itinerary' : 'tab_profile',
|
defaultTabId: BR.conf.transit ? 'tab_itinerary' : 'tab_profile',
|
||||||
listeningTabs: {
|
listeningTabs: {
|
||||||
tab_profile: profile,
|
tab_profile: profile,
|
||||||
tab_data: trackMessages
|
tab_data: trackMessages,
|
||||||
|
tab_analysis: trackAnalysis
|
||||||
}
|
}
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
if (BR.conf.transit) {
|
if (BR.conf.transit) {
|
||||||
|
|
|
||||||
79
js/util/TrackEdges.js
Normal file
79
js/util/TrackEdges.js
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
/**
|
||||||
|
* The track messages and track analysis panels share some functionality
|
||||||
|
* which is defined in this class to prevent code duplication.
|
||||||
|
*
|
||||||
|
* @type {L.Class}
|
||||||
|
*/
|
||||||
|
BR.TrackEdges = L.Class.extend({
|
||||||
|
/**
|
||||||
|
* List of indexes for the track array where
|
||||||
|
* a segment with different features ends
|
||||||
|
*
|
||||||
|
* @type {number[]}
|
||||||
|
* @see BR.TrackMessages
|
||||||
|
*/
|
||||||
|
edges: [],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array} segments
|
||||||
|
*/
|
||||||
|
initialize: function(segments) {
|
||||||
|
this.edges = this.getTrackEdges(segments);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the indexes where a track segment ends, i.e. where the waytags change.
|
||||||
|
*
|
||||||
|
* Used in TrackMessages and TrackAnalysis for highlighting track segments.
|
||||||
|
*
|
||||||
|
* @param {Array} segments
|
||||||
|
*
|
||||||
|
* @return {number[]}
|
||||||
|
*/
|
||||||
|
getTrackEdges: function(segments) {
|
||||||
|
var messages,
|
||||||
|
segLatLngs,
|
||||||
|
length,
|
||||||
|
si,
|
||||||
|
mi,
|
||||||
|
latLng,
|
||||||
|
i,
|
||||||
|
segIndex,
|
||||||
|
baseIndex = 0,
|
||||||
|
edges = [];
|
||||||
|
|
||||||
|
// track latLngs index for end node of edge
|
||||||
|
for (si = 0; si < segments.length; si++) {
|
||||||
|
messages = segments[si].feature.properties.messages;
|
||||||
|
segLatLngs = segments[si].getLatLngs();
|
||||||
|
length = segLatLngs.length;
|
||||||
|
segIndex = 0;
|
||||||
|
|
||||||
|
for (mi = 1; mi < messages.length; mi++) {
|
||||||
|
latLng = this.getMessageLatLng(messages[mi]);
|
||||||
|
|
||||||
|
for (i = segIndex; i < length; i++) {
|
||||||
|
if (latLng.equals(segLatLngs[i])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i === length) {
|
||||||
|
i = length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
segIndex = i + 1;
|
||||||
|
edges.push(baseIndex + i);
|
||||||
|
}
|
||||||
|
baseIndex += length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return edges;
|
||||||
|
},
|
||||||
|
|
||||||
|
getMessageLatLng: function(message) {
|
||||||
|
var lon = message[0] / 1000000,
|
||||||
|
lat = message[1] / 1000000;
|
||||||
|
|
||||||
|
return L.latLng(lat, lon);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -180,6 +180,31 @@
|
||||||
"itinerary": {
|
"itinerary": {
|
||||||
"title": "Reiseroute"
|
"title": "Reiseroute"
|
||||||
},
|
},
|
||||||
|
"analysis": {
|
||||||
|
"title": "Analyse",
|
||||||
|
"header": {
|
||||||
|
"highway": "Weg",
|
||||||
|
"surface": "Material",
|
||||||
|
"smoothness": "Beschaffenheit"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"category": "Art",
|
||||||
|
"length": "Länge",
|
||||||
|
"total_known": "Gesamt bekannt:",
|
||||||
|
"unknown": "Unbekannt"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"highway": {
|
||||||
|
"living_street": "living street"
|
||||||
|
},
|
||||||
|
"surface": {
|
||||||
|
"paving_stones": "paving stones"
|
||||||
|
},
|
||||||
|
"smoothness": {
|
||||||
|
"very_bad": "very bad"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"layers": {
|
"layers": {
|
||||||
"category": {
|
"category": {
|
||||||
"base-layers": "Grundkarten",
|
"base-layers": "Grundkarten",
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,32 @@
|
||||||
"itinerary": {
|
"itinerary": {
|
||||||
"title": "Itinerary"
|
"title": "Itinerary"
|
||||||
},
|
},
|
||||||
|
"analysis": {
|
||||||
|
"title": "Analysis",
|
||||||
|
"header": {
|
||||||
|
"highway": "Highway",
|
||||||
|
"surface": "Surface",
|
||||||
|
"smoothness": "Smoothness"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"category": "Category",
|
||||||
|
"length": "Length",
|
||||||
|
"total_known": "Total Known:",
|
||||||
|
"unknown": "Unknown"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"highway": {
|
||||||
|
"living_street": "Living Street"
|
||||||
|
},
|
||||||
|
"surface": {
|
||||||
|
"fine_gravel": "Fine Gravel",
|
||||||
|
"paving_stones": "Paving Stones"
|
||||||
|
},
|
||||||
|
"smoothness": {
|
||||||
|
"very_bad": "Very Bad"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"layers": {
|
"layers": {
|
||||||
"category": {
|
"category": {
|
||||||
"base-layers": "Base layers",
|
"base-layers": "Base layers",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue