diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index be802579e..e88be04cc 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -324,7 +324,7 @@ correspondence with entities:
* `iD.svgLayers` - sets up a number of layers that ensure that map elements
appear in an appropriate z-order.
* `iD.svgOsm` - sets up the OSM-specific data layers
-* `iD.svgGpx` - draws gpx traces
+* `iD.svgData` - draws any other overlaid vector data (gpx, kml, geojson, mvt, pbf)
* `iD.svgDebug` - draws debugging information
### Other UI
diff --git a/css/20_map.css b/css/20_map.css
index 6ec2927ba..ce45960a9 100644
--- a/css/20_map.css
+++ b/css/20_map.css
@@ -307,60 +307,30 @@ g.turn circle {
}
-/* GPX Paths */
+/* Other Data (gpx, kml, geojson, mvt, pbf) */
-.layer-gpx {
+.layer-geojson {
pointer-events: none;
}
-path.gpx {
+.layer-geojson path {
stroke: #ff26d4;
stroke-width: 2;
fill: none;
}
-text.gpxlabel-halo,
-text.gpxlabel {
+.layer-geojson text.label-halo,
+.layer-geojson text.label {
font-size: 10px;
font-weight: bold;
dominant-baseline: middle;
}
-text.gpxlabel {
+.layer-geojson text.label {
fill: #ff26d4;
}
-text.gpxlabel-halo {
- opacity: 0.7;
- stroke: #000;
- stroke-width: 5px;
- stroke-miterlimit: 1;
-}
-
-/* MVT Paths */
-
-.layer-mvt {
- pointer-events: none;
-}
-
-path.mvt {
- stroke: #ff26d4;
- stroke-width: 2;
- fill: none;
-}
-
-text.mvtlabel-halo,
-text.mvtlabel {
- font-size: 10px;
- font-weight: bold;
- dominant-baseline: middle;
-}
-
-text.mvtlabel {
- fill: #ff26d4;
-}
-
-text.mvtlabel-halo {
+.layer-geojson text.label-halo {
opacity: 0.7;
stroke: #000;
stroke-width: 5px;
diff --git a/css/80_app.css b/css/80_app.css
index 55bf424dc..7749c193c 100644
--- a/css/80_app.css
+++ b/css/80_app.css
@@ -2566,8 +2566,7 @@ div.full-screen > button:hover {
float: right;
}
-[dir='rtl'] .list-item-gpx-browse svg,
-[dir='rtl'] .list-item-mvt-browse svg {
+[dir='rtl'] .list-item-data-browse svg {
transform: rotateY(180deg);
}
diff --git a/modules/renderer/background.js b/modules/renderer/background.js
index 71325195f..ff9010004 100644
--- a/modules/renderer/background.js
+++ b/modules/renderer/background.js
@@ -171,10 +171,10 @@ export function rendererBackground(context) {
.filter(function (d) { return !d.source().isLocatorOverlay() && !d.source().isHidden(); })
.forEach(function (d) { imageryUsed.push(d.source().imageryUsed()); });
- var gpx = context.layers().layer('gpx');
- if (gpx && gpx.enabled() && gpx.hasGpx()) {
+ var data = context.layers().layer('data');
+ if (data && data.enabled() && data.hasData()) {
// Include a string like '.gpx data file' or '.geojson data file'
- var match = gpx.getSrc().match(/(kml|gpx|(?:geo)?json)$/i);
+ var match = data.getSrc().match(/(kml|gpx|pbf|mvt|(?:geo)?json)$/i);
var extension = match ? ('.' + match[0].toLowerCase() + ' ') : '';
imageryUsed.push(extension + 'data file');
}
@@ -184,14 +184,6 @@ export function rendererBackground(context) {
imageryUsed.push('Bing Streetside');
}
- var mvt = context.layers().layer('mvt');
- if (mvt && mvt.enabled() && mvt.hasMvt()) {
- // Include a string like '.mvt data file' or '.geojson data file'
- var matchmvt = mvt.getSrc().match(/(pbf|mvt|(?:geo)?json)$/i);
- var extensionmvt = matchmvt ? ('.' + matchmvt[0].toLowerCase() + ' ') : '';
- imageryUsed.push(extensionmvt + 'data file');
- }
-
var mapillary_images = context.layers().layer('mapillary-images');
if (mapillary_images && mapillary_images.enabled()) {
imageryUsed.push('Mapillary Images');
@@ -464,19 +456,12 @@ export function rendererBackground(context) {
});
if (q.gpx) {
- var gpx = context.layers().layer('gpx');
+ var gpx = context.layers().layer('data');
if (gpx) {
gpx.url(q.gpx);
}
}
- if (q.mvt) {
- var mvt = context.layers().layer('mvt');
- if (mvt) {
- mvt.url(q.mvt);
- }
- }
-
if (q.offset) {
var offset = q.offset.replace(/;/g, ',').split(',').map(function(n) {
return !isNaN(n) && n;
diff --git a/modules/svg/gpx.js b/modules/svg/data.js
similarity index 53%
rename from modules/svg/gpx.js
rename to modules/svg/data.js
index cfd785742..350d5d76a 100644
--- a/modules/svg/gpx.js
+++ b/modules/svg/data.js
@@ -4,16 +4,24 @@ import _reduce from 'lodash-es/reduce';
import _union from 'lodash-es/union';
import { geoBounds as d3_geoBounds } from 'd3-geo';
-import { text as d3_text } from 'd3-request';
+
+import {
+ request as d3_request,
+ text as d3_text
+} from 'd3-request';
+
import {
event as d3_event,
select as d3_select
} from 'd3-selection';
+import toGeoJSON from '@mapbox/togeojson';
+import vt from '@mapbox/vector-tile';
+import Protobuf from 'pbf';
+
import { geoExtent, geoPolygonIntersectsPolygon } from '../geo';
import { svgPath } from './index';
import { utilDetect } from '../util/detect';
-import toGeoJSON from '@mapbox/togeojson';
var _initialized = false;
@@ -21,7 +29,7 @@ var _enabled = false;
var _geojson;
-export function svgGpx(projection, context, dispatch) {
+export function svgData(projection, context, dispatch) {
var _showLabels = true;
var detected = utilDetect();
var layer;
@@ -42,24 +50,24 @@ export function svgGpx(projection, context, dispatch) {
d3_select('body')
.attr('dropzone', 'copy')
- .on('drop.localgpx', function() {
+ .on('drop.svgData', function() {
d3_event.stopPropagation();
d3_event.preventDefault();
if (!detected.filedrop) return;
- drawGpx.files(d3_event.dataTransfer.files);
+ drawData.files(d3_event.dataTransfer.files);
})
- .on('dragenter.localgpx', over)
- .on('dragexit.localgpx', over)
- .on('dragover.localgpx', over);
+ .on('dragenter.svgData', over)
+ .on('dragexit.svgData', over)
+ .on('dragover.svgData', over);
_initialized = true;
}
- function drawGpx(selection) {
+ function drawData(selection) {
var getPath = svgPath(projection).geojson;
- layer = selection.selectAll('.layer-gpx')
+ layer = selection.selectAll('.layer-geojson')
.data(_enabled ? [0] : []);
layer.exit()
@@ -67,7 +75,7 @@ export function svgGpx(projection, context, dispatch) {
layer = layer.enter()
.append('g')
- .attr('class', 'layer-gpx')
+ .attr('class', 'layer-geojson')
.merge(layer);
@@ -80,7 +88,7 @@ export function svgGpx(projection, context, dispatch) {
paths = paths.enter()
.append('path')
- .attr('class', 'gpx')
+ .attr('class', 'pathdata')
.merge(paths);
paths
@@ -91,8 +99,8 @@ export function svgGpx(projection, context, dispatch) {
labelData = labelData.filter(getPath);
layer
- .call(drawLabels, 'gpxlabel-halo', labelData)
- .call(drawLabels, 'gpxlabel', labelData);
+ .call(drawLabels, 'label-halo', labelData)
+ .call(drawLabels, 'label', labelData);
function drawLabels(selection, textClass, data) {
@@ -126,11 +134,6 @@ export function svgGpx(projection, context, dispatch) {
}
- function toDom(x) {
- return (new DOMParser()).parseFromString(x, 'text/xml');
- }
-
-
function getExtension(fileName) {
if (fileName === undefined) {
return '';
@@ -145,43 +148,76 @@ export function svgGpx(projection, context, dispatch) {
}
- function parseSaveAndZoom(extension, data, src) {
+ function toDom(textdata) {
+ return (new DOMParser()).parseFromString(textdata, 'text/xml');
+ }
+
+
+ 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 parseSaveAndZoom(extension, data, name) {
switch (extension) {
- default:
- drawGpx.geojson(toGeoJSON.gpx(toDom(data)), src).fitZoom();
+ case '.gpx':
+ drawData.geojson(toGeoJSON.gpx(toDom(data)), name).fitZoom();
break;
case '.kml':
- drawGpx.geojson(toGeoJSON.kml(toDom(data)), src).fitZoom();
+ drawData.geojson(toGeoJSON.kml(toDom(data)), name).fitZoom();
+ break;
+ case '.pbf':
+ drawData.geojson(vtToGeoJSON(data), name).fitZoom();
+ break;
+ case '.mvt':
+ drawData.geojson(vtToGeoJSON(data), name).fitZoom();
break;
case '.geojson':
case '.json':
- drawGpx.geojson(JSON.parse(data), src).fitZoom();
+ drawData.geojson(JSON.parse(data), name).fitZoom();
break;
}
}
- drawGpx.showLabels = function(_) {
+ drawData.showLabels = function(val) {
if (!arguments.length) return _showLabels;
- _showLabels = _;
+ _showLabels = val;
return this;
};
- drawGpx.enabled = function(_) {
+ drawData.enabled = function(val) {
if (!arguments.length) return _enabled;
- _enabled = _;
+ _enabled = val;
dispatch.call('change');
return this;
};
- drawGpx.hasGpx = function() {
+ drawData.hasData = function() {
return (!(_isEmpty(_geojson) || _isEmpty(_geojson.features)));
};
- drawGpx.geojson = function(gj, src) {
+ drawData.geojson = function(gj, src) {
if (!arguments.length) return _geojson;
if (_isEmpty(gj) || _isEmpty(gj.features)) return this;
_geojson = gj;
@@ -191,41 +227,82 @@ export function svgGpx(projection, context, dispatch) {
};
- drawGpx.url = function(url) {
- d3_text(url, function(err, data) {
- if (!err) {
- var extension = getExtension(url);
- parseSaveAndZoom(extension, data, url);
- }
- });
+ drawData.url = function(url) {
+ var extension = getExtension(url);
+ if (extension === 'mvt' || extension === 'pbf') {
+ d3_request(url)
+ .responseType('arraybuffer')
+ .get(function(err, data) {
+ if (err || !data) return;
+ _src = url;
+ var match = url.match(/(pbf|mvt)/i);
+ var extension = match ? ('.' + match[0].toLowerCase()) : '';
+ var zxy = url.match(/\/(\d+)\/(\d+)\/(\d+)/);
+ var bufferdata = {
+ data : data,
+ zxy : zxy
+ };
+ parseSaveAndZoom(extension, bufferdata);
+ });
+ } else {
+ d3_text(url, function(err, data) {
+ if (!err) {
+ parseSaveAndZoom(extension, data, url);
+ }
+ });
+ }
+
return this;
};
- drawGpx.files = function(fileList) {
+ drawData.files = function(fileList) {
if (!fileList.length) return this;
var f = fileList[0];
var reader = new FileReader();
+ var extension = getExtension(f.name);
- reader.onload = (function(file) {
- var extension = getExtension(file.name);
- return function (e) {
- parseSaveAndZoom(extension, e.target.result, file.name);
- };
- })(f);
+ if (extension === 'mvt' || extension === 'pbf') {
+ reader.onload = (function(file) {
+ return; // todo find x,y,z
+ var data = [];
+ var zxy = [0,0,0];
+
+ _src = file.name;
+ var extension = getExtension(file.name);
+ var bufferdata = {
+ data: data,
+ zxy: zxy
+ };
+ return function (e) {
+ bufferdata.data = e.target.result;
+ parseSaveAndZoom(extension, bufferdata);
+ };
+ })(f);
+
+ reader.readAsArrayBuffer(f);
+
+ } else {
+ reader.onload = (function(file) {
+ return function (e) {
+ parseSaveAndZoom(extension, e.target.result, file.name);
+ };
+ })(f);
+
+ reader.readAsText(f);
+ }
- reader.readAsText(f);
return this;
};
- drawGpx.getSrc = function () {
+ drawData.getSrc = function() {
return _src;
};
- drawGpx.fitZoom = function() {
- if (!this.hasGpx()) return this;
+ drawData.fitZoom = function() {
+ if (!this.hasData()) return this;
var map = context.map();
var viewport = map.trimmedExtent().polygon();
@@ -262,5 +339,5 @@ export function svgGpx(projection, context, dispatch) {
init();
- return drawGpx;
+ return drawData;
}
diff --git a/modules/svg/index.js b/modules/svg/index.js
index c7760b2aa..4b1bd37a3 100644
--- a/modules/svg/index.js
+++ b/modules/svg/index.js
@@ -1,8 +1,7 @@
export { svgAreas } from './areas.js';
+export { svgData } from './data.js';
export { svgDebug } from './debug.js';
export { svgDefs } from './defs.js';
-export { svgGpx } from './gpx.js';
-export { svgMvt } from './mvt.js';
export { svgIcon } from './icon.js';
export { svgLabels } from './labels.js';
export { svgLayers } from './layers.js';
diff --git a/modules/svg/layers.js b/modules/svg/layers.js
index 31067aea2..7493cc695 100644
--- a/modules/svg/layers.js
+++ b/modules/svg/layers.js
@@ -7,10 +7,9 @@ import _reject from 'lodash-es/reject';
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { select as d3_select } from 'd3-selection';
+import { svgData } from './data';
import { svgDebug } from './debug';
-import { svgGpx } from './gpx';
import { svgStreetside } from './streetside';
-import { svgMvt } from './mvt';
import { svgMapillaryImages } from './mapillary_images';
import { svgMapillarySigns } from './mapillary_signs';
import { svgOpenstreetcamImages } from './openstreetcam_images';
@@ -26,8 +25,7 @@ export function svgLayers(projection, context) {
var layers = [
{ id: 'osm', layer: svgOsm(projection, context, dispatch) },
{ id: 'notes', layer: svgNotes(projection, context, dispatch) },
- { id: 'gpx', layer: svgGpx(projection, context, dispatch) },
- { id: 'mvt', layer: svgMvt(projection, context, dispatch) },
+ { id: 'data', layer: svgData(projection, context, dispatch) },
{ id: 'streetside', layer: svgStreetside(projection, context, dispatch)},
{ id: 'mapillary-images', layer: svgMapillaryImages(projection, context, dispatch) },
{ id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) },
diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js
index 974e1c0ba..d1f86a7b1 100644
--- a/modules/ui/map_data.js
+++ b/modules/ui/map_data.js
@@ -207,14 +207,14 @@ export function uiMapData(context) {
}
- function drawGpxItem(selection) {
- var gpx = layers.layer('gpx');
- var hasGpx = gpx && gpx.hasGpx();
- var showsGpx = hasGpx && gpx.enabled();
+ function drawDataItems(selection) {
+ var dataLayer = layers.layer('data');
+ var hasData = dataLayer && dataLayer.hasData();
+ var showsData = hasData && dataLayer.enabled();
var ul = selection
- .selectAll('.layer-list-gpx')
- .data(gpx ? [0] : []);
+ .selectAll('.layer-list-data')
+ .data(dataLayer ? [0] : []);
// Exit
ul.exit()
@@ -223,15 +223,15 @@ export function uiMapData(context) {
// Enter
var ulEnter = ul.enter()
.append('ul')
- .attr('class', 'layer-list layer-list-gpx');
+ .attr('class', 'layer-list layer-list-data');
var liEnter = ulEnter
.append('li')
- .attr('class', 'list-item-gpx');
+ .attr('class', 'list-item-data');
liEnter
.append('button')
- .attr('class', 'list-item-gpx-extent')
+ .attr('class', 'list-item-data-extent')
.call(tooltip()
.title(t('gpx.zoom'))
.placement((textDirection === 'rtl') ? 'right' : 'left')
@@ -239,13 +239,13 @@ export function uiMapData(context) {
.on('click', function() {
d3_event.preventDefault();
d3_event.stopPropagation();
- gpx.fitZoom();
+ dataLayer.fitZoom();
})
.call(svgIcon('#iD-icon-search'));
liEnter
.append('button')
- .attr('class', 'list-item-gpx-browse')
+ .attr('class', 'list-item-data-browse')
.call(tooltip()
.title(t('gpx.browse'))
.placement((textDirection === 'rtl') ? 'right' : 'left')
@@ -254,7 +254,7 @@ export function uiMapData(context) {
d3_select(document.createElement('input'))
.attr('type', 'file')
.on('change', function() {
- gpx.files(d3_event.target.files);
+ dataLayer.files(d3_event.target.files);
})
.node().click();
})
@@ -270,7 +270,7 @@ export function uiMapData(context) {
labelEnter
.append('input')
.attr('type', 'checkbox')
- .on('change', function() { toggleLayer('gpx'); });
+ .on('change', function() { toggleLayer('data'); });
labelEnter
.append('span')
@@ -280,96 +280,15 @@ export function uiMapData(context) {
ul = ul
.merge(ulEnter);
- ul.selectAll('.list-item-gpx')
- .classed('active', showsGpx)
+ ul.selectAll('.list-item-data')
+ .classed('active', showsData)
.selectAll('label')
- .classed('deemphasize', !hasGpx)
+ .classed('deemphasize', !hasData)
.selectAll('input')
- .property('disabled', !hasGpx)
- .property('checked', showsGpx);
+ .property('disabled', !hasData)
+ .property('checked', showsData);
}
- function drawMvtItem(selection) {
- var mvt = layers.layer('mvt'),
- hasMvt = mvt && mvt.hasMvt(),
- showsMvt = hasMvt && mvt.enabled();
-
- var ul = selection
- .selectAll('.layer-list-mvt')
- .data(mvt ? [0] : []);
-
- // Exit
- ul.exit()
- .remove();
-
- // Enter
- var ulEnter = ul.enter()
- .append('ul')
- .attr('class', 'layer-list layer-list-mvt');
-
- var liEnter = ulEnter
- .append('li')
- .attr('class', 'list-item-mvt');
-
- liEnter
- .append('button')
- .attr('class', 'list-item-mvt-extent')
- .call(tooltip()
- .title(t('mvt.zoom'))
- .placement((textDirection === 'rtl') ? 'right' : 'left')
- )
- .on('click', function() {
- d3_event.preventDefault();
- d3_event.stopPropagation();
- mvt.fitZoom();
- })
- .call(svgIcon('#iD-icon-search'));
-
- liEnter
- .append('button')
- .attr('class', 'list-item-mvt-browse')
- .call(tooltip()
- .title(t('mvt.browse'))
- .placement((textDirection === 'rtl') ? 'right' : 'left')
- )
- .on('click', function() {
- d3_select(document.createElement('input'))
- .attr('type', 'file')
- .on('change', function() {
- mvt.files(d3_event.target.files);
- })
- .node().click();
- })
- .call(svgIcon('#iD-icon-geolocate'));
-
- var labelEnter = liEnter
- .append('label')
- .call(tooltip()
- .title(t('mvt.drag_drop'))
- .placement('top')
- );
-
- labelEnter
- .append('input')
- .attr('type', 'checkbox')
- .on('change', function() { toggleLayer('mvt'); });
-
- labelEnter
- .append('span')
- .text(t('mvt.local_layer'));
-
- // Update
- ul = ul
- .merge(ulEnter);
-
- ul.selectAll('.list-item-mvt')
- .classed('active', showsMvt)
- .selectAll('label')
- .classed('deemphasize', !hasMvt)
- .selectAll('input')
- .property('disabled', !hasMvt)
- .property('checked', showsMvt);
- }
function drawListItems(selection, data, type, name, change, active) {
var items = selection.selectAll('li')
@@ -462,8 +381,7 @@ export function uiMapData(context) {
_dataLayerContainer
.call(drawOsmItems)
.call(drawPhotoItems)
- .call(drawGpxItem);
- // .call(drawMvtItem);
+ .call(drawDataItems);
_fillList
.call(drawListItems, fills, 'radio', 'area_fill', setFill, showsFill);
diff --git a/modules/ui/map_in_map.js b/modules/ui/map_in_map.js
index 3e056e4b0..139f41f6c 100644
--- a/modules/ui/map_in_map.js
+++ b/modules/ui/map_in_map.js
@@ -22,7 +22,7 @@ import {
} from '../geo';
import { rendererTileLayer } from '../renderer';
-import { svgDebug, svgGpx } from '../svg';
+import { svgDebug, svgData } from '../svg';
import { utilSetTransform } from '../util';
import { utilGetDimensions } from '../util/dimensions';
@@ -33,7 +33,7 @@ export function uiMapInMap(context) {
var backgroundLayer = rendererTileLayer(context);
var overlayLayers = {};
var projection = geoRawMercator();
- var gpxLayer = svgGpx(projection, context).showLabels(false);
+ var dataLayer = svgData(projection, context).showLabels(false);
var debugLayer = svgDebug(projection, context);
var zoom = d3_zoom()
.scaleExtent([geoZoomToScale(0.5), geoZoomToScale(24)])
@@ -242,7 +242,7 @@ export function uiMapInMap(context) {
.append('svg')
.attr('class', 'map-in-map-data')
.merge(dataLayers)
- .call(gpxLayer)
+ .call(dataLayer)
.call(debugLayer);
diff --git a/test/index.html b/test/index.html
index 6832766ee..abcf0ed83 100644
--- a/test/index.html
+++ b/test/index.html
@@ -115,12 +115,11 @@
-
+
-
@@ -149,4 +148,4 @@