diff --git a/dist/locales/en.json b/dist/locales/en.json index 0e247edf1..97b5a1693 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -646,7 +646,7 @@ }, "custom_data": { "tooltip": "Edit custom data layer", - "header": "Custom Data Settings", + "header": "Custom Map Data Settings", "file": { "instructions": "Choose a local data file. Supported types are:\n .gpx, .kml, .geojson/.json, .pbf, .mvt", "label": "Browse files" diff --git a/modules/renderer/background.js b/modules/renderer/background.js index b227784ca..2754c1001 100644 --- a/modules/renderer/background.js +++ b/modules/renderer/background.js @@ -1,5 +1,4 @@ import _find from 'lodash-es/find'; -import _omit from 'lodash-es/omit'; import { dispatch as d3_dispatch } from 'd3-dispatch'; import { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate'; diff --git a/modules/services/index.js b/modules/services/index.js index 789628ed5..83cc8114b 100644 --- a/modules/services/index.js +++ b/modules/services/index.js @@ -4,6 +4,7 @@ import serviceOpenstreetcam from './openstreetcam'; import serviceOsm from './osm'; import serviceStreetside from './streetside'; import serviceTaginfo from './taginfo'; +import serviceVectorTile from './vector_tile'; import serviceWikidata from './wikidata'; import serviceWikipedia from './wikipedia'; @@ -14,6 +15,7 @@ export var services = { osm: serviceOsm, streetside: serviceStreetside, taginfo: serviceTaginfo, + vectorTile: serviceVectorTile, wikidata: serviceWikidata, wikipedia: serviceWikipedia }; @@ -25,6 +27,7 @@ export { serviceOsm, serviceStreetside, serviceTaginfo, + serviceVectorTile, serviceWikidata, serviceWikipedia }; diff --git a/modules/services/vector_tile.js b/modules/services/vector_tile.js new file mode 100644 index 000000000..03c1dbd42 --- /dev/null +++ b/modules/services/vector_tile.js @@ -0,0 +1,147 @@ +import _find from 'lodash-es/find'; +import _forEach from 'lodash-es/forEach'; + +import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { request as d3_request } from 'd3-request'; + +import Protobuf from 'pbf'; +import vt from '@mapbox/vector-tile'; + +import { utilRebind, utilTiler } from '../util'; + + +var tiler = utilTiler().tileSize(512); +var dispatch = d3_dispatch('loadedData'); +var _vtCache; + + +function abortRequest(i) { + i.abort(); +} + + +function vtToGeoJSON(bufferdata) { + var tile = new vt.VectorTile(new Protobuf(bufferdata.data.response)); + var layers = Object.keys(tile.layers); + if (!Array.isArray(layers)) { layers = [layers]; } + + var collection = { type: 'FeatureCollection', features: [] }; + + layers.forEach(function (layerID) { + var layer = tile.layers[layerID]; + if (layer) { + for (var i = 0; i < layer.length; i++) { + var feature = layer.feature(i).toGeoJSON(bufferdata.zxy[2], bufferdata.zxy[3], bufferdata.zxy[1]); + if (layers.length > 1) feature.properties.vt_layer = layerID; + collection.features.push(feature); + } + } + }); + + return collection; +} + + +function loadTile(source, tile) { + if (source.loaded[tile.id] || source.inflight[tile.id]) return; + + var url = source.template + .replace('{x}', tile.xyz[0]) + .replace('{y}', tile.xyz[1]) + // TMS-flipped y coordinate + .replace(/\{[t-]y\}/, Math.pow(2, tile.xyz[2]) - tile.xyz[1] - 1) + .replace(/\{z(oom)?\}/, tile.xyz[2]) + .replace(/\{switch:([^}]+)\}/, function(s, r) { + var subdomains = r.split(','); + return subdomains[(tile.xyz[0] + tile.xyz[1]) % subdomains.length]; + }); + + + source.inflight[tile.id] = d3_request(url) + .responseType('arraybuffer') + .get(function(err, data) { + source.loaded[tile.id] = true; + delete source.inflight[tile.id]; + if (err || !data) return; + + source.loaded[tile.id] = { + bufferdata: data, + geojson: vtToGeoJSON(data) + }; + + dispatch.call('loadedData'); + }); +} + + +export default { + + init: function() { + if (!_vtCache) { + this.reset(); + } + + this.event = utilRebind(this, dispatch, 'on'); + }, + + + reset: function() { + for (var sourceID in _vtCache) { + var source = _vtCache[sourceID]; + if (source && source.inflight) { + _forEach(source.inflight, abortRequest); + } + } + + _vtCache = {}; + }, + + + addSource: function(sourceID, template) { + _vtCache[sourceID] = { template: template, inflight: {}, loaded: {} }; + return _vtCache[sourceID]; + }, + + + data: function(sourceID, projection) { + var source = _vtCache[sourceID]; + if (!source) return []; + + // for now, return the FeatureCollection for each tile + var tiles = tiler.getTiles(projection); + return tiles.map(function(tile) { + var loaded = source.loaded[tile.id]; + return loaded && loaded.geojson; + }).filter(Boolean); + }, + + + loadTiles: function(sourceID, template, projection) { + var source = _vtCache[sourceID]; + if (!source) { + source = this.addSource(sourceID, template); + } + + var tiles = tiler.getTiles(projection); + + // abort inflight requests that are no longer needed + _forEach(source.inflight, function(v, k) { + var wanted = _find(tiles, function(tile) { return k === tile.id; }); + + if (!wanted) { + abortRequest(v); + delete source.inflight[k]; + } + }); + + tiles.forEach(function(tile) { + loadTile(source, tile); + }); + }, + + + cache: function() { + return _vtCache; + } + +};