commit c622356c180f7a4a26bf8a2594e1a2b30a337a16
parent fc57f65107bcc30a0c362bad91b8b535fad47014
Author: Sergej Orlov <wladimirych@gmail.com>
Date: Sat, 3 Dec 2016 01:37:38 +0300
added elevation profile control; fixed false drag events when selecting range on profile
Diffstat:
6 files changed, 976 insertions(+), 2 deletions(-)
diff --git a/src/config.js b/src/config.js
@@ -3,5 +3,6 @@ export default {
googleApiUrl: 'https://maps.googleapis.com/maps/api/js?v=3',
bingKey: 'AhZy06XFi8uAADPQvWNyVseFx4NHYAOH-7OTMKDPctGtYo86kMfx2T0zUrF5AAaM',
westraDataBaseUrl: 'http://nakarte.tk/westraPasses/',
- CORSProxyUrl: 'http://proxy.nakarte.tk/'
+ CORSProxyUrl: 'http://proxy.nakarte.tk/',
+ elevationsServer: 'http://elevation.nakarte.tk/',
}
diff --git a/src/lib/leaflet.control.elevation-profile/close.png b/src/lib/leaflet.control.elevation-profile/close.png
Binary files differ.
diff --git a/src/lib/leaflet.control.elevation-profile/elevation-profile.css b/src/lib/leaflet.control.elevation-profile/elevation-profile.css
@@ -0,0 +1,179 @@
+.elevation-profile-container {
+ background-color: white;
+ width: 100%;
+ height: 220px;
+ position: absolute;
+ bottom: 0;
+ z-index: 2000;
+ cursor: default;
+}
+
+.elevation-profile-properties {
+ padding-left: 4px;
+ height: 100%;
+ float: left;
+ width: 240px;
+ border-right: 1px solid #dddddd;
+}
+
+.elevation-profile-properties table {
+ border-collapse: collapse;
+}
+.elevation-profile-properties .start-group td{
+ border-top: 1px solid #c3c3c3;
+}
+
+.elevation-profile-properties td {
+ white-space: nowrap;
+}
+
+.elevation-profile-properties-selected {
+ background-color: #dfeef4;
+}
+
+.elevation-profile-properties td:last-child {
+ padding-left: 4px;
+}
+
+.elevation-profile-properties td {
+ /*border-bottom: 1px solid #ddd;*/
+}
+
+.elevation-profile-left-axis {
+ position: relative;
+ height: 100%;
+ float: left;
+ width: 50px;
+}
+
+.elevation-profile-drawingContainer {
+ height: 100%;
+ width: auto;
+ overflow-x: scroll;
+ overflow-y: hidden;
+ position: relative;
+}
+
+.elevation-profile-grid-label {
+ font-family: Arial sans-serif;
+ font-size: 12px;
+ line-height: 12px;
+ margin-top: -6px;
+ position: absolute;
+ text-align: right;
+ right: 4px;
+}
+
+.elevation-profile-container svg {
+ /*cursor: default;*/
+}
+
+.elevation-profile-cursor {
+ border-right: 1px solid blue;
+ width: 0;
+ padding: 0;
+ margin: 0;
+ position: absolute;
+ top: 8px;
+ bottom: 24px;
+ pointer-events: none;
+}
+
+.elevation-profile-cursor-label {
+ position: absolute;
+ pointer-events: none;
+ top: 8px;
+ font-family: Arial sans-serif;
+ font-size: 12px;
+ line-height: 16px;
+ padding-left: 8px;
+ user-select: none;
+
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ white-space: nowrap;
+
+
+ text-shadow: 1px 0 0 #fff,
+ 1px 1px 0 #fff,
+ 0 1px 0 #fff,
+ -1px 1px 0 #fff,
+ -1px 0 0 #fff,
+ -1px -1px 0 #fff,
+ 0 -1px 0 #fff,
+ 1px -1px 0 #fff;
+}
+
+.elevation-profile-cursor-label-left {
+ width: 100px;
+ margin-left: -116px;
+ text-align: right;
+}
+
+.elevation-profile-cursor-hidden {
+ visibility: hidden;
+}
+
+.elevation-profile-cursor-hidden-while-drag {
+ visibility: hidden;
+}
+
+.elevation-profile-selection {
+ background-color: #0078A8;
+ opacity: .3;
+ position: absolute;
+ top: 8px;
+ bottom: 24px;
+ pointer-events: none;
+}
+
+.elevation-profile-marker {
+ margin-left: 0 !important;
+ margin-top: 0 !important;
+ white-space: nowrap;
+ pointer-events: none;
+}
+
+.elevation-profile-marker-icon {
+ display: inline-block;
+ width: 8px !important;
+ height: 8px !important;
+ margin-left: -4px;
+ margin-top: -4px;
+ border-radius: 4px;
+ background-color: #0078A8;
+ position: absolute;
+ top: 0;
+ left: 0;
+ /*opacity: .5;*/
+}
+
+.elevation-profile-marker-label {
+ display: inline-block;
+ text-shadow: 1px 0 0 #fff,
+ 1px 1px 1px #fff,
+ 0 1px 1px #fff,
+ -1px 1px 1px #fff,
+ -1px 0 1px #fff,
+ -1px -1px 1px #fff,
+ 0 -1px 1px #fff,
+ 1px -1px 1px #fff;
+ margin-left: 20px;
+}
+
+.elevation-profile-close {
+ position: absolute;
+ /*right: 0;*/
+ left: 244px;
+ width: 16px;
+ height: 16px;
+ background-image: url("close.png");
+ margin-left: 4px;
+ margin-top: 4px;
+ cursor: pointer;
+}
+
diff --git a/src/lib/leaflet.control.elevation-profile/elevation-profile.js b/src/lib/leaflet.control.elevation-profile/elevation-profile.js
@@ -0,0 +1,792 @@
+import L from 'leaflet';
+import './elevation-profile.css';
+import {fetch} from 'lib/xhr-promise/xhr-promise';
+import config from 'config';
+
+function createSvg(tagName, attributes, parent) {
+ var element = document.createElementNS('http://www.w3.org/2000/svg', tagName);
+ if (attributes) {
+ var keys = Object.keys(attributes),
+ key, value;
+ for (var i = 0; i < keys.length; i++) {
+ key = keys[i];
+ value = attributes[key];
+ element.setAttribute(key, value);
+ }
+ }
+ if (parent) {
+ parent.appendChild(element);
+ }
+ return element;
+}
+
+function pointOnSegmentAtDistance(p1, p2, dist) {
+ //FIXME: we should place markers along projected line to avoid transformation distortions
+ var q = dist / p1.distanceTo(p2),
+ x = p1.lng + (p2.lng - p1.lng) * q,
+ y = p1.lat + (p2.lat - p1.lat) * q;
+ return L.latLng(y, x);
+}
+
+
+function gradientToAngle(g) {
+ return Math.round(Math.atan(g) * 180 / Math.PI);
+}
+
+function pathRegularSamples(latlngs, step) {
+ var samples = [],
+ lastSampleDist = 0,
+ lastPointDistance = 0,
+ nextPointDistance = 0,
+ segmentLength, i;
+
+ samples.push(latlngs[0]);
+ for (i = 1; i < latlngs.length; i++) {
+ segmentLength = latlngs[i].distanceTo(latlngs[i - 1]);
+ nextPointDistance = lastPointDistance + segmentLength;
+ if (nextPointDistance >= lastSampleDist + step) {
+ while (lastSampleDist + step <= nextPointDistance) {
+ lastSampleDist += step;
+ samples.push(
+ pointOnSegmentAtDistance(latlngs[i - 1], latlngs[i], lastSampleDist - lastPointDistance)
+ );
+ }
+ }
+ lastPointDistance = nextPointDistance;
+ }
+ if (samples.length < 2) {
+ samples.push(latlngs[latlngs.length - 1]);
+ }
+ return samples;
+}
+
+function offestFromEvent(e) {
+ if (e.offsetX === undefined) {
+ var rect = e.target.getBoundingClientRect();
+ return {
+ offsetX: e.clientX - rect.left,
+ offestY: e.clientY - rect.top
+ }
+ } else {
+ return {
+ offsetX: e.offsetX,
+ offestY: e.offsetY
+ }
+ }
+}
+
+function movementFromEvents(e1, e2) {
+ return {
+ movementX: e2.clientX - e1.clientX,
+ movementY: e2.clientY - e1.clientY
+ }
+}
+
+var DragEvents = L.Class.extend({
+ options: {
+ dragTolerance: 2,
+ dragButtons: {0: true}
+ },
+
+ includes: L.Mixin.Events,
+
+ initialize: function(eventsSource, eventsTarget, options) {
+ options = L.setOptions(this, options);
+ if (eventsTarget) {
+ this.eventsTarget = eventsTarget;
+ } else {
+ this.eventsTarget = this;
+ }
+ this.dragStartPos = [];
+ this.prevEvent = [];
+ this.isDragging = [];
+
+ L.DomEvent.on(eventsSource, 'mousemove', this.onMouseMove, this);
+ L.DomEvent.on(eventsSource, 'mouseup', this.onMouseUp, this);
+ L.DomEvent.on(eventsSource, 'mousedown', this.onMouseDown, this);
+ L.DomEvent.on(eventsSource, 'mouseleave', this.onMouseLeave, this);
+ },
+
+ onMouseDown: function(e) {
+ if (this.options.dragButtons[e.button]) {
+ e._offset = offestFromEvent(e);
+ this.dragStartPos[e.button] = e;
+ this.prevEvent[e.button] = e;
+ L.DomUtil.disableImageDrag();
+ L.DomUtil.disableTextSelection();
+ }
+ },
+
+ onMouseUp: function(e) {
+ L.DomUtil.enableImageDrag();
+ L.DomUtil.enableTextSelection();
+
+ if (this.options.dragButtons[e.button]) {
+ this.dragStartPos[e.button] = null;
+ if (this.isDragging[e.button]) {
+ this.isDragging[e.button] = false;
+ this.fire('dragend', L.extend({dragButton: e.button, origEvent: e},
+ offestFromEvent(e), movementFromEvents(this.prevEvent[e.button], e)
+ )
+ );
+ } else {
+ this.fire('click', L.extend({dragButton: e.button, origEvent: e},
+ offestFromEvent(e)
+ )
+ );
+ }
+ }
+ },
+
+ onMouseMove: function(e) {
+ var i, button, self = this;
+
+ function exceedsTolerance(button) {
+ var tolerance = self.options.dragTolerance;
+ return Math.abs(e.clientX - self.dragStartPos[button].clientX) > tolerance ||
+ Math.abs(e.clientY - self.dragStartPos[button].clientY) > tolerance;
+ }
+
+ var dragButtons = Object.keys(this.options.dragButtons);
+ for (i = 0; i < dragButtons.length; i++) {
+ button = dragButtons[i];
+ if (this.isDragging[button]) {
+ this.eventsTarget.fire('drag', L.extend({dragButton: button, origEvent: e},
+ offestFromEvent(e), movementFromEvents(this.prevEvent[button], e)
+ )
+ );
+ } else if (this.dragStartPos[button] && exceedsTolerance(button)) {
+ this.isDragging[button] = true;
+ this.eventsTarget.fire('dragstart', L.extend(
+ {dragButton: button, origEvent: this.dragStartPos[button]},
+ this.dragStartPos[button]._offset
+ )
+ );
+ this.eventsTarget.fire('drag', L.extend({
+ dragButton: button,
+ origEvent: e,
+ startEvent: self.dragStartPos[button]
+ }, offestFromEvent(e), movementFromEvents(this.prevEvent[button], e)
+ )
+ );
+ }
+ this.prevEvent[button] = e;
+ }
+ },
+
+ onMouseLeave: function(e) {
+ var i, button;
+ var dragButtons = Object.keys(this.options.dragButtons);
+ for (i = 0; i < dragButtons.length; i++) {
+ button = dragButtons[i];
+ if (this.isDragging[button]) {
+ this.isDragging[button] = false;
+ this.fire('dragend', L.extend({dragButton: button, origEvent: e},
+ offestFromEvent(e), movementFromEvents(this.prevEvent[button], e)
+ )
+ );
+ }
+ }
+ this.dragStartPos = {};
+ }
+ }
+);
+
+L.Control.ElevationProfile = L.Class.extend({
+ options: {
+ elevationsServer: config.elevationsServer,
+ samplingInterval: 50
+ },
+
+ initialize: function(latlngs, options) {
+ L.setOptions(this, options);
+ this.path = latlngs;
+ var samples = this.samples = pathRegularSamples(this.path, this.options.samplingInterval);
+ var self = this;
+ this.horizZoom = 1;
+ this.dragStart = null;
+
+ this._getElevation(samples).then(function(values) {
+ self.values = values;
+ self.updateGraph();
+ }
+ );
+ this.values = null;
+
+ },
+
+ addTo: function(map) {
+ this._map = map;
+ var container = this._container = L.DomUtil.create('div', 'elevation-profile-container');
+ if (!L.Browser.touch) {
+ L.DomEvent
+ .disableClickPropagation(container)
+ .disableScrollPropagation(container);
+ } else {
+ L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation);
+ }
+ this._map._controlContainer.appendChild(container);
+ this.setupContainerLayout();
+ this.updateGraph();
+ this.trackMarker = L.marker([1000, 0], {clickable: false, icon: L.divIcon()});
+ this.polyline = L.polyline(this.path, {weight: 30, opacity: 0}).addTo(map);
+ this.polyline.on('mousemove', this.onLineMouseMove, this);
+ this.polyline.on('mouseover', this.onLineMouseEnter, this);
+ this.polyline.on('mouseout', this.onLineMouseLeave, this);
+ this.polyLineSelection = L.polyline([], {weight: 20, opacity: .5, color: 'yellow', lineCap: 'butt'});
+ return this;
+ },
+
+ setupContainerLayout: function() {
+ var horizZoom = this.horizZoom = 1;
+ var container = this._container;
+ this.propsContainer = L.DomUtil.create('div', 'elevation-profile-properties', container);
+ this.leftAxisLables = L.DomUtil.create('div', 'elevation-profile-left-axis', container);
+ this.closeButton = L.DomUtil.create('div', 'elevation-profile-close', container);
+ L.DomEvent.on(this.closeButton, 'click', this.onCloseButtonClick, this);
+ this.drawingContainer = L.DomUtil.create('div', 'elevation-profile-drawingContainer', container);
+ this.graphCursor = L.DomUtil.create('div', 'elevation-profile-cursor elevation-profile-cursor-hidden',
+ this.drawingContainer
+ );
+ this.graphCursorLabel =
+ L.DomUtil.create('div', 'elevation-profile-cursor-label elevation-profile-cursor-hidden',
+ this.drawingContainer
+ );
+ this.graphSelection = L.DomUtil.create('div', 'elevation-profile-selection elevation-profile-cursor-hidden',
+ this.drawingContainer
+ );
+ var svgWidth = this.svgWidth = this.drawingContainer.clientWidth * horizZoom,
+ svgHeight = this.svgHeight = this.drawingContainer.clientHeight;
+ var svg = this.svg = createSvg('svg', {width: svgWidth, height: svgHeight}, this.drawingContainer);
+ L.DomEvent.on(svg, 'mousemove', this.onSvgMouseMove, this);
+ L.DomEvent.on(svg, 'mouseenter', this.onSvgEnter, this);
+ L.DomEvent.on(svg, 'mouseleave', this.onSvgLeave, this);
+ L.DomEvent.on(svg, 'mousewheel', this.onSvgMouseWheel, this);
+ this.svgDragEvents = new DragEvents(this.drawingContainer, null, {dragButtons: {0: true, 2: true}});
+ this.svgDragEvents.on('dragstart', this.onSvgDragStart, this);
+ this.svgDragEvents.on('dragend', this.onSvgDragEnd, this);
+ this.svgDragEvents.on('drag', this.onSvgDrag, this);
+ this.svgDragEvents.on('click', this.onSvgClick, this);
+ L.DomEvent.on(svg, 'dblclick', this.onSvgDblClick, this);
+ },
+
+ removeFrom: function(map) {
+ if (!this._map) {
+ return;
+ }
+ this._map._controlContainer.removeChild(this._container);
+ map.removeLayer(this.polyline);
+ map.removeLayer(this.trackMarker);
+ map.removeLayer(this.polyLineSelection);
+ this._map = null;
+ return this;
+ },
+
+ onSvgDragStart: function(e) {
+
+ if (e.dragButton == 0) {
+ // FIXME: restore hiding when we make display of selection on map
+ // this.cursorHide();
+ this.polyLineSelection.addTo(this._map).bringToBack();
+ this.dragStart = e.offsetX;
+ }
+ },
+
+ xToIndex: function(x) {
+ return x / (this.svgWidth - 1) * (this.values.length - 1);
+ },
+
+ updateGraphSelection: function(e) {
+ if (this.dragStart === null) {
+ return;
+ }
+ var selStart, selEnd;
+ if (e) {
+ var x = e.offsetX;
+ selStart = Math.min(x, this.dragStart);
+ selEnd = Math.max(x, this.dragStart);
+ this.selStartInd = Math.round(this.xToIndex(selStart));
+ this.selEndInd = Math.round(this.xToIndex(selEnd));
+
+ if (this.selStartInd < 0) {
+ this.selStartInd = 0;
+ }
+ if (this.selEndInd > this.values.length - 1) {
+ this.selEndInd = this.values.length - 1;
+ }
+
+ } else {
+ selStart = this.selStartInd * (this.svgWidth - 1) / (this.values.length - 1);
+ selEnd = this.selEndInd * (this.svgWidth - 1) / (this.values.length - 1);
+ }
+ this.graphSelection.style.left = selStart + 'px';
+ this.graphSelection.style.width = (selEnd - selStart) + 'px';
+ L.DomUtil.removeClass(this.graphSelection, 'elevation-profile-cursor-hidden');
+ },
+
+ onSvgDragEnd: function(e) {
+ if (e.dragButton == 0) {
+ this.cursorShow();
+ this.updateGraphSelection(e);
+ var stats = this.calcProfileStats(this.values.slice(this.selStartInd, this.selEndInd + 1), true);
+ this.updatePropsDisplay(stats);
+ L.DomUtil.addClass(this.propsContainer, 'elevation-profile-properties-selected');
+ }
+ if (e.dragButton === 2) {
+ this.drawingContainer.scrollLeft -= e.movementX;
+ }
+ },
+
+ onSvgDrag: function(e) {
+ if (e.dragButton == 0) {
+ this.updateGraphSelection(e);
+ this.polyLineSelection.setLatLngs(this.samples.slice(this.selStartInd, this.selEndInd + 1));
+ }
+ if (e.dragButton == 2) {
+ this.drawingContainer.scrollLeft -= e.movementX;
+ }
+ },
+
+ onSvgClick: function(e) {
+ if (e.dragButton == 0) {
+ this.dragStart = null;
+ L.DomUtil.addClass(this.graphSelection, 'elevation-profile-cursor-hidden');
+ L.DomUtil.removeClass(this.propsContainer, 'elevation-profile-properties-selected');
+ this._map.removeLayer(this.polyLineSelection);
+ if (this.stats) {
+ this.updatePropsDisplay(this.stats);
+ }
+ }
+ if (e.dragButton == 2) {
+ this.setMapPositionAtIndex(Math.round(this.xToIndex(e.offsetX)));
+ }
+ },
+
+ onSvgDblClick: function(e) {
+ this.setMapPositionAtIndex(Math.round(this.xToIndex(e.offsetX)));
+ },
+
+ setMapPositionAtIndex: function(ind) {
+ var latlng = this.samples[ind];
+ if (latlng) {
+ this._map.panTo(latlng);
+ }
+ },
+
+ onSvgMouseWheel: function(e) {
+ var oldHorizZoom = this.horizZoom;
+ this.horizZoom += L.DomEvent.getWheelDelta(e);
+ if (this.horizZoom < 1) {
+ this.horizZoom = 1;
+ }
+ if (this.horizZoom > 10) {
+ this.horizZoom = 10;
+ }
+
+ var x = offestFromEvent(e).offsetX;
+ var ind = this.xToIndex(x);
+
+ var newScrollLeft = this.drawingContainer.scrollLeft +
+ offestFromEvent(e).offsetX * (this.horizZoom / oldHorizZoom - 1);
+ if (newScrollLeft < 0) {
+ newScrollLeft = 0;
+ }
+
+ this.svgWidth = this.drawingContainer.clientWidth * this.horizZoom;
+ this.svg.setAttribute('width', this.svgWidth + 'px');
+ this.setupGraph();
+ if (newScrollLeft > this.svgWidth - this.drawingContainer.clientWidth) {
+ newScrollLeft = this.svgWidth - this.drawingContainer.clientWidth;
+ }
+ this.drawingContainer.scrollLeft = newScrollLeft;
+
+ this.cursorHide();
+ this.setCursorPosition(ind);
+ this.cursorShow();
+ this.updateGraphSelection();
+ },
+
+
+ updateGraph: function() {
+ if (!this._map || !this.values) {
+ return;
+ }
+
+ this.stats = this.calcProfileStats(this.values);
+ this.updatePropsDisplay(this.stats);
+ this.setupGraph();
+ },
+
+ updatePropsDisplay: function(stats) {
+ if (!this._map) {
+ return;
+ }
+ this.propsContainer.innerHTML = '';
+ var ascentAngleStr = isNaN(stats.angleAvgAscent) ? '-' : L.Util.template('{avg} / {max}°',
+ {avg: stats.angleAvgAscent, max: stats.angleMaxAscent}
+ );
+ var descentAngleStr = isNaN(stats.angleAvgDescent) ? '-' : L.Util.template('{avg} / {max}°',
+ {avg: stats.angleAvgDescent, max: stats.angleMaxDescent}
+ );
+
+ this.propsContainer.innerHTML =
+ '<table>' +
+ '<tr><td>Max elevation:</td><td>' + Math.round(stats.max) + '</td></tr>' +
+ '<tr><td>Min elevation:</td><td>' + Math.round(stats.min) + '</td></tr>' +
+ '<tr class="start-group"><td>Start elevation:</td><td>' + Math.round(stats.start) + '</td></tr>' +
+ '<tr><td>Finish elevation:</td><td>' + Math.round(stats.end) + '</td></tr>' +
+ '<tr><td>Start to finish elevation change:</td><td>' + Math.round(stats.finalAscent) + '</td></tr>' +
+ '<tr class="start-group"><td>Avg / Max ascent inclination:</td><td>' + ascentAngleStr + '</td></tr>' +
+ '<tr><td>Avg / Max descent inclination:</td><td>' + descentAngleStr + '</td></tr>' +
+ '<tr class="start-group"><td>Total ascent:</td><td>' + Math.round(stats.ascent) + '</td></tr>' +
+ '<tr><td>Total descent:</td><td>' + Math.round(stats.descent) + '</td></tr>' +
+ '<tr class="start-group"><td>Distance:</td><td>' + (stats.distance / 1000).toFixed(1) + ' km</td></tr>' +
+ '</table>'
+ },
+
+ calcGridValues: function(minValue, maxValue) {
+ var ticksNs = [3, 4, 5],
+ tickSteps = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000],
+ ticks = [],
+ i, j, k, ticksN, tickStep, tick1, tick2;
+ for (i = 0; i < tickSteps.length; i++) {
+ tickStep = tickSteps[i];
+ for (j = 0; j < ticksNs.length; j++) {
+ ticksN = ticksNs[j];
+ tick1 = Math.floor(minValue / tickStep);
+ tick2 = Math.ceil(maxValue / tickStep);
+ if ((tick2 - tick1) < ticksN) {
+ for (k = tick1; k < tick1 + ticksN; k++) {
+ ticks.push(k * tickStep);
+ }
+ return ticks;
+ }
+ }
+ }
+ },
+
+ filterElevations: function(values, tolerance) {
+ var filtered = values.slice(0);
+ if (filtered.length < 3) {
+ return filtered;
+ }
+ var scanStart, scanEnd, job, linearValue, linearDelta, maxError, maxErrorInd, i, error;
+ var queue = [[0, filtered.length - 1]];
+ while (queue.length) {
+ job = queue.pop();
+ scanStart = job[0];
+ scanEnd = job[1];
+ linearValue = filtered[scanStart];
+ linearDelta = (filtered[scanEnd] - filtered[scanStart]) / (scanEnd - scanStart);
+ maxError = null;
+ maxErrorInd = null;
+ for (i = scanStart + 1; i < scanEnd; i++) {
+ linearValue += linearDelta;
+ error = Math.abs(filtered[i] - linearValue);
+ if (error === null || error > maxError) {
+ maxError = error;
+ maxErrorInd = i;
+ }
+ }
+ if (maxError > tolerance) {
+ if (scanEnd > scanStart + 2) {
+ queue.push([scanStart, maxErrorInd]);
+ queue.push([maxErrorInd, scanEnd]);
+ }
+ } else {
+ filtered.splice(scanStart + 1, scanEnd - scanStart - 1);
+ }
+ }
+ return filtered;
+ },
+
+ calcProfileStats: function(values, partial) {
+ var stats = {},
+ gradient, i;
+ stats.min = Math.min.apply(null, values);
+ stats.max = Math.max.apply(null, values);
+ stats.finalAscent = values[values.length - 1] - values[0];
+ var ascents = [],
+ descents = [];
+ for (i = 1; i < values.length; i++) {
+ gradient = (values[i] - values[i - 1]);
+ if (gradient > 0) {
+ ascents.push(gradient);
+ } else if (gradient < 0) {
+ descents.push(-gradient);
+ }
+ }
+ function sum(a, b) {
+ return a + b;
+ }
+
+ stats.gradientAvgAscent = ascents.reduce(sum, 0) / ascents.length / this.options.samplingInterval;
+ stats.gradientMinAscent = Math.min.apply(null, ascents) / this.options.samplingInterval;
+ stats.gradientMaxAscent = Math.max.apply(null, ascents) / this.options.samplingInterval;
+ stats.gradientAvgDescent = descents.reduce(sum, 0) / descents.length / this.options.samplingInterval;
+ stats.gradientMinDescent = Math.min.apply(null, descents) / this.options.samplingInterval;
+ stats.gradientMaxDescent = Math.max.apply(null, descents) / this.options.samplingInterval;
+
+ stats.angleAvgAscent = gradientToAngle(stats.gradientAvgAscent);
+ stats.angleMinAscent = gradientToAngle(stats.gradientMinAscent);
+ stats.angleMaxAscent = gradientToAngle(stats.gradientMaxAscent);
+ stats.angleAvgDescent = gradientToAngle(stats.gradientAvgDescent);
+ stats.angleMinDescent = gradientToAngle(stats.gradientMinDescent);
+ stats.angleMaxDescent = gradientToAngle(stats.gradientMaxDescent);
+
+ stats.start = values[0];
+ stats.end = values[values.length - 1];
+ stats.distance = (values.length - 1) * this.options.samplingInterval;
+
+ var filterTolerance = 5;
+ var filtered = this.filterElevations(values, filterTolerance);
+ var ascent = 0,
+ descent = 0,
+ delta;
+ for (i = 1; i < filtered.length; i++) {
+ delta = filtered[i] - filtered[i - 1];
+ if (delta < 0) {
+ descent += -delta;
+ } else {
+ ascent += delta;
+ }
+ }
+ stats.ascent = ascent;
+ stats.descent = descent;
+
+ return stats;
+
+ },
+
+ setCursorPosition: function(ind) {
+ var distance = this.options.samplingInterval * ind;
+ distance = (distance / 1000).toFixed(2);
+ var gradient = (this.values[Math.ceil(ind)] - this.values[Math.floor(ind)]) / this.options.samplingInterval;
+ var angle = Math.round(Math.atan(gradient) * 180 / Math.PI);
+ gradient = Math.round(gradient * 100);
+
+ var x = Math.round(ind / (this.values.length - 1) * (this.svgWidth - 1));
+ var indInt = Math.round(ind);
+ var elevation = this.values[indInt];
+ this.graphCursorLabel.innerHTML = L.Util.template('{ele} m<br>{dist} km<br>{angle}°',
+ {ele: Math.round(elevation), dist: distance, grad: gradient, angle: angle}
+ );
+
+ this.graphCursor.style.left = x + 'px';
+ this.graphCursorLabel.style.left = x + 'px';
+ if (this.drawingContainer.getBoundingClientRect().left - this.drawingContainer.scrollLeft + x +
+ this.graphCursorLabel.offsetWidth >= this._container.getBoundingClientRect().right) {
+ L.DomUtil.addClass(this.graphCursorLabel, 'elevation-profile-cursor-label-left');
+ } else {
+ L.DomUtil.removeClass(this.graphCursorLabel, 'elevation-profile-cursor-label-left');
+ }
+
+ var markerPos;
+ if (ind <= 0) {
+ markerPos = this.samples[0];
+ } else if (ind >= this.samples.length - 1) {
+ markerPos = this.samples[this.samples.length - 1];
+ } else {
+ var p1 = this.samples[Math.floor(ind)],
+ p2 = this.samples[Math.ceil(ind)],
+ indFrac = ind - Math.floor(ind);
+ markerPos = [p1.lat + (p2.lat - p1.lat) * indFrac, p1.lng + (p2.lng - p1.lng) * indFrac];
+ }
+ this.trackMarker.setLatLng(markerPos);
+ var label = L.Util.template('{ele} m<br>{dist} km<br>{angle}°',
+ {ele: Math.round(elevation), dist: distance, grad: gradient, angle: angle}
+ );
+ var icon = L.divIcon({
+ className: 'elevation-profile-marker',
+ html: '<div class="elevation-profile-marker-icon"></div><div class="elevation-profile-marker-label">' +
+ label + '</div>'
+ }
+ );
+ this.trackMarker.setIcon(icon);
+ },
+
+ onSvgMouseMove: function(e) {
+ if (!this.values) {
+ return;
+ }
+ var x = offestFromEvent(e).offsetX;
+ var ind = (x / (this.svgWidth - 1) * (this.values.length - 1));
+ this.setCursorPosition(ind);
+ },
+
+ cursorShow: function() {
+ L.DomUtil.removeClass(this.graphCursor, 'elevation-profile-cursor-hidden');
+ L.DomUtil.removeClass(this.graphCursorLabel, 'elevation-profile-cursor-hidden');
+ this._map.addLayer(this.trackMarker);
+ },
+
+ cursorHide: function() {
+ L.DomUtil.addClass(this.graphCursor, 'elevation-profile-cursor-hidden');
+ L.DomUtil.addClass(this.graphCursorLabel, 'elevation-profile-cursor-hidden');
+ this._map.removeLayer(this.trackMarker);
+ },
+
+ onSvgEnter: function() {
+ this.cursorShow();
+ },
+
+ onSvgLeave: function() {
+ this.cursorHide();
+ },
+
+ onLineMouseEnter: function() {
+ this.cursorShow();
+ },
+
+ onLineMouseLeave: function() {
+ this.cursorHide();
+ },
+
+ onLineMouseMove: function(e) {
+ function sqrDist(latlng1, latlng2) {
+ var dx = (latlng1.lng - latlng2.lng);
+ var dy = (latlng1.lat - latlng2.lat);
+ return dx * dx + dy * dy;
+ }
+
+ var nearestInd = null, ind,
+ minDist = null,
+ mouseLatlng = e.latlng,
+ i, sampleLatlng, dist, di;
+ for (i = 0; i < this.samples.length; i++) {
+ sampleLatlng = this.samples[i];
+ dist = sqrDist(sampleLatlng, mouseLatlng);
+ if (nearestInd === null || dist < minDist) {
+ nearestInd = i;
+ minDist = dist;
+ }
+ }
+
+ if (nearestInd !== null) {
+ ind = nearestInd;
+ if (nearestInd > 0) {
+ var prevDist = sqrDist(mouseLatlng, this.samples[nearestInd - 1]),
+ prevSampleDist = sqrDist(this.samples[nearestInd], this.samples[nearestInd - 1]);
+ }
+ if (nearestInd < this.samples.length - 1) {
+ var nextDist = sqrDist(mouseLatlng, this.samples[nearestInd + 1]),
+ nextSampleDist = sqrDist(this.samples[nearestInd], this.samples[nearestInd + 1]);
+ }
+
+ if (nearestInd === 0) {
+ if (nextDist < minDist + nextSampleDist) {
+ di = (minDist - nextDist) / 2 / nextSampleDist + 1 / 2;
+ } else {
+ di = .001;
+ }
+ } else if (nearestInd === this.samples.length - 1) {
+ if (prevDist < minDist + prevSampleDist) {
+ di = -((minDist - prevDist) / 2 / prevSampleDist + 1 / 2);
+ } else {
+ di = -0.001
+ }
+ } else {
+ if (prevDist < nextDist) {
+ di = -((minDist - prevDist) / 2 / prevSampleDist + 1 / 2);
+ } else {
+ di = (minDist - nextDist) / 2 / nextSampleDist + 1 / 2;
+ }
+ }
+ if (di < -1) {
+ di = -1;
+ }
+ if (di > 1) {
+ di = 1;
+ }
+ this.setCursorPosition(ind + di);
+ }
+
+ },
+
+
+ setupGraph: function() {
+ if (!this._map) {
+ return;
+ }
+
+ while (this.svg.hasChildNodes()) {
+ this.svg.removeChild(this.svg.lastChild);
+ }
+ while (this.leftAxisLables.hasChildNodes()) {
+ this.leftAxisLables.removeChild(this.leftAxisLables.lastChild);
+ }
+
+ var maxValue = Math.max.apply(null, this.values),
+ minValue = Math.min.apply(null, this.values),
+ svg = this.svg,
+ path, i, horizStep, verticalMultiplier, x, y, gridValues, label;
+
+
+ var paddingBottom = 8 + 16,
+ paddingTop = 8;
+
+ gridValues = this.calcGridValues(minValue, maxValue);
+ var gridStep = (this.svgHeight - paddingBottom - paddingTop) / (gridValues.length - 1);
+ for (i = 0; i < gridValues.length; i++) {
+ y = Math.round(i * gridStep - 0.5) + 0.5 + paddingTop;
+ path = L.Util.template('M{x1} {y} L{x2} {y}', {x1: 0, x2: this.svgWidth * this.horizZoom, y: y});
+ createSvg('path', {d: path, 'stroke-width': '1px', stroke: 'green', fill: 'none'}, svg);
+
+ label = L.DomUtil.create('div', 'elevation-profile-grid-label', this.leftAxisLables);
+ label.innerHTML = gridValues[gridValues.length - i - 1];
+ label.style.top = (gridStep * i + paddingTop) + 'px';
+ }
+
+ horizStep = this.svgWidth / (this.values.length - 1);
+ verticalMultiplier =
+ (this.svgHeight - paddingTop - paddingBottom) / (gridValues[gridValues.length - 1] - gridValues[0]);
+
+ path = [];
+ for (i = 0; i < this.values.length; i++) {
+ path.push(i ? 'L' : 'M');
+ x = i * horizStep;
+ y = (this.values[i] - gridValues[0]) * verticalMultiplier;
+ y = this.svgHeight - y - paddingBottom;
+ path.push(x + ' ' + y + ' ');
+ }
+ path = path.join('');
+ createSvg('path', {d: path, 'stroke-width': '1px', stroke: 'brown', fill: 'none'}, svg);
+ },
+
+ _getElevation: function(latlngs) {
+ function parseResponse(s) {
+ var values = [], v;
+ s = s.split('\n');
+ for (var i = 0; i < s.length; i++) {
+ if (s[i]) {
+ if (s[i] == 'NULL') {
+ v = 0;
+ } else {
+ v = parseFloat(s[i]);
+ }
+ values.push(v);
+ }
+ }
+ return values;
+ }
+
+ var req = [];
+ for (var i = 0; i < latlngs.length; i++) {
+ req.push(latlngs[i].lat.toFixed(6) + ' ' + latlngs[i].lng.toFixed(5));
+ }
+ req = req.join('\n');
+ return fetch(this.options.elevationsServer, {method: 'POST', data: req})
+ .then(
+ function(xhr) {
+ return parseResponse(xhr.responseText);
+ },
+ function() {
+ alert('Failed to plot elevation profile, server error');
+ }
+ );
+ },
+ onCloseButtonClick: function() {
+ this.removeFrom(this._map);
+ }
+ }
+);
diff --git a/src/lib/leaflet.control.track-list/track-list.js b/src/lib/leaflet.control.track-list/track-list.js
@@ -16,6 +16,7 @@ import 'lib/leaflet.layer.canvasMarkers/canvasMarkers';
import 'lib/leaflet.lineutil.simplifyLatLngs/simplify';
import iconFromBackgroundImage from 'lib/iconFromBackgroundImage/iconFromBackgroundImage';
import 'lib/controls-styles/controls-styles.css';
+import 'lib/leaflet.control.elevation-profile/elevation-profile';
var MeasuredEditableLine = L.MeasuredLine.extend({});
MeasuredEditableLine.include(L.Polyline.EditMixin);
diff --git a/src/lib/xhr-promise/xhr-promise.js b/src/lib/xhr-promise/xhr-promise.js
@@ -20,6 +20,7 @@ class XMLHttpRequestPromise {
this.catch = promise.catch.bind(promise);
this.method = method;
this.url = url;
+ this.postData = data;
this._isResponseSuccess = isResponseSuccess;
this._responseNeedsRetry = responseNeedsRetry;
this._retryTimeWait = retryTimeWait;
@@ -72,7 +73,7 @@ class XMLHttpRequestPromise {
send() {
// console.log('send', this.url);
this.triesLeft -= 1;
- this.xhr.send();
+ this.xhr.send(this.postData);
}
}