diff --git a/package.json b/package.json index 4e4daba..149a80a 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "file-loader": "^3.0.1", "jest": "^23.6.0", "jquery": "^3.3.1", + "leaflet": "1.6.0", "ng-annotate-webpack-plugin": "^0.3.0", "style-loader": "^0.23.1", "ts-jest": "^23.10.5", diff --git a/src/data_formatter.ts b/src/data_formatter.ts index 13121f0..6c9f5f1 100644 --- a/src/data_formatter.ts +++ b/src/data_formatter.ts @@ -165,49 +165,51 @@ export default class DataFormatter { let highestValue = 0; let lowestValue = Number.MAX_VALUE; - tableData[0].forEach(datapoint => { - let key; - let longitude; - let latitude; - - if (this.ctrl.panel.tableQueryOptions.queryType === 'geohash') { - const encodedGeohash = datapoint[this.ctrl.panel.tableQueryOptions.geohashField]; - const decodedGeohash = decodeGeoHash(encodedGeohash); - - latitude = decodedGeohash.latitude; - longitude = decodedGeohash.longitude; - key = encodedGeohash; - } else { - latitude = datapoint[this.ctrl.panel.tableQueryOptions.latitudeField]; - longitude = datapoint[this.ctrl.panel.tableQueryOptions.longitudeField]; - key = `${latitude}_${longitude}`; - } - - const dataValue = { - key: key, - locationName: datapoint[this.ctrl.panel.tableQueryOptions.labelField] || 'n/a', - locationLatitude: latitude, - locationLongitude: longitude, - value: datapoint[this.ctrl.panel.tableQueryOptions.metricField], - valueFormatted: datapoint[this.ctrl.panel.tableQueryOptions.metricField], - valueRounded: 0, - }; - - if (dataValue.value > highestValue) { - highestValue = dataValue.value; - } - - if (dataValue.value < lowestValue) { - lowestValue = dataValue.value; - } - - dataValue.valueRounded = kbn.roundValue(dataValue.value, this.ctrl.panel.decimals || 0); - data.push(dataValue); + tableData.forEach(series => { + series.forEach(datapoint => { + let key; + let longitude; + let latitude; + + if (this.ctrl.panel.tableQueryOptions.queryType === 'geohash') { + const encodedGeohash = datapoint[this.ctrl.panel.tableQueryOptions.geohashField]; + const decodedGeohash = decodeGeoHash(encodedGeohash); + + latitude = decodedGeohash.latitude; + longitude = decodedGeohash.longitude; + key = encodedGeohash; + } else { + latitude = datapoint[this.ctrl.panel.tableQueryOptions.latitudeField]; + longitude = datapoint[this.ctrl.panel.tableQueryOptions.longitudeField]; + key = `${latitude}_${longitude}`; + } + + const dataValue = { + key: key, + locationName: datapoint[this.ctrl.panel.tableQueryOptions.labelField] || 'n/a', + locationLatitude: latitude, + locationLongitude: longitude, + value: datapoint[this.ctrl.panel.tableQueryOptions.metricField], + valueFormatted: datapoint[this.ctrl.panel.tableQueryOptions.metricField], + valueRounded: 0, + }; + + if (dataValue.value > highestValue) { + highestValue = dataValue.value; + } + + if (dataValue.value < lowestValue) { + lowestValue = dataValue.value; + } + + dataValue.valueRounded = kbn.roundValue(dataValue.value, this.ctrl.panel.decimals || 0); + data.push(dataValue); + }); + + data.highestValue = highestValue; + data.lowestValue = lowestValue; + data.valueRange = highestValue - lowestValue; }); - - data.highestValue = highestValue; - data.lowestValue = lowestValue; - data.valueRange = highestValue - lowestValue; } } diff --git a/src/libs/Polyline.encoded.js b/src/libs/Polyline.encoded.js new file mode 100644 index 0000000..957f6cb --- /dev/null +++ b/src/libs/Polyline.encoded.js @@ -0,0 +1,233 @@ +/* + * Utility functions to decode/encode numbers and array's of numbers + * to/from strings (Google maps polyline encoding) + * + * Extends the L.Polyline and L.Polygon object with methods to convert + * to and create from these strings. + * + * Jan Pieter Waagmeester + * + * Original code from: + * http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/ + * (which is down as of december 2014) + */ + +(function () { + 'use strict'; + + var defaultOptions = function (options) { + if (typeof options === 'number') { + // Legacy + options = { + precision: options + }; + } else { + options = options || {}; + } + + options.precision = options.precision || 5; + options.factor = options.factor || Math.pow(10, options.precision); + options.dimension = options.dimension || 2; + return options; + }; + + var PolylineUtil = { + encode: function (points, options) { + options = defaultOptions(options); + + var flatPoints = []; + for (var i = 0, len = points.length; i < len; ++i) { + var point = points[i]; + + if (options.dimension === 2) { + flatPoints.push(point.lat || point[0]); + flatPoints.push(point.lng || point[1]); + } else { + for (var dim = 0; dim < options.dimension; ++dim) { + flatPoints.push(point[dim]); + } + } + } + + return this.encodeDeltas(flatPoints, options); + }, + + decode: function (encoded, options) { + options = defaultOptions(options); + + var flatPoints = this.decodeDeltas(encoded, options); + + var points = []; + for (var i = 0, len = flatPoints.length; i + (options.dimension - 1) < len;) { + var point = []; + + for (var dim = 0; dim < options.dimension; ++dim) { + point.push(flatPoints[i++]); + } + + points.push(point); + } + + return points; + }, + + encodeDeltas: function (numbers, options) { + options = defaultOptions(options); + + var lastNumbers = []; + + for (var i = 0, len = numbers.length; i < len;) { + for (var d = 0; d < options.dimension; ++d, ++i) { + var num = numbers[i].toFixed(options.precision); + var delta = num - (lastNumbers[d] || 0); + lastNumbers[d] = num; + + numbers[i] = delta; + } + } + + return this.encodeFloats(numbers, options); + }, + + decodeDeltas: function (encoded, options) { + options = defaultOptions(options); + + var lastNumbers = []; + + var numbers = this.decodeFloats(encoded, options); + for (var i = 0, len = numbers.length; i < len;) { + for (var d = 0; d < options.dimension; ++d, ++i) { + numbers[i] = Math.round((lastNumbers[d] = numbers[i] + (lastNumbers[d] || 0)) * options.factor) / options.factor; + } + } + + return numbers; + }, + + encodeFloats: function (numbers, options) { + options = defaultOptions(options); + + for (var i = 0, len = numbers.length; i < len; ++i) { + numbers[i] = Math.round(numbers[i] * options.factor); + } + + return this.encodeSignedIntegers(numbers); + }, + + decodeFloats: function (encoded, options) { + options = defaultOptions(options); + + var numbers = this.decodeSignedIntegers(encoded); + for (var i = 0, len = numbers.length; i < len; ++i) { + numbers[i] /= options.factor; + } + + return numbers; + }, + + encodeSignedIntegers: function (numbers) { + for (var i = 0, len = numbers.length; i < len; ++i) { + var num = numbers[i]; + numbers[i] = (num < 0) ? ~(num << 1) : (num << 1); + } + + return this.encodeUnsignedIntegers(numbers); + }, + + decodeSignedIntegers: function (encoded) { + var numbers = this.decodeUnsignedIntegers(encoded); + + for (var i = 0, len = numbers.length; i < len; ++i) { + var num = numbers[i]; + numbers[i] = (num & 1) ? ~(num >> 1) : (num >> 1); + } + + return numbers; + }, + + encodeUnsignedIntegers: function (numbers) { + var encoded = ''; + for (var i = 0, len = numbers.length; i < len; ++i) { + encoded += this.encodeUnsignedInteger(numbers[i]); + } + return encoded; + }, + + decodeUnsignedIntegers: function (encoded) { + var numbers = []; + + var current = 0; + var shift = 0; + + for (var i = 0, len = encoded.length; i < len; ++i) { + var b = encoded.charCodeAt(i) - 63; + + current |= (b & 0x1f) << shift; + + if (b < 0x20) { + numbers.push(current); + current = 0; + shift = 0; + } else { + shift += 5; + } + } + + return numbers; + }, + + encodeSignedInteger: function (num) { + num = (num < 0) ? ~(num << 1) : (num << 1); + return this.encodeUnsignedInteger(num); + }, + + // This function is very similar to Google's, but I added + // some stuff to deal with the double slash issue. + encodeUnsignedInteger: function (num) { + var value, encoded = ''; + while (num >= 0x20) { + value = (0x20 | (num & 0x1f)) + 63; + encoded += (String.fromCharCode(value)); + num >>= 5; + } + value = num + 63; + encoded += (String.fromCharCode(value)); + + return encoded; + } + }; + + // Export Node module + if (typeof module === 'object' && typeof module.exports === 'object') { + module.exports = PolylineUtil; + } + + // Inject functionality into Leaflet + if (typeof L === 'object') { + if (!(L.Polyline.prototype.fromEncoded)) { + L.Polyline.fromEncoded = function (encoded, options) { + return L.polyline(PolylineUtil.decode(encoded), options); + }; + } + if (!(L.Polygon.prototype.fromEncoded)) { + L.Polygon.fromEncoded = function (encoded, options) { + return L.polygon(PolylineUtil.decode(encoded), options); + }; + } + + var encodeMixin = { + encodePath: function () { + return PolylineUtil.encode(this.getLatLngs()); + } + }; + + if (!L.Polyline.prototype.encodePath) { + L.Polyline.include(encodeMixin); + } + if (!L.Polygon.prototype.encodePath) { + L.Polygon.include(encodeMixin); + } + + L.PolylineUtil = PolylineUtil; + } +})(); diff --git a/src/partials/editor.html b/src/partials/editor.html index 63c474e..cd54206 100644 --- a/src/partials/editor.html +++ b/src/partials/editor.html @@ -52,7 +52,7 @@
Map Data Options
-
diff --git a/src/worldmap.ts b/src/worldmap.ts index 341f9a6..62f8300 100644 --- a/src/worldmap.ts +++ b/src/worldmap.ts @@ -1,5 +1,6 @@ import * as _ from 'lodash'; -import * as L from './libs/leaflet'; +import * as L from 'leaflet'; +import './libs/Polyline.encoded'; import WorldmapCtrl from './worldmap_ctrl'; const tileServers = { @@ -26,6 +27,7 @@ export default class WorldMap { map: any; legend: any; circlesLayer: any; + polylineLayer: any; constructor(ctrl, mapContainer) { this.ctrl = ctrl; @@ -45,6 +47,7 @@ export default class WorldMap { zoom: parseInt(this.ctrl.panel.initialZoom, 10) || 1, }); this.setMouseWheelZoom(); + this.polylineLayer = L.featureGroup(); const selectedTileServer = tileServers[this.ctrl.tileServer]; (window).L.tileLayer(selectedTileServer.url, { @@ -253,6 +256,23 @@ export default class WorldMap { this.map.setZoom(parseInt(zoomFactor, 10)); } + addPolyline(encoded) { + var coordinates = L.Polyline.fromEncoded(encoded).getLatLngs(); + L.polyline(coordinates).addTo(this.polylineLayer); + this.polylineLayer.addTo(this.map); + } + + addPolylines(series, doCenter) { + series.map(line => { + return this.addPolyline(line.polyline); + }); + + if (doCenter) { + this.map.fitBounds(this.polylineLayer.getBounds()); + } + } + + remove() { this.circles = []; if (this.circlesLayer) { diff --git a/src/worldmap_ctrl.ts b/src/worldmap_ctrl.ts index 4aa746c..cdb8f54 100644 --- a/src/worldmap_ctrl.ts +++ b/src/worldmap_ctrl.ts @@ -195,6 +195,13 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { } else if (this.panel.locationData === "table") { const tableData = dataList.map(DataFormatter.tableHandler.bind(this)); this.dataFormatter.setTableValues(tableData, data); + } else if (this.panel.locationData === "polyline") { + this.series = dataList.map(c => ({ + // Todo: Hard code for now, but this should eventually become configurable. + // But, it may make sense to convert to react first? + polyline: c.meta.map.summary_polyline, + name: c.meta.name, + })); } else if (this.panel.locationData === "json result") { this.series = dataList; this.dataFormatter.setJsonValues(data); @@ -294,8 +301,10 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { changeLocationData() { this.loadLocationDataFromFile(true); - if (this.panel.locationData === "geohash") { - this.render(); + switch (this.panel.locationData) { + case "geohash": + this.render(); + break; } } @@ -307,7 +316,7 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { ctrl.renderingCompleted(); }); - function render() { + function render() { if (!ctrl.data) { return; } @@ -332,6 +341,10 @@ export default class WorldmapCtrl extends MetricsPanelCtrl { } ctrl.map.resize(); + + if (ctrl.panel.locationData === "polyline") { + ctrl.map.addPolylines(ctrl.series, true); + } if (ctrl.mapCenterMoved) { ctrl.map.panToMapCenter(); diff --git a/yarn.lock b/yarn.lock index df35947..8bd1645 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3789,6 +3789,11 @@ lcid@^2.0.0: dependencies: invert-kv "^2.0.0" +leaflet@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308" + integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ== + left-pad@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"