commit 83bce41a4811f4499063bf2b57f23b2702dcbc4d
parent 7468bf16aa9ef7723718b9fa0dc9864d3f81ae2c
Author: Sergej Orlov <wladimirych@gmail.com>
Date:   Thu,  1 Nov 2018 22:22:40 +0100
Merge branch '141-fix-elevation-profile-with-missing-data' into release-5
Diffstat:
1 file changed, 166 insertions(+), 70 deletions(-)
diff --git a/src/lib/leaflet.control.elevation-profile/index.js b/src/lib/leaflet.control.elevation-profile/index.js
@@ -397,7 +397,7 @@ const ElevationProfile = L.Class.extend({
             if (e.dragButton === 0) {
                 this.cursorShow();
                 this.updateGraphSelection(e);
-                var stats = this.calcProfileStats(this.values.slice(this.selStartInd, this.selEndInd + 1), true);
+                var stats = this.calcProfileStats(this.values.slice(this.selStartInd, this.selEndInd + 1));
                 this.updatePropsDisplay(stats);
                 L.DomUtil.addClass(this.propsContainer, 'elevation-profile-properties-selected');
             }
@@ -490,27 +490,62 @@ const ElevationProfile = L.Class.extend({
             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>'
+            let d;
+            if (stats.noData) {
+                d = {
+                    maxElev: '-',
+                    minElev: '-',
+                    startElev: '-',
+                    endElev: '-',
+                    change: '-',
+                    ascentAngleStr: '-',
+                    descentAngleStr: '-',
+                    ascent: '-',
+                    descent: '-',
+                    startApprox: '',
+                    endApprox: '',
+                    approx: '',
+                    incomplete: 'No elevation data',
+                }
+            } else {
+                d = {
+                    maxElev: Math.round(stats.max),
+                    minElev: Math.round(stats.min),
+                    startElev: Math.round(stats.start),
+                    endElev: Math.round(stats.end),
+                    change: Math.round(stats.finalAscent),
+                    ascentAngleStr: isNaN(stats.angleAvgAscent) ? '-' : L.Util.template('{avg} / {max}°',
+                            {avg: stats.angleAvgAscent, max: stats.angleMaxAscent}
+                        ),
+                    descentAngleStr: isNaN(stats.angleAvgDescent) ? '-' : L.Util.template('{avg} / {max}°',
+                            {avg: stats.angleAvgDescent, max: stats.angleMaxDescent}
+                        ),
+                    ascent: Math.round(stats.ascent),
+                    descent: Math.round(stats.descent),
+                    dist: (stats.distance / 1000).toFixed(1),
+                    startApprox: stats.dataLostAtStart > 0.02 ? '~ ' : '',
+                    endApprox: stats.dataLostAtEnd > 0.02 ? '~ ' : '',
+                    approx: stats.dataLost > 0.02 ? '~ ' : '',
+                    incomplete: stats.dataLost > 0.02 ? 'Some elevation data missing' : '',
+                };
+            }
+            d.dist = (stats.distance / 1000).toFixed(1);
+
+            this.propsContainer.innerHTML = `
+                <table>
+                <tr><td>Max elevation:</td><td>${d.maxElev}</td></tr>
+                <tr><td>Min elevation:</td><td>${d.minElev}</td></tr>
+                <tr class="start-group"><td>Start elevation:</td><td>${d.startApprox}${d.startElev}</td></tr>
+                <tr><td>Finish elevation:</td><td>${d.endApprox}${d.endElev}</td></tr>
+                <tr><td>Start to finish elevation change:</td><td>${d.startApprox || d.endApprox}${d.change}</td></tr>
+                <tr class="start-group"><td>Avg / Max ascent inclination:</td><td>${d.ascentAngleStr}</td></tr>
+                <tr><td>Avg / Max descent inclination:</td><td>${d.descentAngleStr}</td></tr>
+                <tr class="start-group"><td>Total ascent:</td><td>${d.approx}${d.ascent}</td></tr>
+                <tr><td>Total descent:</td><td>${d.approx}${d.descent}</td></tr>
+                <tr class="start-group"><td>Distance:</td><td>${d.dist} km</td></tr>
+                <tr><td colspan="2" style="text-align: center">${d.incomplete}</td></tr>
+                </table>
+                `
         },
 
         calcGridValues: function(minValue, maxValue) {
@@ -559,6 +594,9 @@ const ElevationProfile = L.Class.extend({
                 maxErrorInd = null;
                 for (i = scanStart + 1; i < scanEnd; i++) {
                     linearValue += linearDelta;
+                    if (filtered[i] === null) {
+                        continue
+                    }
                     error = Math.abs(filtered[i] - linearValue);
                     if (error === null || error > maxError) {
                         maxError = error;
@@ -577,50 +615,96 @@ const ElevationProfile = L.Class.extend({
             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 = [],
+        calcProfileStats: function(values) {
+            const stats = {
+                distance: (values.length - 1) * this.options.samplingInterval
+            };
+            const notNullValues = values.filter((value) => value !== null);
+            if (notNullValues.length === 0) {
+                stats.noData = true;
+                return stats;
+            }
+            stats.min = Math.min(...notNullValues);
+            stats.max = Math.max(...notNullValues);
+            let firstNotNullIndex = true,
+                lastNotNullIndex = true,
+                firstNotNullValue,
+                lastNotNullValue;
+            for (let i = 0; i < values.length; i++) {
+                let value = values[i];
+                if (value !== null) {
+                    firstNotNullValue = value;
+                    firstNotNullIndex = i;
+                    break;
+                }
+            }
+            for (let i = values.length - 1; i >= 0; i--) {
+                let value = values[i];
+                if (value !== null) {
+                    lastNotNullValue = value;
+                    lastNotNullIndex = i;
+                    break;
+                }
+            }
+            stats.finalAscent = lastNotNullValue - firstNotNullValue;
+
+            const ascents = [],
                 descents = [];
-            for (i = 1; i < values.length; i++) {
-                gradient = (values[i] - values[i - 1]);
+            let prevNotNullValue = values[firstNotNullIndex],
+                prevNotNullIndex=firstNotNullIndex;
+            for (let i = firstNotNullIndex + 1; i <= lastNotNullIndex; i++) {
+                let value = values[i];
+                if (value === null) {
+                    continue
+                }
+                let length = i - prevNotNullIndex;
+                let gradient = (value - prevNotNullValue) / length;
                 if (gradient > 0) {
-                    ascents.push(gradient);
+                    for (let j = 0; j < length; j++) {
+                        ascents.push(gradient);
+                    }
                 } else if (gradient < 0) {
-                    descents.push(-gradient);
+                    for (let j = 0; j < length; j++) {
+                        descents.push(-gradient);
+                    }
                 }
+                prevNotNullIndex = i;
+                prevNotNullValue = value;
             }
             function sum(a, b) {
                 return a + b;
             }
+            if (ascents.length !== 0) {
+                stats.gradientAvgAscent = ascents.reduce(sum, 0) / ascents.length / this.options.samplingInterval;
+                stats.gradientMinAscent = Math.min(...ascents) / this.options.samplingInterval;
+                stats.gradientMaxAscent = Math.max(...ascents) / this.options.samplingInterval;
+                stats.angleAvgAscent = gradientToAngle(stats.gradientAvgAscent);
+                stats.angleMinAscent = gradientToAngle(stats.gradientMinAscent);
+                stats.angleMaxAscent = gradientToAngle(stats.gradientMaxAscent);
 
-            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);
+            }
+            if (descents.length !== 0) {
+                stats.gradientAvgDescent = descents.reduce(sum, 0) / descents.length / this.options.samplingInterval;
+                stats.gradientMinDescent = Math.min(...descents) / this.options.samplingInterval;
+                stats.gradientMaxDescent = Math.max(...descents) / this.options.samplingInterval;
+                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.start = firstNotNullValue;
+            stats.end = lastNotNullValue;
             stats.distance = (values.length - 1) * this.options.samplingInterval;
+            stats.dataLostAtStart = firstNotNullIndex / values.length;
+            stats.dataLostAtEnd = 1 - lastNotNullIndex / (values.length - 1);
+            stats.dataLost = 1 - notNullValues.length / values.length;
 
-            var filterTolerance = 5;
-            var filtered = this.filterElevations(values, filterTolerance);
-            var ascent = 0,
+            const filterTolerance = 5;
+            const filtered = this.filterElevations(values.slice(firstNotNullIndex, lastNotNullIndex), filterTolerance);
+            let ascent = 0,
                 descent = 0,
                 delta;
-            for (i = 1; i < filtered.length; i++) {
+            for (let i = 1; i < filtered.length; i++) {
                 delta = filtered[i] - filtered[i - 1];
                 if (delta < 0) {
                     descent += -delta;
@@ -639,18 +723,33 @@ const ElevationProfile = L.Class.extend({
             if (!this._map || !this.values) {
                 return;
             }
-            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}
-            );
+            const samplingInterval = this.options.samplingInterval;
+            const distanceKm = samplingInterval * ind / 1000;
+            const distanceStr = `${distanceKm.toFixed(2)} km`;
+            const sample1 = this.values[Math.ceil(ind)];
+            const sample2 = this.values[Math.floor(ind)];
+            let angleStr;
+            if (sample1 !== null && sample2 !== null) {
+                const gradient = (sample2 - sample1) / samplingInterval;
+                angleStr = `${Math.round(Math.atan(gradient) * 180 / Math.PI)}°`;
+            } else {
+                angleStr = '-';
+            }
+
+            const x = Math.round(ind / (this.values.length - 1) * (this.svgWidth - 1));
+            const indInt = Math.round(ind);
+            let elevation = this.values[indInt];
+            if (elevation === null) {
+                elevation = sample1;
+            }
+            if (elevation === null) {
+                elevation = sample2;
+            }
+            const elevationStr = (elevation === null) ? '-' : `${elevation} m`;
+
+            const cursorLabel = `${elevationStr}<br>${distanceStr}<br>${angleStr}`;
+
+            this.graphCursorLabel.innerHTML = cursorLabel;
 
             this.graphCursor.style.left = x + 'px';
             this.graphCursorLabel.style.left = x + 'px';
@@ -661,23 +760,20 @@ const ElevationProfile = L.Class.extend({
                 L.DomUtil.removeClass(this.graphCursorLabel, 'elevation-profile-cursor-label-left');
             }
 
-            var markerPos;
+            let 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)],
+                const 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}
-            );
 
-            this.setTrackMarkerLabel(label);
+            this.setTrackMarkerLabel(cursorLabel);
         },
 
         onSvgMouseMove: function(e) {