From 4b446e9e780be4b932647ea6ebe4dc700b49ff28 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 17 Aug 2018 13:03:07 -0400 Subject: [PATCH 01/20] WIP merge 'gpx' and 'mvt' layers into single 'data' layer --- ARCHITECTURE.md | 2 +- css/20_map.css | 44 ++------ css/80_app.css | 3 +- modules/renderer/background.js | 23 +--- modules/svg/{gpx.js => data.js} | 177 +++++++++++++++++++++--------- modules/svg/index.js | 3 +- modules/svg/layers.js | 6 +- modules/ui/map_data.js | 120 ++++---------------- modules/ui/map_in_map.js | 6 +- test/index.html | 5 +- test/spec/svg/{gpx.js => data.js} | 36 +++--- test/spec/svg/layers.js | 15 ++- 12 files changed, 192 insertions(+), 248 deletions(-) rename modules/svg/{gpx.js => data.js} (53%) rename test/spec/svg/{gpx.js => data.js} (69%) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index be802579e7..e88be04ccf 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 6ec2927bab..ce45960a92 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 55bf424dca..7749c193c7 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 71325195f0..ff90100045 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 cfd7857428..350d5d76a4 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); + + 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.onload = (function(file) { - var extension = getExtension(file.name); - return function (e) { - parseSaveAndZoom(extension, e.target.result, file.name); - }; - })(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 c7760b2aad..4b1bd37a33 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 31067aea2a..7493cc6953 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 974e1c0bab..d1f86a7b11 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 3e056e4b00..139f41f6cb 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 6832766ee8..abcf0ed831 100644 --- a/test/index.html +++ b/test/index.html @@ -115,12 +115,11 @@ - + - @@ -149,4 +148,4 @@ - \ No newline at end of file + diff --git a/test/spec/svg/gpx.js b/test/spec/svg/data.js similarity index 69% rename from test/spec/svg/gpx.js rename to test/spec/svg/data.js index cb101af22f..fa92be0d50 100644 --- a/test/spec/svg/gpx.js +++ b/test/spec/svg/data.js @@ -1,4 +1,4 @@ -describe('iD.svgGpx', function () { +describe('iD.svgData', function () { var context; var surface; var dispatch = d3.dispatch('change'); @@ -41,19 +41,19 @@ describe('iD.svgGpx', function () { }); - it('creates layer-gpx', function () { - var render = iD.svgGpx(projection, context, dispatch); + it('creates layer-geojson', function () { + var render = iD.svgData(projection, context, dispatch); surface.call(render); - var layers = surface.selectAll('g.layer-gpx').nodes(); + var layers = surface.selectAll('g.layer-geojson').nodes(); expect(layers.length).to.eql(1); }); it('draws geojson', function () { - var render = iD.svgGpx(projection, context, dispatch).geojson(gj); + var render = iD.svgData(projection, context, dispatch).geojson(gj); surface.call(render); - var path = surface.selectAll('path.gpx'); + var path = surface.selectAll('path'); expect(path.nodes().length).to.eql(1); expect(path.attr('d')).to.match(/^M.*z$/); }); @@ -61,30 +61,30 @@ describe('iD.svgGpx', function () { describe('#files', function() { it('handles gpx files', function () { var files = '../../data/gpxtest.gpx'; - var render = iD.svgGpx(projection, context, dispatch).files(files); + var render = iD.svgData(projection, context, dispatch).files(files); surface.call(render); - var path = surface.selectAll('path.gpx'); + var path = surface.selectAll('path'); expect(path.nodes().length).to.eql(1); expect(path.attr('d')).to.match(/^M.*z$/); }); it('handles geojson files', function () { var files = '../../data/gpxtest.json'; - var render = iD.svgGpx(projection, context, dispatch).files(files); + var render = iD.svgData(projection, context, dispatch).files(files); surface.call(render); - var path = surface.selectAll('path.gpx'); + var path = surface.selectAll('path'); expect(path.nodes().length).to.eql(1); expect(path.attr('d')).to.match(/^M.*z$/); }); it('handles kml files', function () { var files = '../../data/gpxtest.kml'; - var render = iD.svgGpx(projection, context, dispatch).files(files); + var render = iD.svgData(projection, context, dispatch).files(files); surface.call(render); - var path = surface.selectAll('path.gpx'); + var path = surface.selectAll('path'); expect(path.nodes().length).to.eql(1); expect(path.attr('d')).to.match(/^M.*z$/); }); @@ -93,25 +93,25 @@ describe('iD.svgGpx', function () { describe('#showLabels', function() { it('shows labels by default', function () { - var render = iD.svgGpx(projection, context, dispatch).geojson(gj); + var render = iD.svgData(projection, context, dispatch).geojson(gj); surface.call(render); - var label = surface.selectAll('text.gpxlabel'); + var label = surface.selectAll('text.label'); expect(label.nodes().length).to.eql(1); expect(label.text()).to.eql('New Jersey'); - var halo = surface.selectAll('text.gpxlabel-halo'); + var halo = surface.selectAll('text.label-halo'); expect(halo.nodes().length).to.eql(1); expect(halo.text()).to.eql('New Jersey'); }); it('hides labels with showLabels(false)', function () { - var render = iD.svgGpx(projection, context, dispatch).geojson(gj).showLabels(false); + var render = iD.svgData(projection, context, dispatch).geojson(gj).showLabels(false); surface.call(render); - expect(surface.selectAll('text.gpxlabel').empty()).to.be.ok; - expect(surface.selectAll('text.gpxlabel-halo').empty()).to.be.ok; + expect(surface.selectAll('text.label').empty()).to.be.ok; + expect(surface.selectAll('text.label-halo').empty()).to.be.ok; }); }); diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index 3feba34725..cc327edf1c 100644 --- a/test/spec/svg/layers.js +++ b/test/spec/svg/layers.js @@ -26,16 +26,15 @@ describe('iD.svgLayers', function () { it('creates default data layers', function () { container.call(iD.svgLayers(projection, context)); var nodes = container.selectAll('svg .data-layer').nodes(); - expect(nodes.length).to.eql(9); + expect(nodes.length).to.eql(8); expect(d3.select(nodes[0]).classed('data-layer-osm')).to.be.true; expect(d3.select(nodes[1]).classed('data-layer-notes')).to.be.true; - expect(d3.select(nodes[2]).classed('data-layer-gpx')).to.be.true; - expect(d3.select(nodes[3]).classed('data-layer-mvt')).to.be.true; - expect(d3.select(nodes[4]).classed('data-layer-streetside')).to.be.true; - expect(d3.select(nodes[5]).classed('data-layer-mapillary-images')).to.be.true; - expect(d3.select(nodes[6]).classed('data-layer-mapillary-signs')).to.be.true; - expect(d3.select(nodes[7]).classed('data-layer-openstreetcam-images')).to.be.true; - expect(d3.select(nodes[8]).classed('data-layer-debug')).to.be.true; + expect(d3.select(nodes[2]).classed('data-layer-data')).to.be.true; + expect(d3.select(nodes[3]).classed('data-layer-streetside')).to.be.true; + expect(d3.select(nodes[4]).classed('data-layer-mapillary-images')).to.be.true; + expect(d3.select(nodes[5]).classed('data-layer-mapillary-signs')).to.be.true; + expect(d3.select(nodes[6]).classed('data-layer-openstreetcam-images')).to.be.true; + expect(d3.select(nodes[7]).classed('data-layer-debug')).to.be.true; }); }); From 0ae4099ff651ec1ca76961d86af662111d76008e Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 17 Aug 2018 17:26:12 -0400 Subject: [PATCH 02/20] Add custom data settings modal with file picker or vector tile url --- css/80_app.css | 20 +++- data/core.yaml | 10 ++ dist/locales/en.json | 13 +++ modules/ui/map_data.js | 69 ++++++++++---- modules/ui/settings/custom_background.js | 9 +- modules/ui/settings/custom_data.js | 113 +++++++++++++++++++++++ modules/ui/settings/index.js | 1 + 7 files changed, 207 insertions(+), 28 deletions(-) create mode 100644 modules/ui/settings/custom_data.js diff --git a/css/80_app.css b/css/80_app.css index 7749c193c7..c33de8c30b 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -3875,16 +3875,26 @@ svg.mouseclick use.right { /* Settings Modals ------------------------------------------------------- */ -.settings-custom-background .instructions { - margin-bottom: 20px; -} -.settings-custom-background textarea { +.settings-modal textarea { height: 60px; } -.settings-custom-background .buttons .button.col3 { +.settings-modal .buttons .button.col3 { float: none; /* undo float left */ } +.settings-custom-background .instructions-template { + margin-bottom: 20px; +} + + +.settings-custom-data .instructions-file { + margin-bottom: 10px; +} +.settings-custom-data .field-file, +.settings-custom-data .instructions-template { + margin-bottom: 20px; +} + /* Save Mode ------------------------------------------------------- */ diff --git a/data/core.yaml b/data/core.yaml index 62285567c2..0faecc8575 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -519,6 +519,16 @@ en: instructions: "Enter a tile URL template. Valid tokens are:\n {zoom} or {z}, {x}, {y} for Z/X/Y tile scheme\n {-y} or {ty} for flipped TMS-style Y coordinates\n {u} for quadtile scheme\n {switch:a,b,c} for DNS server multiplexing\n\nExample:\n{example}" template: placeholder: Enter a url template + custom_data: + tooltip: Edit custom data layer + header: Custom Data Settings + file: + instructions: "Choose a local data file. Supported types are:\n .gpx, .kml, .geojson/.json, .pbf, .mvt" + label: "Browse files" + or: "Or" + template: + instructions: "Enter a vector tile URL template. Valid tokens are:\n {zoom} or {z}, {x}, {y} for Z/X/Y tile scheme" + placeholder: Enter a url template restore: heading: You have unsaved changes description: "Do you wish to restore unsaved changes from a previous editing session?" diff --git a/dist/locales/en.json b/dist/locales/en.json index ca551b5a03..8489e25b19 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -638,6 +638,19 @@ "template": { "placeholder": "Enter a url template" } + }, + "custom_data": { + "tooltip": "Edit custom data layer", + "header": "Custom Data Settings", + "file": { + "instructions": "Choose a local data file. Supported types are:\n .gpx, .kml, .geojson/.json, .pbf, .mvt", + "label": "Browse files" + }, + "or": "Or", + "template": { + "instructions": "Enter a vector tile URL template. Valid tokens are:\n {zoom} or {z}, {x}, {y} for Z/X/Y tile scheme", + "placeholder": "Enter a url template" + } } }, "restore": { diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js index d1f86a7b11..ad4ba47fdc 100644 --- a/modules/ui/map_data.js +++ b/modules/ui/map_data.js @@ -12,6 +12,7 @@ import { modeBrowse } from '../modes'; import { uiBackground } from './background'; import { uiDisclosure } from './disclosure'; import { uiHelp } from './help'; +import { uiSettingsCustomData } from './settings/custom_data'; import { uiTooltipHtml } from './tooltipHtml'; @@ -21,6 +22,9 @@ export function uiMapData(context) { var layers = context.layers(); var fills = ['wireframe', 'partial', 'full']; + var settingsCustomData = uiSettingsCustomData(context) + .on('change', customChanged); + var _fillSelected = context.storage('area-fill') || 'partial'; var _shown = false; var _dataLayerContainer = d3_select(null); @@ -207,7 +211,7 @@ export function uiMapData(context) { } - function drawDataItems(selection) { + function drawCustomDataItems(selection) { var dataLayer = layers.layer('data'); var hasData = dataLayer && dataLayer.hasData(); var showsData = hasData && dataLayer.enabled(); @@ -231,34 +235,42 @@ export function uiMapData(context) { liEnter .append('button') - .attr('class', 'list-item-data-extent') .call(tooltip() - .title(t('gpx.zoom')) + .title(t('settings.custom_data.tooltip')) .placement((textDirection === 'rtl') ? 'right' : 'left') ) - .on('click', function() { - d3_event.preventDefault(); - d3_event.stopPropagation(); - dataLayer.fitZoom(); - }) - .call(svgIcon('#iD-icon-search')); + .on('click', editCustom) + .call(svgIcon('#iD-icon-more')); liEnter .append('button') - .attr('class', 'list-item-data-browse') .call(tooltip() - .title(t('gpx.browse')) + .title(t('gpx.zoom')) .placement((textDirection === 'rtl') ? 'right' : 'left') ) .on('click', function() { - d3_select(document.createElement('input')) - .attr('type', 'file') - .on('change', function() { - dataLayer.files(d3_event.target.files); - }) - .node().click(); + d3_event.preventDefault(); + d3_event.stopPropagation(); + dataLayer.fitZoom(); }) - .call(svgIcon('#iD-icon-geolocate')); + .call(svgIcon('#iD-icon-search')); + + // liEnter + // .append('button') + // .attr('class', 'list-item-data-browse') + // .call(tooltip() + // .title(t('gpx.browse')) + // .placement((textDirection === 'rtl') ? 'right' : 'left') + // ) + // .on('click', function() { + // d3_select(document.createElement('input')) + // .attr('type', 'file') + // .on('change', function() { + // dataLayer.files(d3_event.target.files); + // }) + // .node().click(); + // }) + // .call(svgIcon('#iD-icon-geolocate')); var labelEnter = liEnter .append('label') @@ -290,6 +302,25 @@ export function uiMapData(context) { } + function editCustom() { + d3_event.preventDefault(); + context.container() + .call(settingsCustomData); + } + + + function customChanged(d) { +console.log('custom was changed'); + // if (d && d.template) { + // _customSource.template(d.template); + // chooseBackground(_customSource); + // } else { + // _customSource.template(''); + // chooseBackground(context.background().findSource('none')); + // } + } + + function drawListItems(selection, data, type, name, change, active) { var items = selection.selectAll('li') .data(data); @@ -381,7 +412,7 @@ export function uiMapData(context) { _dataLayerContainer .call(drawOsmItems) .call(drawPhotoItems) - .call(drawDataItems); + .call(drawCustomDataItems); _fillList .call(drawListItems, fills, 'radio', 'area_fill', setFill, showsFill); diff --git a/modules/ui/settings/custom_background.js b/modules/ui/settings/custom_background.js index e691760531..9b2fc4e05a 100644 --- a/modules/ui/settings/custom_background.js +++ b/modules/ui/settings/custom_background.js @@ -19,7 +19,7 @@ export function uiSettingsCustomBackground(context) { var modal = uiConfirm(selection).okButton(); modal - .classed('settings-custom-background', true); + .classed('settings-modal settings-custom-background', true); modal.select('.modal-section.header') .append('h3') @@ -30,11 +30,12 @@ export function uiSettingsCustomBackground(context) { textSection .append('pre') - .attr('class', 'instructions') + .attr('class', 'instructions-template') .text(t('settings.custom_background.instructions', { example: example })); textSection .append('textarea') + .attr('class', 'field-template') .attr('placeholder', t('settings.custom_background.template.placeholder')) .call(utilNoAuto) .property('value', _currSettings.template); @@ -66,7 +67,7 @@ export function uiSettingsCustomBackground(context) { // restore the original template function clickCancel() { - textSection.select('textarea').property('value', _origSettings.template); + textSection.select('.field-template').property('value', _origSettings.template); context.storage('background-custom-template', _origSettings.template); this.blur(); modal.close(); @@ -74,7 +75,7 @@ export function uiSettingsCustomBackground(context) { // accept the current template function clickSave() { - _currSettings.template = textSection.select('textarea').property('value'); + _currSettings.template = textSection.select('.field-template').property('value'); context.storage('background-custom-template', _currSettings.template); this.blur(); modal.close(); diff --git a/modules/ui/settings/custom_data.js b/modules/ui/settings/custom_data.js new file mode 100644 index 0000000000..26c7039c20 --- /dev/null +++ b/modules/ui/settings/custom_data.js @@ -0,0 +1,113 @@ +import _cloneDeep from 'lodash-es/cloneDeep'; + +import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { event as d3_event } from 'd3-selection'; + +import { t } from '../../util/locale'; +import { uiConfirm } from '../confirm'; +import { utilNoAuto, utilRebind } from '../../util'; + + +export function uiSettingsCustomData(context) { + var dispatch = d3_dispatch('change'); + + function render(selection) { + var _origSettings = { + file: context.storage('settings-custom-data-file'), + template: context.storage('settings-custom-data-template') + }; + var _currSettings = _cloneDeep(_origSettings); + // var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png'; + var modal = uiConfirm(selection).okButton(); + + modal + .classed('settings-modal settings-custom-data', true); + + modal.select('.modal-section.header') + .append('h3') + .text(t('settings.custom_data.header')); + + + var textSection = modal.select('.modal-section.message-text'); + + textSection + .append('pre') + .attr('class', 'instructions-file') + .text(t('settings.custom_data.file.instructions')); + + textSection + .append('input') + .attr('class', 'field-file') + .attr('type', 'file') + .on('change', function() { + var files = d3_event.target.files; + if (files && files.length) { + _currSettings.file = files[0]; + } else { + _currSettings.file = undefined; + } + }); + + textSection + .append('h4') + .text(t('settings.custom_data.or')); + + textSection + .append('pre') + .attr('class', 'instructions-template') + .text(t('settings.custom_data.template.instructions')); + + textSection + .append('textarea') + .attr('class', 'field-template') + .attr('placeholder', t('settings.custom_data.template.placeholder')) + .call(utilNoAuto) + .property('value', _currSettings.template); + + + // insert a cancel button, and adjust the button widths + var buttonSection = modal.select('.modal-section.buttons'); + + buttonSection + .insert('button', '.ok-button') + .attr('class', 'button col3 cancel-button secondary-action') + .text(t('confirm.cancel')); + + + buttonSection.select('.cancel-button') + .on('click.cancel', clickCancel); + + buttonSection.select('.ok-button') + .classed('col3', true) + .classed('col4', false) + .attr('disabled', isSaveDisabled) + .on('click.save', clickSave); + + + function isSaveDisabled() { + return null; + } + + + // restore the original template + function clickCancel() { + textSection.select('.field-template').property('value', _origSettings.template); + context.storage('settings-custom-data-template', _origSettings.template); + context.storage('settings-custom-data-file', _origSettings.file); + this.blur(); + modal.close(); + } + + // accept the current template + function clickSave() { + _currSettings.template = textSection.select('.field-template').property('value'); + context.storage('settings-custom-data-template', _currSettings.template); + context.storage('settings-custom-data-file', _currSettings.file); + this.blur(); + modal.close(); + dispatch.call('change', this, _currSettings); + } + } + + return utilRebind(render, dispatch, 'on'); +} diff --git a/modules/ui/settings/index.js b/modules/ui/settings/index.js index 52579d5d5a..b8a047b261 100644 --- a/modules/ui/settings/index.js +++ b/modules/ui/settings/index.js @@ -1 +1,2 @@ export { uiSettingsCustomBackground } from './custom_background'; +export { uiSettingsCustomData } from './custom_data'; From 4cfd5b0078f0b170c7fd25c6341c090af69d8093 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 18 Aug 2018 08:33:43 -0400 Subject: [PATCH 03/20] Update custom data text strings --- data/core.yaml | 14 ++++---------- dist/locales/en.json | 17 +++++------------ modules/ui/map_data.js | 23 +++-------------------- 3 files changed, 12 insertions(+), 42 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 0faecc8575..5d68d03404 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -460,6 +460,10 @@ en: notes: tooltip: Note data from OpenStreetMap title: OpenStreetMap notes + custom: + tooltip: "Drag and drop a data file onto the page, or click the button to setup" + title: Custom Map Data + zoom: Zoom to data fill_area: Fill Areas map_features: Map Features autohidden: "These features have been automatically hidden because too many would be shown on the screen. You can zoom in to edit them." @@ -620,16 +624,6 @@ en: out: Zoom out cannot_zoom: "Cannot zoom out further in current mode." full_screen: Toggle Full Screen - gpx: - local_layer: "Add a GPX" - drag_drop: "Drag and drop a .gpx, .geojson or .kml file on the page, or click the button to the right to browse" - zoom: "Zoom to layer" - browse: "Browse for a file" - mvt: - local_layer: "Add a MVT" - drag_drop: "Drag and drop a .mvt or .pbf file on the page, or click the button to the right to browse" - zoom: "Zoom to layer" - browse: "Browse for a file" streetside: tooltip: "Streetside photos from Microsoft" title: "Photo Overlay (Bing Streetside)" diff --git a/dist/locales/en.json b/dist/locales/en.json index 8489e25b19..0e247edf19 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -558,6 +558,11 @@ "notes": { "tooltip": "Note data from OpenStreetMap", "title": "OpenStreetMap notes" + }, + "custom": { + "tooltip": "Drag and drop a data file onto the page, or click the button to setup", + "title": "Custom Map Data", + "zoom": "Zoom to data" } }, "fill_area": "Fill Areas", @@ -754,18 +759,6 @@ }, "cannot_zoom": "Cannot zoom out further in current mode.", "full_screen": "Toggle Full Screen", - "gpx": { - "local_layer": "Add a GPX", - "drag_drop": "Drag and drop a .gpx, .geojson or .kml file on the page, or click the button to the right to browse", - "zoom": "Zoom to layer", - "browse": "Browse for a file" - }, - "mvt": { - "local_layer": "Add a MVT", - "drag_drop": "Drag and drop a .mvt or .pbf file on the page, or click the button to the right to browse", - "zoom": "Zoom to layer", - "browse": "Browse for a file" - }, "streetside": { "tooltip": "Streetside photos from Microsoft", "title": "Photo Overlay (Bing Streetside)", diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js index ad4ba47fdc..0f8ff4e9c6 100644 --- a/modules/ui/map_data.js +++ b/modules/ui/map_data.js @@ -245,7 +245,7 @@ export function uiMapData(context) { liEnter .append('button') .call(tooltip() - .title(t('gpx.zoom')) + .title(t('map_data.layers.custom.zoom')) .placement((textDirection === 'rtl') ? 'right' : 'left') ) .on('click', function() { @@ -255,27 +255,10 @@ export function uiMapData(context) { }) .call(svgIcon('#iD-icon-search')); - // liEnter - // .append('button') - // .attr('class', 'list-item-data-browse') - // .call(tooltip() - // .title(t('gpx.browse')) - // .placement((textDirection === 'rtl') ? 'right' : 'left') - // ) - // .on('click', function() { - // d3_select(document.createElement('input')) - // .attr('type', 'file') - // .on('change', function() { - // dataLayer.files(d3_event.target.files); - // }) - // .node().click(); - // }) - // .call(svgIcon('#iD-icon-geolocate')); - var labelEnter = liEnter .append('label') .call(tooltip() - .title(t('gpx.drag_drop')) + .title(t('map_data.layers.custom.tooltip')) .placement('top') ); @@ -286,7 +269,7 @@ export function uiMapData(context) { labelEnter .append('span') - .text(t('gpx.local_layer')); + .text(t('map_data.layers.custom.title')); // Update ul = ul From 9802b2f8317ce3e3986a4b4a002b6a5774720340 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 18 Aug 2018 10:57:26 -0400 Subject: [PATCH 04/20] Restore 'custom' and 'none' options to background imagery list (closes #5226) --- modules/renderer/background.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/modules/renderer/background.js b/modules/renderer/background.js index ff90100045..b227784ca3 100644 --- a/modules/renderer/background.js +++ b/modules/renderer/background.js @@ -211,7 +211,7 @@ export function rendererBackground(context) { matchImagery.forEach(function(d) { matchIDs[d.id] = true; }); return _backgroundSources.filter(function(source) { - return matchIDs[source.id]; + return matchIDs[source.id] || !source.polygon; // no polygon = worldwide }); }; @@ -378,20 +378,18 @@ export function rendererBackground(context) { data.imagery.features = {}; // build efficient index and querying for data.imagery - var world = [[[-180, -90], [-180, 90], [180, 90], [180, -90], [-180, -90]]]; var features = data.imagery.map(function(source) { + if (!source.polygon) return null; var feature = { type: 'Feature', - id: source.id, - properties: _omit(source, ['polygon']), - geometry: { - type: 'MultiPolygon', - coordinates: [ source.polygon || world ] - } + properties: { id: source.id }, + geometry: { type: 'MultiPolygon', coordinates: [ source.polygon ] } }; + data.imagery.features[source.id] = feature; return feature; - }); + }).filter(Boolean); + data.imagery.query = whichPolygon({ type: 'FeatureCollection', features: features From 0840b8dff90576147bb2c324f9b5d15afafd9261 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 19 Aug 2018 00:31:08 -0400 Subject: [PATCH 05/20] More work on custom map data --- data/core.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core.yaml b/data/core.yaml index 5d68d03404..eaa11519b2 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -525,7 +525,7 @@ en: placeholder: Enter a url template 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" From 9009d55fd1023111d9e90c81610b5e7e26c1764c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 20 Aug 2018 21:54:00 -0400 Subject: [PATCH 06/20] Add a vector tile service --- dist/locales/en.json | 2 +- modules/renderer/background.js | 1 - modules/services/index.js | 3 + modules/services/vector_tile.js | 147 ++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 modules/services/vector_tile.js diff --git a/dist/locales/en.json b/dist/locales/en.json index 0e247edf19..97b5a16936 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 b227784ca3..2754c10010 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 789628ed5a..83cc8114bf 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 0000000000..03c1dbd428 --- /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; + } + +}; From 575c98ab282d1d0c6a201170122815fc95e19500 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 21 Aug 2018 01:25:24 -0400 Subject: [PATCH 07/20] Accept both file and vector tile url templates from settings screen --- css/20_map.css | 12 +- modules/svg/data.js | 186 +++++++++++++++++++++-------- modules/ui/map_data.js | 15 ++- modules/ui/settings/custom_data.js | 20 +++- test/spec/svg/data.js | 4 +- 5 files changed, 163 insertions(+), 74 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index ce45960a92..79c1275553 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -309,28 +309,28 @@ g.turn circle { /* Other Data (gpx, kml, geojson, mvt, pbf) */ -.layer-geojson { +.layer-mapdata { pointer-events: none; } -.layer-geojson path { +.layer-mapdata path { stroke: #ff26d4; stroke-width: 2; fill: none; } -.layer-geojson text.label-halo, -.layer-geojson text.label { +.layer-mapdata text.label-halo, +.layer-mapdata text.label { font-size: 10px; font-weight: bold; dominant-baseline: middle; } -.layer-geojson text.label { +.layer-mapdata text.label { fill: #ff26d4; } -.layer-geojson text.label-halo { +.layer-mapdata text.label-halo { opacity: 0.7; stroke: #000; stroke-width: 5px; diff --git a/modules/svg/data.js b/modules/svg/data.js index 350d5d76a4..f773ba9d0f 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -2,6 +2,7 @@ import _flatten from 'lodash-es/flatten'; import _isEmpty from 'lodash-es/isEmpty'; import _reduce from 'lodash-es/reduce'; import _union from 'lodash-es/union'; +import _throttle from 'lodash-es/throttle'; import { geoBounds as d3_geoBounds } from 'd3-geo'; @@ -20,6 +21,7 @@ import vt from '@mapbox/vector-tile'; import Protobuf from 'pbf'; import { geoExtent, geoPolygonIntersectsPolygon } from '../geo'; +import { services } from '../services'; import { svgPath } from './index'; import { utilDetect } from '../util/detect'; @@ -30,9 +32,13 @@ var _geojson; export function svgData(projection, context, dispatch) { + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); var _showLabels = true; var detected = utilDetect(); - var layer; + var layer = d3_select(null); + var _vtService; + var _fileList; + var _template; // todo, if template is set, use vectorTile service var _src; @@ -54,7 +60,7 @@ export function svgData(projection, context, dispatch) { d3_event.stopPropagation(); d3_event.preventDefault(); if (!detected.filedrop) return; - drawData.files(d3_event.dataTransfer.files); + drawData.fileList(d3_event.dataTransfer.files); }) .on('dragenter.svgData', over) .on('dragexit.svgData', over) @@ -64,24 +70,71 @@ export function svgData(projection, context, dispatch) { } + function getService() { + if (services.vectorTile && !_vtService) { + _vtService = services.vectorTile; + _vtService.event.on('loadedData', throttledRedraw); + } else if (!services.vectorTile && _vtService) { + _vtService = null; + } + + return _vtService; + } + + + function showLayer() { + layerOn(); + + layer + .style('opacity', 0) + .transition() + .duration(250) + .style('opacity', 1) + .on('end', function () { dispatch.call('change'); }); + } + + + function hideLayer() { + throttledRedraw.cancel(); + + layer + .transition() + .duration(250) + .style('opacity', 0) + .on('end', layerOff); + } + + + function layerOn() { + layer.style('display', 'block'); + } + + + function layerOff() { + layer.selectAll('.viewfield-group').remove(); + layer.style('display', 'none'); + } + + function drawData(selection) { var getPath = svgPath(projection).geojson; + var hasData = drawData.hasData(); - layer = selection.selectAll('.layer-geojson') - .data(_enabled ? [0] : []); + layer = selection.selectAll('.layer-mapdata') + .data(_enabled && hasData ? [0] : []); layer.exit() .remove(); layer = layer.enter() .append('g') - .attr('class', 'layer-geojson') + .attr('class', 'layer-mapdata') .merge(layer); var paths = layer .selectAll('path') - .data([_geojson]); + .data(hasData ? [_geojson] : []); paths.exit() .remove(); @@ -95,7 +148,7 @@ export function svgData(projection, context, dispatch) { .attr('d', getPath); - var labelData = _showLabels && _geojson.features ? _geojson.features : []; + var labelData = (_showLabels && hasData && _geojson.features) || []; labelData = labelData.filter(getPath); layer @@ -175,26 +228,34 @@ export function svgData(projection, context, dispatch) { } - function parseSaveAndZoom(extension, data, name) { + drawData.setFile = function(extension, data, src) { + var gj; switch (extension) { case '.gpx': - drawData.geojson(toGeoJSON.gpx(toDom(data)), name).fitZoom(); + gj = toGeoJSON.gpx(toDom(data)); break; case '.kml': - drawData.geojson(toGeoJSON.kml(toDom(data)), name).fitZoom(); + gj = toGeoJSON.kml(toDom(data)); break; case '.pbf': - drawData.geojson(vtToGeoJSON(data), name).fitZoom(); + gj = vtToGeoJSON(data); break; case '.mvt': - drawData.geojson(vtToGeoJSON(data), name).fitZoom(); + gj = vtToGeoJSON(data); break; case '.geojson': case '.json': - drawData.geojson(JSON.parse(data), name).fitZoom(); + gj = JSON.parse(data); break; } - } + + if (!gj || _isEmpty(gj) || _isEmpty(gj.features)) return; + _geojson = gj; + _src = src || 'unknown.geojson'; + + dispatch.call('change'); + return this.fitZoom(); + }; drawData.showLabels = function(val) { @@ -207,6 +268,12 @@ export function svgData(projection, context, dispatch) { drawData.enabled = function(val) { if (!arguments.length) return _enabled; _enabled = val; + if (_enabled) { + showLayer(); + } else { + hideLayer(); + } + dispatch.call('change'); return this; }; @@ -217,47 +284,41 @@ export function svgData(projection, context, dispatch) { }; + drawData.template = function(val) { + if (!arguments.length) return _template; + + _template = val; + _fileList = null; + _geojson = null; + _src = 'vector tiles'; + + dispatch.call('change'); + return this; + }; + + drawData.geojson = function(gj, src) { if (!arguments.length) return _geojson; - if (_isEmpty(gj) || _isEmpty(gj.features)) return this; + + _template = null; + _fileList = null; _geojson = gj; _src = src || 'unknown.geojson'; + dispatch.call('change'); return this; }; - 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; - }; + drawData.fileList = function(fileList) { + if (!arguments.length) return _fileList; + _template = null; + _fileList = fileList; + _geojson = null; + _src = null; - drawData.files = function(fileList) { - if (!fileList.length) return this; + if (!fileList || !fileList.length) return this; var f = fileList[0]; var reader = new FileReader(); var extension = getExtension(f.name); @@ -268,15 +329,10 @@ export function svgData(projection, context, dispatch) { var data = []; var zxy = [0,0,0]; - _src = file.name; - var extension = getExtension(file.name); - var bufferdata = { - data: data, - zxy: zxy - }; + var bufferdata = { data: data, zxy: zxy }; return function (e) { bufferdata.data = e.target.result; - parseSaveAndZoom(extension, bufferdata); + drawData.setFile(extension, bufferdata, file.name); }; })(f); @@ -285,7 +341,7 @@ export function svgData(projection, context, dispatch) { } else { reader.onload = (function(file) { return function (e) { - parseSaveAndZoom(extension, e.target.result, file.name); + drawData.setFile(extension, e.target.result, file.name); }; })(f); @@ -296,6 +352,32 @@ export function svgData(projection, context, dispatch) { }; + 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 }; + drawData.setFile(extension, bufferdata, url); + }); + } else { + d3_text(url, function(err, data) { + if (!err) { + drawData.setFile(extension, data, url); + } + }); + } + + return this; + }; + + drawData.getSrc = function() { return _src; }; diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js index 0f8ff4e9c6..8eec067ffa 100644 --- a/modules/ui/map_data.js +++ b/modules/ui/map_data.js @@ -293,14 +293,13 @@ export function uiMapData(context) { function customChanged(d) { -console.log('custom was changed'); - // if (d && d.template) { - // _customSource.template(d.template); - // chooseBackground(_customSource); - // } else { - // _customSource.template(''); - // chooseBackground(context.background().findSource('none')); - // } + var dataLayer = layers.layer('data'); + + if (d && d.template) { + dataLayer.template(d.template); + } else if (d && d.fileList) { + dataLayer.fileList(d.fileList); + } } diff --git a/modules/ui/settings/custom_data.js b/modules/ui/settings/custom_data.js index 26c7039c20..ffa454588c 100644 --- a/modules/ui/settings/custom_data.js +++ b/modules/ui/settings/custom_data.js @@ -12,11 +12,13 @@ export function uiSettingsCustomData(context) { var dispatch = d3_dispatch('change'); function render(selection) { + var dataLayer = context.layers().layer('data'); var _origSettings = { - file: context.storage('settings-custom-data-file'), + fileList: (dataLayer && dataLayer.fileList()) || null, template: context.storage('settings-custom-data-template') }; var _currSettings = _cloneDeep(_origSettings); + // var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png'; var modal = uiConfirm(selection).okButton(); @@ -39,12 +41,15 @@ export function uiSettingsCustomData(context) { .append('input') .attr('class', 'field-file') .attr('type', 'file') + .property('files', _currSettings.fileList) // works for all except IE11 .on('change', function() { var files = d3_event.target.files; if (files && files.length) { - _currSettings.file = files[0]; + _currSettings.template = ''; + textSection.select('.field-template').property('value', ''); + _currSettings.fileList = files; } else { - _currSettings.file = undefined; + _currSettings.fileList = null; } }); @@ -93,16 +98,19 @@ export function uiSettingsCustomData(context) { function clickCancel() { textSection.select('.field-template').property('value', _origSettings.template); context.storage('settings-custom-data-template', _origSettings.template); - context.storage('settings-custom-data-file', _origSettings.file); this.blur(); modal.close(); } // accept the current template function clickSave() { - _currSettings.template = textSection.select('.field-template').property('value'); + _currSettings.template = textSection.select('.field-template').property('value').trim(); + + // one or the other but not both + if (_currSettings.template) { _currSettings.fileList = null; } + if (_currSettings.fileList) { _currSettings.template = ''; } + context.storage('settings-custom-data-template', _currSettings.template); - context.storage('settings-custom-data-file', _currSettings.file); this.blur(); modal.close(); dispatch.call('change', this, _currSettings); diff --git a/test/spec/svg/data.js b/test/spec/svg/data.js index fa92be0d50..8068fc18bc 100644 --- a/test/spec/svg/data.js +++ b/test/spec/svg/data.js @@ -41,11 +41,11 @@ describe('iD.svgData', function () { }); - it('creates layer-geojson', function () { + it('creates layer-mapdata', function () { var render = iD.svgData(projection, context, dispatch); surface.call(render); - var layers = surface.selectAll('g.layer-geojson').nodes(); + var layers = surface.selectAll('g.layer-mapdata').nodes(); expect(layers.length).to.eql(1); }); From 3eb4d919871b47a1e9f4f6bf62c4ad0dba20e2c0 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 21 Aug 2018 18:44:05 -0400 Subject: [PATCH 08/20] Now supports fetching data from vector tile url --- data/core.yaml | 8 +- dist/locales/en.json | 8 +- modules/services/vector_tile.js | 11 +- modules/svg/data.js | 165 ++++++++++++----------------- modules/ui/map_data.js | 4 +- modules/ui/settings/custom_data.js | 32 +++--- 6 files changed, 104 insertions(+), 124 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index eaa11519b2..5472f81d16 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -527,12 +527,12 @@ en: tooltip: Edit custom data layer header: Custom Map Data Settings file: - instructions: "Choose a local data file. Supported types are:\n .gpx, .kml, .geojson/.json, .pbf, .mvt" + instructions: "Choose a local data file. Supported types are:\n .gpx, .kml, .geojson, .json" label: "Browse files" or: "Or" - template: - instructions: "Enter a vector tile URL template. Valid tokens are:\n {zoom} or {z}, {x}, {y} for Z/X/Y tile scheme" - placeholder: Enter a url template + url: + instructions: "Enter a data file URL or vector tile URL template. Valid tokens are:\n {zoom} or {z}, {x}, {y} for Z/X/Y tile scheme" + placeholder: Enter a url restore: heading: You have unsaved changes description: "Do you wish to restore unsaved changes from a previous editing session?" diff --git a/dist/locales/en.json b/dist/locales/en.json index 97b5a16936..516b67eb9a 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -648,13 +648,13 @@ "tooltip": "Edit custom data layer", "header": "Custom Map Data Settings", "file": { - "instructions": "Choose a local data file. Supported types are:\n .gpx, .kml, .geojson/.json, .pbf, .mvt", + "instructions": "Choose a local data file. Supported types are:\n .gpx, .kml, .geojson, .json", "label": "Browse files" }, "or": "Or", - "template": { - "instructions": "Enter a vector tile URL template. Valid tokens are:\n {zoom} or {z}, {x}, {y} for Z/X/Y tile scheme", - "placeholder": "Enter a url template" + "url": { + "instructions": "Enter a data file URL or vector tile URL template. Valid tokens are:\n {zoom} or {z}, {x}, {y} for Z/X/Y tile scheme", + "placeholder": "Enter a url" } } }, diff --git a/modules/services/vector_tile.js b/modules/services/vector_tile.js index 03c1dbd428..4c0556d5ea 100644 --- a/modules/services/vector_tile.js +++ b/modules/services/vector_tile.js @@ -31,7 +31,7 @@ function vtToGeoJSON(bufferdata) { 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]); + var feature = layer.feature(i).toGeoJSON(bufferdata.xyz[0], bufferdata.xyz[1], bufferdata.xyz[2]); if (layers.length > 1) feature.properties.vt_layer = layerID; collection.features.push(feature); } @@ -64,9 +64,14 @@ function loadTile(source, tile) { delete source.inflight[tile.id]; if (err || !data) return; + var bufferdata = { + data: data, + xyz: tile.xyz + }; + source.loaded[tile.id] = { - bufferdata: data, - geojson: vtToGeoJSON(data) + bufferdata: bufferdata, + geojson: vtToGeoJSON(bufferdata) }; dispatch.call('loadedData'); diff --git a/modules/svg/data.js b/modules/svg/data.js index f773ba9d0f..1cf4061fd7 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -5,11 +5,7 @@ import _union from 'lodash-es/union'; import _throttle from 'lodash-es/throttle'; import { geoBounds as d3_geoBounds } from 'd3-geo'; - -import { - request as d3_request, - text as d3_text -} from 'd3-request'; +import { text as d3_text } from 'd3-request'; import { event as d3_event, @@ -17,8 +13,6 @@ import { } from 'd3-selection'; import toGeoJSON from '@mapbox/togeojson'; -import vt from '@mapbox/vector-tile'; -import Protobuf from 'pbf'; import { geoExtent, geoPolygonIntersectsPolygon } from '../geo'; import { services } from '../services'; @@ -38,7 +32,7 @@ export function svgData(projection, context, dispatch) { var layer = d3_select(null); var _vtService; var _fileList; - var _template; // todo, if template is set, use vectorTile service + var _template; var _src; @@ -117,6 +111,7 @@ export function svgData(projection, context, dispatch) { function drawData(selection) { + var vtService = getService(); var getPath = svgPath(projection).geojson; var hasData = drawData.hasData(); @@ -132,9 +127,18 @@ export function svgData(projection, context, dispatch) { .merge(layer); + var geoData; + if (_template && vtService) { // fetch data from vector tile service + var sourceID = _template; + vtService.loadTiles(sourceID, _template, projection); + geoData = vtService.data(sourceID, projection); + } else { + geoData = _geojson ? [_geojson] : []; + } + var paths = layer .selectAll('path') - .data(hasData ? [_geojson] : []); + .data(geoData); paths.exit() .remove(); @@ -148,8 +152,17 @@ export function svgData(projection, context, dispatch) { .attr('d', getPath); - var labelData = (_showLabels && hasData && _geojson.features) || []; - labelData = labelData.filter(getPath); + var labelData = []; + if (_showLabels) { + geoData.forEach(function(f) { + if (f.type === 'FeatureCollection') { + labelData = labelData.concat(f.features); + } else { + labelData.push(f); + } + }); + labelData = labelData.filter(getPath); + } layer .call(drawLabels, 'label-halo', labelData) @@ -188,60 +201,33 @@ export function svgData(projection, context, dispatch) { function getExtension(fileName) { - if (fileName === undefined) { - return ''; - } + if (!fileName) return; var lastDotIndex = fileName.lastIndexOf('.'); - if (lastDotIndex < 0) { - return ''; - } + if (lastDotIndex < 0) return; return fileName.substr(lastDotIndex); } - function toDom(textdata) { + function xmlToDom(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; - } - - drawData.setFile = function(extension, data, src) { + _template = null; + _fileList = null; + _geojson = null; + _src = null; + var gj; switch (extension) { case '.gpx': - gj = toGeoJSON.gpx(toDom(data)); + gj = toGeoJSON.gpx(xmlToDom(data)); break; case '.kml': - gj = toGeoJSON.kml(toDom(data)); - break; - case '.pbf': - gj = vtToGeoJSON(data); - break; - case '.mvt': - gj = vtToGeoJSON(data); + gj = toGeoJSON.kml(xmlToDom(data)); break; case '.geojson': case '.json': @@ -249,17 +235,19 @@ export function svgData(projection, context, dispatch) { break; } - if (!gj || _isEmpty(gj) || _isEmpty(gj.features)) return; - _geojson = gj; - _src = src || 'unknown.geojson'; + if (!_isEmpty(gj)) { + _geojson = gj; + _src = src || 'unknown.geojson'; + return this.fitZoom(); + } dispatch.call('change'); - return this.fitZoom(); }; drawData.showLabels = function(val) { if (!arguments.length) return _showLabels; + _showLabels = val; return this; }; @@ -267,6 +255,7 @@ export function svgData(projection, context, dispatch) { drawData.enabled = function(val) { if (!arguments.length) return _enabled; + _enabled = val; if (_enabled) { showLayer(); @@ -280,7 +269,7 @@ export function svgData(projection, context, dispatch) { drawData.hasData = function() { - return (!(_isEmpty(_geojson) || _isEmpty(_geojson.features))); + return !!(_template || _geojson); }; @@ -302,8 +291,13 @@ export function svgData(projection, context, dispatch) { _template = null; _fileList = null; - _geojson = gj; - _src = src || 'unknown.geojson'; + _geojson = null; + _src = null; + + if (!_isEmpty(gj)) { + _geojson = gj; + _src = src || 'unknown.geojson'; + } dispatch.call('change'); return this; @@ -320,58 +314,38 @@ export function svgData(projection, context, dispatch) { if (!fileList || !fileList.length) return this; var f = fileList[0]; - var reader = new FileReader(); var extension = getExtension(f.name); + var reader = new FileReader(); + reader.onload = (function(file) { + return function(e) { + drawData.setFile(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]; - - var bufferdata = { data: data, zxy: zxy }; - return function (e) { - bufferdata.data = e.target.result; - drawData.setFile(extension, bufferdata, file.name); - }; - })(f); - - reader.readAsArrayBuffer(f); - - } else { - reader.onload = (function(file) { - return function (e) { - drawData.setFile(extension, e.target.result, file.name); - }; - })(f); - - reader.readAsText(f); - } + reader.readAsText(f); return this; }; drawData.url = function(url) { + _template = null; + _fileList = null; + _geojson = null; + _src = null; + 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 }; - drawData.setFile(extension, bufferdata, url); - }); - } else { + var re = /\.(gpx|kml|(geo)?json)$/i; + if (re.test(extension)) { + _template = null; d3_text(url, function(err, data) { if (!err) { drawData.setFile(extension, data, url); } }); + + } else { + drawData.template(url); } return this; @@ -379,12 +353,13 @@ export function svgData(projection, context, dispatch) { drawData.getSrc = function() { - return _src; + return _src || ''; }; drawData.fitZoom = function() { - if (!this.hasData()) return this; + // note: only works on a FeatureCollection + if (_isEmpty(_geojson) || _isEmpty(_geojson.features)) return; var map = context.map(); var viewport = map.trimmedExtent().polygon(); diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js index 8eec067ffa..d76cc1c4df 100644 --- a/modules/ui/map_data.js +++ b/modules/ui/map_data.js @@ -295,8 +295,8 @@ export function uiMapData(context) { function customChanged(d) { var dataLayer = layers.layer('data'); - if (d && d.template) { - dataLayer.template(d.template); + if (d && d.url) { + dataLayer.url(d.url); } else if (d && d.fileList) { dataLayer.fileList(d.fileList); } diff --git a/modules/ui/settings/custom_data.js b/modules/ui/settings/custom_data.js index ffa454588c..090a9ec42f 100644 --- a/modules/ui/settings/custom_data.js +++ b/modules/ui/settings/custom_data.js @@ -15,7 +15,7 @@ export function uiSettingsCustomData(context) { var dataLayer = context.layers().layer('data'); var _origSettings = { fileList: (dataLayer && dataLayer.fileList()) || null, - template: context.storage('settings-custom-data-template') + url: context.storage('settings-custom-data-url') }; var _currSettings = _cloneDeep(_origSettings); @@ -45,8 +45,8 @@ export function uiSettingsCustomData(context) { .on('change', function() { var files = d3_event.target.files; if (files && files.length) { - _currSettings.template = ''; - textSection.select('.field-template').property('value', ''); + _currSettings.url = ''; + textSection.select('.field-url').property('value', ''); _currSettings.fileList = files; } else { _currSettings.fileList = null; @@ -59,15 +59,15 @@ export function uiSettingsCustomData(context) { textSection .append('pre') - .attr('class', 'instructions-template') - .text(t('settings.custom_data.template.instructions')); + .attr('class', 'instructions-url') + .text(t('settings.custom_data.url.instructions')); textSection .append('textarea') - .attr('class', 'field-template') - .attr('placeholder', t('settings.custom_data.template.placeholder')) + .attr('class', 'field-url') + .attr('placeholder', t('settings.custom_data.url.placeholder')) .call(utilNoAuto) - .property('value', _currSettings.template); + .property('value', _currSettings.url); // insert a cancel button, and adjust the button widths @@ -94,23 +94,23 @@ export function uiSettingsCustomData(context) { } - // restore the original template + // restore the original url function clickCancel() { - textSection.select('.field-template').property('value', _origSettings.template); - context.storage('settings-custom-data-template', _origSettings.template); + textSection.select('.field-url').property('value', _origSettings.url); + context.storage('settings-custom-data-url', _origSettings.url); this.blur(); modal.close(); } - // accept the current template + // accept the current url function clickSave() { - _currSettings.template = textSection.select('.field-template').property('value').trim(); + _currSettings.url = textSection.select('.field-url').property('value').trim(); // one or the other but not both - if (_currSettings.template) { _currSettings.fileList = null; } - if (_currSettings.fileList) { _currSettings.template = ''; } + if (_currSettings.url) { _currSettings.fileList = null; } + if (_currSettings.fileList) { _currSettings.url = ''; } - context.storage('settings-custom-data-template', _currSettings.template); + context.storage('settings-custom-data-url', _currSettings.url); this.blur(); modal.close(); dispatch.call('change', this, _currSettings); From 80b583a6f0e77dcd2033307b02aaa903af4ff88f Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 22 Aug 2018 23:16:31 -0400 Subject: [PATCH 09/20] WIP on feature deduplication across tile boundaries It seems like the ids stored in the features are not reliable, so I'm trying to generate ids --- modules/services/vector_tile.js | 59 +++++++++++--------- modules/svg/data.js | 97 ++++++++++++++++++++++----------- modules/svg/helpers.js | 12 +++- modules/util/index.js | 3 +- modules/util/util.js | 16 ++++++ 5 files changed, 128 insertions(+), 59 deletions(-) diff --git a/modules/services/vector_tile.js b/modules/services/vector_tile.js index 4c0556d5ea..e6de67d877 100644 --- a/modules/services/vector_tile.js +++ b/modules/services/vector_tile.js @@ -7,7 +7,7 @@ import { request as d3_request } from 'd3-request'; import Protobuf from 'pbf'; import vt from '@mapbox/vector-tile'; -import { utilRebind, utilTiler } from '../util'; +import { utilHashcode, utilRebind, utilTiler } from '../util'; var tiler = utilTiler().tileSize(512); @@ -20,25 +20,28 @@ function abortRequest(i) { } -function vtToGeoJSON(bufferdata) { - var tile = new vt.VectorTile(new Protobuf(bufferdata.data.response)); - var layers = Object.keys(tile.layers); +function vtToGeoJSON(data, tile) { + var vectorTile = new vt.VectorTile(new Protobuf(data.response)); + var layers = Object.keys(vectorTile.layers); if (!Array.isArray(layers)) { layers = [layers]; } - var collection = { type: 'FeatureCollection', features: [] }; - - layers.forEach(function (layerID) { - var layer = tile.layers[layerID]; + var features = []; + layers.forEach(function(layerID) { + var layer = vectorTile.layers[layerID]; if (layer) { for (var i = 0; i < layer.length; i++) { - var feature = layer.feature(i).toGeoJSON(bufferdata.xyz[0], bufferdata.xyz[1], bufferdata.xyz[2]); - if (layers.length > 1) feature.properties.vt_layer = layerID; - collection.features.push(feature); + var feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]); + if (layers.length > 1) { + feature.properties.vt_layer = layerID; + } + // force unique id generation + feature.__hashcode__ = utilHashcode(JSON.stringify(feature)); + features.push(feature); } } }); - return collection; + return features; } @@ -60,18 +63,13 @@ function loadTile(source, tile) { source.inflight[tile.id] = d3_request(url) .responseType('arraybuffer') .get(function(err, data) { - source.loaded[tile.id] = true; + source.loaded[tile.id] = {}; delete source.inflight[tile.id]; if (err || !data) return; - var bufferdata = { - data: data, - xyz: tile.xyz - }; - source.loaded[tile.id] = { - bufferdata: bufferdata, - geojson: vtToGeoJSON(bufferdata) + data: data, + features: vtToGeoJSON(data, tile) }; dispatch.call('loadedData'); @@ -112,12 +110,23 @@ export default { 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); + var seen = {}; + var results = []; + + for (var i = 0; i < tiles.length; i++) { + var loaded = source.loaded[tiles[i].id]; + if (!loaded || !loaded.features) continue; + + for (var j = 0; j < loaded.features.length; j++) { + var feature = loaded.features[j]; + if (seen[feature.__hashcode__]) continue; + seen[feature.__hashcode__] = true; + results.push(feature); + } + } + + return results; }, diff --git a/modules/svg/data.js b/modules/svg/data.js index 1cf4061fd7..6d263233e9 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -4,7 +4,11 @@ import _reduce from 'lodash-es/reduce'; import _union from 'lodash-es/union'; import _throttle from 'lodash-es/throttle'; -import { geoBounds as d3_geoBounds } from 'd3-geo'; +import { + geoBounds as d3_geoBounds, + geoPath as d3_geoPath +} from 'd3-geo'; + import { text as d3_text } from 'd3-request'; import { @@ -18,6 +22,7 @@ import { geoExtent, geoPolygonIntersectsPolygon } from '../geo'; import { services } from '../services'; import { svgPath } from './index'; import { utilDetect } from '../util/detect'; +import { utilHashcode } from '../util'; var _initialized = false; @@ -110,6 +115,41 @@ export function svgData(projection, context, dispatch) { } + // ensure that all geojson features in a collection have IDs + function ensureIDs(gj) { + if (!gj) return null; + + if (gj.type === 'FeatureCollection') { + for (var i = 0; i < gj.features.length; i++) { + ensureFeatureID(gj.features[i]); + } + } else { + ensureFeatureID(gj); + } + return gj; + } + + + // ensure that each single Feature object has a unique ID + function ensureFeatureID(feature) { + if (!feature) return; + feature.__hashcode__ = utilHashcode(JSON.stringify(feature)); + return feature; + } + + + // Prefer an array of Features instead of a FeatureCollection + function getFeatures(gj) { + if (!gj) return []; + + if (gj.type === 'FeatureCollection') { + return gj.features; + } else { + return [gj]; + } + } + + function drawData(selection) { var vtService = getService(); var getPath = svgPath(projection).geojson; @@ -133,45 +173,42 @@ export function svgData(projection, context, dispatch) { vtService.loadTiles(sourceID, _template, projection); geoData = vtService.data(sourceID, projection); } else { - geoData = _geojson ? [_geojson] : []; + geoData = getFeatures(_geojson); } + geoData = geoData.filter(getPath); + var paths = layer .selectAll('path') - .data(geoData); + .data(geoData, function(d) { return d.__hashcode__; }); + // exit paths.exit() .remove(); + // enter/update paths = paths.enter() .append('path') .attr('class', 'pathdata') - .merge(paths); - - paths + .merge(paths) .attr('d', getPath); - var labelData = []; if (_showLabels) { - geoData.forEach(function(f) { - if (f.type === 'FeatureCollection') { - labelData = labelData.concat(f.features); - } else { - labelData.push(f); - } - }); - labelData = labelData.filter(getPath); + layer + .call(drawLabels, 'label-halo', geoData) + .call(drawLabels, 'label', geoData); } - layer - .call(drawLabels, 'label-halo', labelData) - .call(drawLabels, 'label', labelData); - function drawLabels(selection, textClass, data) { + var labelPath = d3_geoPath(projection); + var labelData = data.filter(function(d) { + return d.properties && (d.properties.desc || d.properties.name); + }); + var labels = selection.selectAll('text.' + textClass) - .data(data); + .data(labelData, function(d) { return d.__hashcode__; }); // exit labels.exit() @@ -183,17 +220,14 @@ export function svgData(projection, context, dispatch) { .attr('class', textClass) .merge(labels) .text(function(d) { - if (d.properties) { - return d.properties.desc || d.properties.name; - } - return null; + return d.properties.desc || d.properties.name; }) .attr('x', function(d) { - var centroid = getPath.centroid(d); + var centroid = labelPath.centroid(d); return centroid[0] + 11; }) .attr('y', function(d) { - var centroid = getPath.centroid(d); + var centroid = labelPath.centroid(d); return centroid[1]; }); } @@ -236,7 +270,7 @@ export function svgData(projection, context, dispatch) { } if (!_isEmpty(gj)) { - _geojson = gj; + _geojson = ensureIDs(gj); _src = src || 'unknown.geojson'; return this.fitZoom(); } @@ -295,7 +329,7 @@ export function svgData(projection, context, dispatch) { _src = null; if (!_isEmpty(gj)) { - _geojson = gj; + _geojson = ensureIDs(gj); _src = src || 'unknown.geojson'; } @@ -343,7 +377,6 @@ export function svgData(projection, context, dispatch) { drawData.setFile(extension, data, url); } }); - } else { drawData.template(url); } @@ -358,12 +391,12 @@ export function svgData(projection, context, dispatch) { drawData.fitZoom = function() { - // note: only works on a FeatureCollection - if (_isEmpty(_geojson) || _isEmpty(_geojson.features)) return; + var features = getFeatures(_geojson); + if (!features.length) return; var map = context.map(); var viewport = map.trimmedExtent().polygon(); - var coords = _reduce(_geojson.features, function(coords, feature) { + var coords = _reduce(features, function(coords, feature) { var c = feature.geometry.coordinates; /* eslint-disable no-fallthrough */ diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index fe57f39262..c450e00f87 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -168,7 +168,17 @@ export function svgPath(projection, graph, isArea) { } }; - svgpath.geojson = path; + svgpath.geojson = function(d) { + if (d.id !== undefined) { + if (d.id in cache) { + return cache[d.id]; + } else { + return cache[d.id] = path(d); + } + } else { + return path(d); + } + }; return svgpath; } diff --git a/modules/util/index.js b/modules/util/index.js index bc06a0a415..16460cd148 100644 --- a/modules/util/index.js +++ b/modules/util/index.js @@ -12,6 +12,7 @@ export { utilFunctor } from './util'; export { utilGetAllNodes } from './util'; export { utilGetPrototypeOf } from './util'; export { utilGetSetValue } from './get_set_value'; +export { utilHashcode } from './util'; export { utilIdleWorker } from './idle_worker'; export { utilNoAuto } from './util'; export { utilPrefixCSSProperty } from './util'; @@ -25,4 +26,4 @@ export { utilSuggestNames } from './suggest_names'; export { utilTagText } from './util'; export { utilTiler } from './tiler'; export { utilTriggerEvent } from './trigger_event'; -export { utilWrap } from './util'; \ No newline at end of file +export { utilWrap } from './util'; diff --git a/modules/util/util.js b/modules/util/util.js index cf226f5ee7..6b03c9f4cd 100644 --- a/modules/util/util.js +++ b/modules/util/util.js @@ -266,3 +266,19 @@ export function utilNoAuto(selection) { .attr('autocapitalize', 'off') .attr('spellcheck', isText ? 'true' : 'false'); } + + +// https://stackoverflow.com/questions/194846/is-there-any-kind-of-hash-code-function-in-javascript +// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ +export function utilHashcode(str) { + var hash = 0; + if (str.length === 0) { + return hash; + } + for (var i = 0; i < str.length; i++) { + var char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32bit integer + } + return hash; +} From bc18f538a01cbc7f6bad9a1784c10c0a5750055d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 23 Aug 2018 01:13:03 -0400 Subject: [PATCH 10/20] Use geojson hashcode instead of id for path cache --- modules/svg/helpers.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index c450e00f87..54929d9eb6 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -169,11 +169,11 @@ export function svgPath(projection, graph, isArea) { }; svgpath.geojson = function(d) { - if (d.id !== undefined) { - if (d.id in cache) { - return cache[d.id]; + if (d.__hashcode__ !== undefined) { + if (d.__hashcode__ in cache) { + return cache[d.__hashcode__]; } else { - return cache[d.id] = path(d); + return cache[d.__hashcode__] = path(d); } } else { return path(d); From 0f07393fb3b0919da9f4b7a63864d3b43521a5a1 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 23 Aug 2018 11:11:14 -0400 Subject: [PATCH 11/20] Fix data file tests (in both phantom and real browsers) --- modules/services/vector_tile.js | 9 +- modules/svg/data.js | 24 ++- modules/svg/helpers.js | 8 +- modules/svg/mvt.js | 301 -------------------------------- test/data/gpxtest.gpx | 13 -- test/data/gpxtest.json | 23 --- test/data/gpxtest.kml | 22 --- test/data/mvttest.pbf | Bin 24576 -> 0 bytes test/spec/svg/data.js | 166 +++++++++++++----- test/spec/svg/mvt.js | 97 ---------- 10 files changed, 138 insertions(+), 525 deletions(-) delete mode 100644 modules/svg/mvt.js delete mode 100644 test/data/gpxtest.gpx delete mode 100644 test/data/gpxtest.json delete mode 100644 test/data/gpxtest.kml delete mode 100644 test/data/mvttest.pbf delete mode 100644 test/spec/svg/mvt.js diff --git a/modules/services/vector_tile.js b/modules/services/vector_tile.js index e6de67d877..6c947c918c 100644 --- a/modules/services/vector_tile.js +++ b/modules/services/vector_tile.js @@ -34,8 +34,9 @@ function vtToGeoJSON(data, tile) { if (layers.length > 1) { feature.properties.vt_layer = layerID; } - // force unique id generation - feature.__hashcode__ = utilHashcode(JSON.stringify(feature)); + // force some unique id generation + feature.__featurehash__ = utilHashcode(JSON.stringify(feature)); + feature.__propertyhash__ = utilHashcode(JSON.stringify(feature.properties || {})); features.push(feature); } } @@ -120,8 +121,8 @@ export default { for (var j = 0; j < loaded.features.length; j++) { var feature = loaded.features[j]; - if (seen[feature.__hashcode__]) continue; - seen[feature.__hashcode__] = true; + if (seen[feature.__featurehash__]) continue; + seen[feature.__featurehash__] = true; results.push(feature); } } diff --git a/modules/svg/data.js b/modules/svg/data.js index 6d263233e9..38e0c15e01 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -133,7 +133,7 @@ export function svgData(projection, context, dispatch) { // ensure that each single Feature object has a unique ID function ensureFeatureID(feature) { if (!feature) return; - feature.__hashcode__ = utilHashcode(JSON.stringify(feature)); + feature.__featurehash__ = utilHashcode(JSON.stringify(feature)); return feature; } @@ -180,7 +180,7 @@ export function svgData(projection, context, dispatch) { var paths = layer .selectAll('path') - .data(geoData, function(d) { return d.__hashcode__; }); + .data(geoData, function(d) { return d.__featurehash__; }); // exit paths.exit() @@ -194,21 +194,19 @@ export function svgData(projection, context, dispatch) { .attr('d', getPath); - if (_showLabels) { - layer - .call(drawLabels, 'label-halo', geoData) - .call(drawLabels, 'label', geoData); - } + layer + .call(drawLabels, 'label-halo', geoData) + .call(drawLabels, 'label', geoData); function drawLabels(selection, textClass, data) { var labelPath = d3_geoPath(projection); var labelData = data.filter(function(d) { - return d.properties && (d.properties.desc || d.properties.name); + return _showLabels && d.properties && (d.properties.desc || d.properties.name); }); var labels = selection.selectAll('text.' + textClass) - .data(labelData, function(d) { return d.__hashcode__; }); + .data(labelData, function(d) { return d.__featurehash__; }); // exit labels.exit() @@ -272,10 +270,11 @@ export function svgData(projection, context, dispatch) { if (!_isEmpty(gj)) { _geojson = ensureIDs(gj); _src = src || 'unknown.geojson'; - return this.fitZoom(); + this.fitZoom(); } dispatch.call('change'); + return this; }; @@ -373,9 +372,8 @@ export function svgData(projection, context, dispatch) { if (re.test(extension)) { _template = null; d3_text(url, function(err, data) { - if (!err) { - drawData.setFile(extension, data, url); - } + if (err) return; + drawData.setFile(extension, data, url); }); } else { drawData.template(url); diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index 54929d9eb6..6c9bd344a7 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -169,11 +169,11 @@ export function svgPath(projection, graph, isArea) { }; svgpath.geojson = function(d) { - if (d.__hashcode__ !== undefined) { - if (d.__hashcode__ in cache) { - return cache[d.__hashcode__]; + if (d.__featurehash__ !== undefined) { + if (d.__featurehash__ in cache) { + return cache[d.__featurehash__]; } else { - return cache[d.__hashcode__] = path(d); + return cache[d.__featurehash__] = path(d); } } else { return path(d); diff --git a/modules/svg/mvt.js b/modules/svg/mvt.js deleted file mode 100644 index ce6a8a5403..0000000000 --- a/modules/svg/mvt.js +++ /dev/null @@ -1,301 +0,0 @@ -import _flatten from 'lodash-es/flatten'; -import _isEmpty from 'lodash-es/isEmpty'; -import _reduce from 'lodash-es/reduce'; -import _union from 'lodash-es/union'; - -import { geoBounds as d3_geoBounds } from 'd3-geo'; -import { request as d3_request } from 'd3-request'; - -import { - event as d3_event, - select as d3_select -} from 'd3-selection'; - -import vt from '@mapbox/vector-tile'; -import Protobuf from 'pbf'; - -import { geoExtent, geoPolygonIntersectsPolygon } from '../geo'; -import { svgPath } from './index'; -import { utilDetect } from '../util/detect'; - - -var _initialized = false; -var _enabled = false; -var _geojson; - - -export function svgMvt(projection, context, dispatch) { - var _showLabels = true; - var detected = utilDetect(); - var layer; - var _src; - - - function init() { - if (_initialized) return; // run once - - _geojson = {}; - _enabled = true; - - function over() { - d3_event.stopPropagation(); - d3_event.preventDefault(); - d3_event.dataTransfer.dropEffect = 'copy'; - } - - d3_select('body') - .attr('dropzone', 'copy') - .on('drop.localmvt', function() { - d3_event.stopPropagation(); - d3_event.preventDefault(); - if (!detected.filedrop) return; - drawMvt.files(d3_event.dataTransfer.files); - }) - .on('dragenter.localmvt', over) - .on('dragexit.localmvt', over) - .on('dragover.localmvt', over); - - _initialized = true; - } - - - function drawMvt(selection) { - var getPath = svgPath(projection).geojson; - - layer = selection.selectAll('.layer-mvt') - .data(_enabled ? [0] : []); - - layer.exit() - .remove(); - - layer = layer.enter() - .append('g') - .attr('class', 'layer-mvt') - .merge(layer); - - - var paths = layer - .selectAll('path') - .data([_geojson]); - - paths.exit() - .remove(); - - paths = paths.enter() - .append('path') - .attr('class', 'mvt') - .merge(paths); - - paths - .attr('d', getPath); - - - var labelData = _showLabels && _geojson.features ? _geojson.features : []; - labelData = labelData.filter(getPath); - - layer - .call(drawLabels, 'mvtlabel-halo', labelData) - .call(drawLabels, 'mvtlabel', labelData); - - - function drawLabels(selection, textClass, data) { - var labels = selection.selectAll('text.' + textClass) - .data(data); - - // exit - labels.exit() - .remove(); - - // enter/update - labels = labels.enter() - .append('text') - .attr('class', textClass) - .merge(labels) - .text(function(d) { - if (d.properties) { - return d.properties.desc || d.properties.name; - } - return null; - }) - .attr('x', function(d) { - var centroid = getPath.centroid(d); - return centroid[0] + 11; - }) - .attr('y', function(d) { - var centroid = getPath.centroid(d); - return centroid[1]; - }); - } - } - - - 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 getExtension(fileName) { - if (fileName === undefined) { - return ''; - } - - var lastDotIndex = fileName.lastIndexOf('.'); - if (lastDotIndex < 0) { - return ''; - } - - return fileName.substr(lastDotIndex); - } - - - function parseSaveAndZoom(extension, bufferdata) { - switch (extension) { - case '.pbf': - drawMvt.geojson(vtToGeoJson(bufferdata)).fitZoom(); - break; - case '.mvt': - drawMvt.geojson(vtToGeoJson(bufferdata)).fitZoom(); - break; - } - } - - - drawMvt.showLabels = function(_) { - if (!arguments.length) return _showLabels; - _showLabels = _; - return this; - }; - - - drawMvt.enabled = function(_) { - if (!arguments.length) return _enabled; - _enabled = _; - dispatch.call('change'); - return this; - }; - - - drawMvt.hasMvt = function() { - return (!(_isEmpty(_geojson) || _isEmpty(_geojson.features))); - }; - - - drawMvt.geojson = function(gj) { - if (!arguments.length) return _geojson; - if (_isEmpty(gj) || _isEmpty(gj.features)) return this; - _geojson = gj; - dispatch.call('change'); - return this; - }; - - - drawMvt.url = function(url) { - 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); - }); - - return this; - }; - - - drawMvt.files = function(fileList) { - if (!fileList.length) return this; - var f = fileList[0], - reader = new FileReader(); - - 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); - return this; - }; - - - drawMvt.getSrc = function () { - return _src; - }; - - - drawMvt.fitZoom = function() { - if (!this.hasMvt()) return this; - - var map = context.map(); - var viewport = map.trimmedExtent().polygon(); - var coords = _reduce(_geojson.features, function(coords, feature) { - var c = feature.geometry.coordinates; - - /* eslint-disable no-fallthrough */ - switch (feature.geometry.type) { - case 'Point': - c = [c]; - case 'MultiPoint': - case 'LineString': - break; - - case 'MultiPolygon': - c = _flatten(c); - case 'Polygon': - case 'MultiLineString': - c = _flatten(c); - break; - } - /* eslint-enable no-fallthrough */ - - return _union(coords, c); - }, []); - - if (!geoPolygonIntersectsPolygon(viewport, coords, true)) { - var extent = geoExtent(d3_geoBounds({ type: 'LineString', coordinates: coords })); - map.centerZoom(extent.center(), map.trimmedExtentZoom(extent)); - } - - return this; - }; - - - init(); - return drawMvt; -} diff --git a/test/data/gpxtest.gpx b/test/data/gpxtest.gpx deleted file mode 100644 index 0df3c35d5a..0000000000 --- a/test/data/gpxtest.gpx +++ /dev/null @@ -1,13 +0,0 @@ - - - - - New Jersey - - N.J. - 19717.8 - New Jersey - 316973311 - - - diff --git a/test/data/gpxtest.json b/test/data/gpxtest.json deleted file mode 100644 index 4982aa6c3c..0000000000 --- a/test/data/gpxtest.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -74.38928604125977, - 40.150275473401365 - ] - }, - "properties": { - "abbr": "N.J.", - "area": 19717.8, - "name": "New Jersey", - "name_en": "New Jersey", - "osm_id": 316973311 - }, - "id": 316973311 - } - ] -} diff --git a/test/data/gpxtest.kml b/test/data/gpxtest.kml deleted file mode 100644 index 5acf1ad164..0000000000 --- a/test/data/gpxtest.kml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - -gpxtest - - New Jersey - - N.J. - 19717.8 - New Jersey - 316973311 - - -74.3892860412598,40.1502754734014 - - - diff --git a/test/data/mvttest.pbf b/test/data/mvttest.pbf deleted file mode 100644 index 63858a7a95cc69bbca67f9ba98ddd88fa0f8ada7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 zcmeI4eQXnD9LMjyy|(K!nOz-odUP&q^Hz52-heorqb3{6#>+4$!Exf00e*l5C8%|;J-&;ttfdbD%dTi z9*P>Np(tKY_NGkT)Uso#Z9}lNC#dwaKG+#lvX4}np{N?^8&yf0ssX{~UFKzXmq+4# z#;6&I8ol#TN=$dMsjGecrl8W^wI;Y(S*W8V;#Mi8R6dbN#CoH~h!It3g>WJ|9E+z5 z$;6JTHeT|2%Gj{gdt8qhF~ijRbW;~{4MHlA?lYB?N|LD>=4@UsR@-jPohV6JJATn@ ztyH!qB@#D{0VBCc+f7NV@umzBr-Y)3P*poCdCSV!0c+HZq|7I`XzWau3RXaw z=&)hMRb*}MaMdyBM|2*&i&y9;&|b72wW0t{!UhC@01yBIKmZ5;0U!VbfWZHpz}_bv zg6F{4yd3hWim%T|g_Ds%Gm?mt zBP11z#K{^B7i&pLA5E9EdR**}7|}k`-MCK=g_5MrMl%_S50I96($Gn!Yd97%k|aZx zVvFmLOs`=Lw$Lz82CPgtArj(Pf)%(gCfhv?Ye)Chk&GeG5|?r!g66er~?- zLd^jqfmd6}F;Zb&0x$7=s!unu?$EGlWO{7K%2~kh6f<>5Lu`*%JC1Q-_kefMMx2CuBY0lYMj_C)q#+6NhfTqm@n9pPo%qJ$c;@5;r=1Yp`59WiR zez%?Xl#+9l7%`IgVyUz7CA}ehRrIB5n(F<5=9P8LjcR?ZUu&qXYidw)B^8M;Ssga; zmjcOLhgGfKCGw@WrSB50-YM{Ow|@MUgg+LtF7YCk z_f!zp*-5hgN49YWCEZutJq7O-)VXF{h0a}$pB<0NAIkOi8M_-Fh7AY+0U!VbfB+D< zH3W7SB8la=$zu1C50-y&_W`@rEyo|Oetp}wq3#v0RJPanKi$6WP^i7Lqndjzcyelk zIyQCoW}-IBdd-ddP_K7=;itH@}il-hHHi!e={lc%U87nS7XHVfDDBx!EnHSsa# zLeJ-isU#%h^}=Qb9HQE5~(0TknCpCai%IWA9n-hBG^X^xQxqnj)}PEL~T@?>$I)XW{bD)Ef{ z8C9ZcSjld;=TFV_1((d+)B46XqQ*hqXqrK; zvE?Xp(b%$bIcaQps2mOvyO&sEUXo=TvMhED*=dCBIyM?%pNvE!Y)}ztgqhM%s)?2Yfv=u`V-M2>tg@_ diff --git a/test/spec/svg/data.js b/test/spec/svg/data.js index 8068fc18bc..323420756f 100644 --- a/test/spec/svg/data.js +++ b/test/spec/svg/data.js @@ -7,29 +7,78 @@ describe('iD.svgData', function () { .scale(iD.geoZoomToScale(17)) .clipExtent([[0, 0], [1000, 1000]]); - var gj = { - 'type': 'FeatureCollection', - 'features': [ - { - 'type': 'Feature', - 'id': 316973311, - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -74.38928604125977, - 40.150275473401365 - ] - }, - 'properties': { - 'abbr': 'N.J.', - 'area': 19717.8, - 'name': 'New Jersey', - 'name_en': 'New Jersey', - 'osm_id': 316973311 - } - } - ] - }; + var geojson = + '{' + + ' "type": "FeatureCollection",' + + ' "features": [' + + ' {' + + ' "type": "Feature",' + + ' "geometry": {' + + ' "type": "Point",' + + ' "coordinates": [-74.38928604125977, 40.150275473401365]' + + ' },' + + ' "properties": {' + + ' "abbr": "N.J.",' + + ' "area": 19717.8,' + + ' "name": "New Jersey",' + + ' "name_en": "New Jersey",' + + ' "osm_id": 316973311' + + ' },' + + ' "id": 316973311' + + ' }' + + ' ]' + + '}'; + + var gj = JSON.parse(geojson); + + var gpx = + '' + + '' + + '' + + '' + + ' New Jersey' + + ' ' + + ' N.J.' + + ' 19717.8' + + ' New Jersey' + + ' 316973311' + + ' ' + + '' + + ''; + + var kml = + '' + + '' + + '' + + '' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + + 'gpxtest' + + ' ' + + ' New Jersey' + + ' ' + + ' N.J.' + + ' 19717.8' + + ' New Jersey' + + ' 316973311' + + ' ' + + ' -74.3892860412598,40.1502754734014' + + ' ' + + '' + + '' + + ''; + + + // this is because PhantomJS hasn't implemented a proper File constructor + function makeFile(contents, fileName, mimeType) { + var blob = new Blob([contents], { type: mimeType }); + blob.lastModifiedDate = new Date(); + blob.name = fileName; + return blob; + } beforeEach(function () { context = iD.coreContext(); @@ -58,35 +107,56 @@ describe('iD.svgData', function () { expect(path.attr('d')).to.match(/^M.*z$/); }); - describe('#files', function() { - it('handles gpx files', function () { - var files = '../../data/gpxtest.gpx'; - var render = iD.svgData(projection, context, dispatch).files(files); - surface.call(render); - - var path = surface.selectAll('path'); - expect(path.nodes().length).to.eql(1); - expect(path.attr('d')).to.match(/^M.*z$/); + describe('#fileList', function() { + it('handles gpx files', function (done) { + var files = [ makeFile(gpx, 'test.gpx', 'application/gpx+xml') ]; + var render = iD.svgData(projection, context, dispatch); + var spy = sinon.spy(); + dispatch.on('change', spy); + render.fileList(files); + + window.setTimeout(function() { + expect(spy).to.have.been.calledOnce; + surface.call(render); + var path = surface.selectAll('path'); + expect(path.nodes().length).to.eql(1); + expect(path.attr('d')).to.match(/^M.*z$/); + done(); + }, 200); }); - it('handles geojson files', function () { - var files = '../../data/gpxtest.json'; - var render = iD.svgData(projection, context, dispatch).files(files); - surface.call(render); - - var path = surface.selectAll('path'); - expect(path.nodes().length).to.eql(1); - expect(path.attr('d')).to.match(/^M.*z$/); + it('handles kml files', function (done) { + var files = [ makeFile(kml, 'test.kml', 'application/vnd.google-earth.kml+xml') ]; + var render = iD.svgData(projection, context, dispatch); + var spy = sinon.spy(); + dispatch.on('change', spy); + render.fileList(files); + + window.setTimeout(function() { + expect(spy).to.have.been.calledOnce; + surface.call(render); + var path = surface.selectAll('path'); + expect(path.nodes().length).to.eql(1); + expect(path.attr('d')).to.match(/^M.*z$/); + done(); + }, 200); }); - it('handles kml files', function () { - var files = '../../data/gpxtest.kml'; - var render = iD.svgData(projection, context, dispatch).files(files); - surface.call(render); - - var path = surface.selectAll('path'); - expect(path.nodes().length).to.eql(1); - expect(path.attr('d')).to.match(/^M.*z$/); + it('handles geojson files', function (done) { + var files = [ makeFile(geojson, 'test.geojson', 'application/vnd.geo+json') ]; + var render = iD.svgData(projection, context, dispatch); + var spy = sinon.spy(); + dispatch.on('change', spy); + render.fileList(files); + + window.setTimeout(function() { + expect(spy).to.have.been.calledOnce; + surface.call(render); + var path = surface.selectAll('path'); + expect(path.nodes().length).to.eql(1); + expect(path.attr('d')).to.match(/^M.*z$/); + done(); + }, 200); }); }); diff --git a/test/spec/svg/mvt.js b/test/spec/svg/mvt.js deleted file mode 100644 index e5ab522aa4..0000000000 --- a/test/spec/svg/mvt.js +++ /dev/null @@ -1,97 +0,0 @@ -describe('iD.svgMvt', function () { - var context; - var surface; - var dispatch = d3.dispatch('change'); - var projection = iD.geoRawMercator() - .translate([6934098.868981334, 4092682.5519805425]) - .scale(iD.geoZoomToScale(17)) - .clipExtent([[0, 0], [1000, 1000]]); - - - var gj = { - 'type': 'FeatureCollection', - 'features': [ - { - 'type': 'Feature', - 'id': 316973311, - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -74.38928604125977, - 40.150275473401365 - ] - }, - 'properties': { - 'abbr': 'N.J.', - 'area': 19717.8, - 'name': 'New Jersey', - 'name_en': 'New Jersey', - 'osm_id': 316973311 - } - } - ] - }; - - beforeEach(function () { - context = iD.coreContext(); - d3.select(document.createElement('div')) - .attr('id', 'map') - .call(context.map().centerZoom([-74.389286, 40.1502754], 17)); - - surface = context.surface(); - }); - - it('creates layer-mvt', function () { - var render = iD.svgMvt(projection, context, dispatch); - surface.call(render); - - var layers = surface.selectAll('g.layer-mvt').nodes(); - expect(layers.length).to.eql(1); - }); - - it('draws geojson', function () { - var render = iD.svgMvt(projection, context, dispatch).geojson(gj); - surface.call(render); - - var path = surface.selectAll('path.mvt'); - expect(path.nodes().length).to.eql(1); - expect(path.attr('d')).to.match(/^M.*z$/); - }); - - describe('#url', function() { - it('handles pbf url', function () { - var url = '../../data/mvttest.pbf'; - var render = iD.svgMvt(projection, context, dispatch).url(url); - surface.call(render); - - var path = surface.selectAll('path.mvt'); - expect(path.nodes().length).to.eql(1); - expect(path.attr('d')).to.match(/^M.*z$/); - }); - }); - - describe('#showLabels', function() { - it('shows labels by default', function () { - var render = iD.svgMvt(projection, context, dispatch).geojson(gj); - surface.call(render); - - var label = surface.selectAll('text.mvtlabel'); - expect(label.nodes().length).to.eql(1); - expect(label.text()).to.eql('New Jersey'); - - var halo = surface.selectAll('text.mvtlabel-halo'); - expect(halo.nodes().length).to.eql(1); - expect(halo.text()).to.eql('New Jersey'); - }); - - - it('hides labels with showLabels(false)', function () { - var render = iD.svgMvt(projection, context, dispatch).geojson(gj).showLabels(false); - surface.call(render); - - expect(surface.selectAll('text.mvtlabel').empty()).to.be.ok; - expect(surface.selectAll('text.mvtlabel-halo').empty()).to.be.ok; - }); - }); - -}); From 259e410b43e6f65c24217b4d127c7bd2c1f0a470 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 23 Aug 2018 13:03:18 -0400 Subject: [PATCH 12/20] Clip polygons to tile boundaries --- modules/services/vector_tile.js | 4 ++++ package.json | 1 + 2 files changed, 5 insertions(+) diff --git a/modules/services/vector_tile.js b/modules/services/vector_tile.js index 6c947c918c..01ac65148b 100644 --- a/modules/services/vector_tile.js +++ b/modules/services/vector_tile.js @@ -4,6 +4,7 @@ import _forEach from 'lodash-es/forEach'; import { dispatch as d3_dispatch } from 'd3-dispatch'; import { request as d3_request } from 'd3-request'; +import bboxClip from '@turf/bbox-clip'; import Protobuf from 'pbf'; import vt from '@mapbox/vector-tile'; @@ -34,6 +35,9 @@ function vtToGeoJSON(data, tile) { if (layers.length > 1) { feature.properties.vt_layer = layerID; } + // clip to tile bounds + feature = bboxClip(feature, tile.extent.rectangle()); + // force some unique id generation feature.__featurehash__ = utilHashcode(JSON.stringify(feature)); feature.__propertyhash__ = utilHashcode(JSON.stringify(feature.properties || {})); diff --git a/package.json b/package.json index 312868496a..d87e1818d7 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@mapbox/sexagesimal": "1.1.0", "@mapbox/togeojson": "0.16.0", "@mapbox/vector-tile": "^1.3.1", + "@turf/bbox-clip": "^6.0.0", "diacritics": "1.3.0", "lodash-es": "4.17.10", "marked": "0.5.0", From b0f86cdf31c3b35e18139a7ee7758fdb81cfd882 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 23 Aug 2018 22:03:51 -0400 Subject: [PATCH 13/20] Merge matching polygons across tile boundaries --- build_src.js | 5 +- modules/services/vector_tile.js | 89 +++++++++++++++++++++++++-------- modules/svg/data.js | 3 +- package.json | 2 + 4 files changed, 76 insertions(+), 23 deletions(-) diff --git a/build_src.js b/build_src.js index 425eb61205..347bb922b7 100644 --- a/build_src.js +++ b/build_src.js @@ -27,7 +27,10 @@ module.exports = function buildSrc() { input: './modules/id.js', plugins: [ includePaths( { - paths: ['node_modules/d3/node_modules'] // npm2 or windows + paths: ['node_modules/d3/node_modules'], // npm2 or windows + include: { + 'martinez-polygon-clipping': 'node_modules/martinez-polygon-clipping/dist/martinez.umd.js' + } }), nodeResolve({ module: true, diff --git a/modules/services/vector_tile.js b/modules/services/vector_tile.js index 01ac65148b..d5a5e2fc96 100644 --- a/modules/services/vector_tile.js +++ b/modules/services/vector_tile.js @@ -1,17 +1,21 @@ import _find from 'lodash-es/find'; +import _isEqual from 'lodash-es/isEqual'; import _forEach from 'lodash-es/forEach'; import { dispatch as d3_dispatch } from 'd3-dispatch'; import { request as d3_request } from 'd3-request'; -import bboxClip from '@turf/bbox-clip'; +import turf_bboxClip from '@turf/bbox-clip'; +import stringify from 'fast-json-stable-stringify'; +import martinez from 'martinez-polygon-clipping'; + import Protobuf from 'pbf'; import vt from '@mapbox/vector-tile'; import { utilHashcode, utilRebind, utilTiler } from '../util'; -var tiler = utilTiler().tileSize(512); +var tiler = utilTiler().tileSize(512).margin(1); var dispatch = d3_dispatch('loadedData'); var _vtCache; @@ -21,7 +25,7 @@ function abortRequest(i) { } -function vtToGeoJSON(data, tile) { +function vtToGeoJSON(data, tile, mergeCache) { var vectorTile = new vt.VectorTile(new Protobuf(data.response)); var layers = Object.keys(vectorTile.layers); if (!Array.isArray(layers)) { layers = [layers]; } @@ -32,16 +36,58 @@ function vtToGeoJSON(data, tile) { if (layer) { for (var i = 0; i < layer.length; i++) { var feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]); + var geometry = feature.geometry; if (layers.length > 1) { feature.properties.vt_layer = layerID; } - // clip to tile bounds - feature = bboxClip(feature, tile.extent.rectangle()); + + // Treat all Polygons as MultiPolygons + if (geometry.type === 'Polygon') { + geometry.type = 'MultiPolygon'; + geometry.coordinates = [geometry.coordinates]; + } + + // Clip to tile bounds + if (geometry.type === 'MultiPolygon') { + var isClipped = false; + var featureClip = turf_bboxClip(feature, tile.extent.rectangle()); + if (!_isEqual(feature.geometry, featureClip.geometry)) { + // feature = featureClip; + isClipped = true; + } + if (!feature.geometry.coordinates.length) continue; // not actually on this tile + if (!feature.geometry.coordinates[0].length) continue; // not actually on this tile + } // force some unique id generation - feature.__featurehash__ = utilHashcode(JSON.stringify(feature)); - feature.__propertyhash__ = utilHashcode(JSON.stringify(feature.properties || {})); + var featurehash = utilHashcode(stringify(feature)); + var propertyhash = utilHashcode(stringify(feature.properties || {})); + feature.__featurehash__ = featurehash; + feature.__propertyhash__ = propertyhash; features.push(feature); + + // Clipped Polygons at same zoom with identical properties can get merged + if (isClipped && geometry.type === 'MultiPolygon') { + var merged = mergeCache[propertyhash]; + if (merged && merged.length) { + var other = merged[0]; + var coords = martinez.union( + feature.geometry.coordinates, other.geometry.coordinates + ); + + if (!coords || !coords.length) { + continue; // something failed in martinez union + } + + merged.push(feature); + for (var j = 0; j < merged.length; j++) { // all these features get... + merged[j].geometry.coordinates = coords; // same coords + merged[j].__featurehash__ = featurehash; // same hash, so deduplication works + } + } else { + mergeCache[propertyhash] = [feature]; + } + } } } }); @@ -64,19 +110,19 @@ function loadTile(source, tile) { 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] = {}; + source.loaded[tile.id] = []; delete source.inflight[tile.id]; if (err || !data) return; - source.loaded[tile.id] = { - data: data, - features: vtToGeoJSON(data, tile) - }; + var z = tile.xyz[2]; + if (!source.canMerge[z]) { + source.canMerge[z] = {}; // initialize mergeCache + } + source.loaded[tile.id] = vtToGeoJSON(data, tile, source.canMerge[z]); dispatch.call('loadedData'); }); } @@ -106,7 +152,7 @@ export default { addSource: function(sourceID, template) { - _vtCache[sourceID] = { template: template, inflight: {}, loaded: {} }; + _vtCache[sourceID] = { template: template, inflight: {}, loaded: {}, canMerge: {} }; return _vtCache[sourceID]; }, @@ -120,13 +166,14 @@ export default { var results = []; for (var i = 0; i < tiles.length; i++) { - var loaded = source.loaded[tiles[i].id]; - if (!loaded || !loaded.features) continue; - - for (var j = 0; j < loaded.features.length; j++) { - var feature = loaded.features[j]; - if (seen[feature.__featurehash__]) continue; - seen[feature.__featurehash__] = true; + var features = source.loaded[tiles[i].id]; + if (!features || !features.length) continue; + + for (var j = 0; j < features.length; j++) { + var feature = features[j]; + var hash = feature.__featurehash__; + if (seen[hash]) continue; + seen[hash] = true; results.push(feature); } } diff --git a/modules/svg/data.js b/modules/svg/data.js index 38e0c15e01..581da8fc73 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -16,6 +16,7 @@ import { select as d3_select } from 'd3-selection'; +import stringify from 'fast-json-stable-stringify'; import toGeoJSON from '@mapbox/togeojson'; import { geoExtent, geoPolygonIntersectsPolygon } from '../geo'; @@ -133,7 +134,7 @@ export function svgData(projection, context, dispatch) { // ensure that each single Feature object has a unique ID function ensureFeatureID(feature) { if (!feature) return; - feature.__featurehash__ = utilHashcode(JSON.stringify(feature)); + feature.__featurehash__ = utilHashcode(stringify(feature)); return feature; } diff --git a/package.json b/package.json index d87e1818d7..a037d7b769 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,10 @@ "@mapbox/vector-tile": "^1.3.1", "@turf/bbox-clip": "^6.0.0", "diacritics": "1.3.0", + "fast-json-stable-stringify": "2.0.0", "lodash-es": "4.17.10", "marked": "0.5.0", + "martinez-polygon-clipping": "0.5.0", "node-diff3": "1.0.0", "osm-auth": "1.0.2", "pannellum": "2.4.1", From 48e233e4a3d6d8f88fa33e6464a757d755efb53a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 24 Aug 2018 10:39:03 -0400 Subject: [PATCH 14/20] Move some css rules around, add hash and geometry.type to css classlist --- css/20_map.css | 30 ------- css/65_data.css | 137 ++++++-------------------------- css/80_app.css | 129 +++++++++++++++++++++++++++++- modules/services/vector_tile.js | 6 +- modules/svg/data.js | 13 ++- 5 files changed, 164 insertions(+), 151 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 79c1275553..13c72a12ba 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -306,33 +306,3 @@ g.turn circle { stroke: #68f; } - -/* Other Data (gpx, kml, geojson, mvt, pbf) */ - -.layer-mapdata { - pointer-events: none; -} - -.layer-mapdata path { - stroke: #ff26d4; - stroke-width: 2; - fill: none; -} - -.layer-mapdata text.label-halo, -.layer-mapdata text.label { - font-size: 10px; - font-weight: bold; - dominant-baseline: middle; -} - -.layer-mapdata text.label { - fill: #ff26d4; -} - -.layer-mapdata text.label-halo { - opacity: 0.7; - stroke: #000; - stroke-width: 5px; - stroke-miterlimit: 1; -} diff --git a/css/65_data.css b/css/65_data.css index 38346e8a26..fcf5d3a66b 100644 --- a/css/65_data.css +++ b/css/65_data.css @@ -41,140 +41,49 @@ .note-header-icon .preset-icon-28 { top: 18px; } - .note-header-icon .note-icon-annotation { position: absolute; top: 22px; left: 22px; margin: auto; } - .note-header-icon .note-icon-annotation .icon { width: 15px; height: 15px; } -/* OSM Note UI */ -.note-header { - background-color: #f6f6f6; - border-radius: 5px; - border: 1px solid #ccc; - display: flex; - flex-flow: row nowrap; - align-items: center; -} - -.note-header-icon { - background-color: #fff; - padding: 10px; - flex: 0 0 62px; - position: relative; - width: 60px; - height: 60px; - border-right: 1px solid #ccc; - border-radius: 5px 0 0 5px; -} -[dir='rtl'] .note-header-icon { - border-right: unset; - border-left: 1px solid #ccc; - border-radius: 0 5px 5px 0; -} - -.note-header-icon .icon-wrap { - position: absolute; - top: 0px; -} - -.note-header-label { - background-color: #f6f6f6; - padding: 0 15px; - flex: 1 1 100%; - font-size: 14px; - font-weight: bold; - border-radius: 0 5px 5px 0; -} -[dir='rtl'] .note-header-label { - border-radius: 5px 0 0 5px; -} +/* Custom Map Data (geojson, gpx, kml, vector tile) */ -.note-category { - margin: 20px 0px; +.layer-mapdata { + pointer-events: none; } -.comments-container { - background: #ececec; - padding: 1px 10px; - border-radius: 8px; - margin-top: 20px; +.layer-mapdata path { + stroke: #ff26d4; + stroke-width: 2; + fill: none; } - -.comment { - background-color: #fff; - border-radius: 5px; - border: 1px solid #ccc; - margin: 10px auto; - display: flex; - flex-flow: row nowrap; -} -.comment-avatar { - padding: 10px; - flex: 0 0 62px; -} -.comment-avatar .icon.comment-avatar-icon { - width: 40px; - height: 40px; - object-fit: cover; - border: 1px solid #ccc; - border-radius: 20px; -} -.comment-main { - padding: 10px 10px 10px 0; - flex: 1 1 100%; - flex-flow: column nowrap; - overflow: hidden; - overflow-wrap: break-word; -} -[dir='rtl'] .comment-main { - padding: 10px 0 10px 10px; +.layer-mapdata path.MultiPolygon, +.layer-mapdata path.Polygon { + stroke-width: 1; + fill: #ff26d4; + fill-opacity: 0.2; } -.comment-metadata { - flex-flow: row nowrap; - justify-content: space-between; -} -.comment-author { +.layer-mapdata text.label-halo, +.layer-mapdata text.label { + font-size: 10px; font-weight: bold; - color: #333; + dominant-baseline: middle; } -.comment-date { - color: #aaa; +.layer-mapdata text.label { + fill: #ff26d4; } -.comment-text { - color: #333; - margin-top: 10px; - overflow-y: auto; - max-height: 250px; -} -.comment-text::-webkit-scrollbar { - border-left: none; -} - -.note-save { - padding: 10px; +.layer-mapdata text.label-halo { + opacity: 0.7; + stroke: #000; + stroke-width: 5px; + stroke-miterlimit: 1; } -.note-save #new-comment-input { - width: 100%; - height: 100px; - max-height: 300px; - min-height: 100px; -} - -.note-save .detail-section { - margin: 10px 0; -} - -.note-report { - float: right; -} diff --git a/css/80_app.css b/css/80_app.css index c33de8c30b..0894a06fdd 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -1204,7 +1204,7 @@ a.hide-toggle { } -/* preset form basics */ +/* Preset Editor */ .preset-editor { overflow: hidden; @@ -2415,6 +2415,133 @@ input.key-trap { border: 1px solid rgba(0,0,0,0); } + +/* OSM Note UI */ +.note-header { + background-color: #f6f6f6; + border-radius: 5px; + border: 1px solid #ccc; + display: flex; + flex-flow: row nowrap; + align-items: center; +} + +.note-header-icon { + background-color: #fff; + padding: 10px; + flex: 0 0 62px; + position: relative; + width: 60px; + height: 60px; + border-right: 1px solid #ccc; + border-radius: 5px 0 0 5px; +} +[dir='rtl'] .note-header-icon { + border-right: unset; + border-left: 1px solid #ccc; + border-radius: 0 5px 5px 0; +} + +.note-header-icon .icon-wrap { + position: absolute; + top: 0px; +} + +.note-header-label { + background-color: #f6f6f6; + padding: 0 15px; + flex: 1 1 100%; + font-size: 14px; + font-weight: bold; + border-radius: 0 5px 5px 0; +} +[dir='rtl'] .note-header-label { + border-radius: 5px 0 0 5px; +} + +.note-category { + margin: 20px 0px; +} + +.comments-container { + background: #ececec; + padding: 1px 10px; + border-radius: 8px; + margin-top: 20px; +} + +.comment { + background-color: #fff; + border-radius: 5px; + border: 1px solid #ccc; + margin: 10px auto; + display: flex; + flex-flow: row nowrap; +} +.comment-avatar { + padding: 10px; + flex: 0 0 62px; +} +.comment-avatar .icon.comment-avatar-icon { + width: 40px; + height: 40px; + object-fit: cover; + border: 1px solid #ccc; + border-radius: 20px; +} +.comment-main { + padding: 10px 10px 10px 0; + flex: 1 1 100%; + flex-flow: column nowrap; + overflow: hidden; + overflow-wrap: break-word; +} +[dir='rtl'] .comment-main { + padding: 10px 0 10px 10px; +} + +.comment-metadata { + flex-flow: row nowrap; + justify-content: space-between; +} +.comment-author { + font-weight: bold; + color: #333; +} +.comment-date { + color: #aaa; +} +.comment-text { + color: #333; + margin-top: 10px; + overflow-y: auto; + max-height: 250px; +} +.comment-text::-webkit-scrollbar { + border-left: none; +} + +.note-save { + padding: 10px; +} + +.note-save #new-comment-input { + width: 100%; + height: 100px; + max-height: 300px; + min-height: 100px; +} + +.note-save .detail-section { + margin: 10px 0; +} + +.note-report { + float: right; +} + + + /* Fullscreen button */ div.full-screen { float: right; diff --git a/modules/services/vector_tile.js b/modules/services/vector_tile.js index d5a5e2fc96..1239dd06b8 100644 --- a/modules/services/vector_tile.js +++ b/modules/services/vector_tile.js @@ -37,9 +37,6 @@ function vtToGeoJSON(data, tile, mergeCache) { for (var i = 0; i < layer.length; i++) { var feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]); var geometry = feature.geometry; - if (layers.length > 1) { - feature.properties.vt_layer = layerID; - } // Treat all Polygons as MultiPolygons if (geometry.type === 'Polygon') { @@ -59,9 +56,10 @@ function vtToGeoJSON(data, tile, mergeCache) { if (!feature.geometry.coordinates[0].length) continue; // not actually on this tile } - // force some unique id generation + // Generate some unique IDs and add some metadata var featurehash = utilHashcode(stringify(feature)); var propertyhash = utilHashcode(stringify(feature.properties || {})); + feature.__layerID__ = layerID.replace(/[^_a-zA-Z0-9\-]/g, '_'); feature.__featurehash__ = featurehash; feature.__propertyhash__ = propertyhash; features.push(feature); diff --git a/modules/svg/data.js b/modules/svg/data.js index 581da8fc73..8ae1ab4fcb 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -151,6 +151,15 @@ export function svgData(projection, context, dispatch) { } + function featureClasses(d) { + return [ + 'data' + d.__featurehash__, + d.geometry.type, + d.__layerID__ || '' + ].join(' '); + } + + function drawData(selection) { var vtService = getService(); var getPath = svgPath(projection).geojson; @@ -190,7 +199,7 @@ export function svgData(projection, context, dispatch) { // enter/update paths = paths.enter() .append('path') - .attr('class', 'pathdata') + .attr('class', function(d) { return 'pathdata ' + featureClasses(d); }) .merge(paths) .attr('d', getPath); @@ -216,7 +225,7 @@ export function svgData(projection, context, dispatch) { // enter/update labels = labels.enter() .append('text') - .attr('class', textClass) + .attr('class', function(d) { return textClass + ' ' + featureClasses(d); }) .merge(labels) .text(function(d) { return d.properties.desc || d.properties.name; From 7714e88f3ddf2debeecf727627526ae598602793 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 24 Aug 2018 14:07:00 -0400 Subject: [PATCH 15/20] Support data area filling (full/partial/wireframe), shadow strokes --- css/65_data.css | 27 ++++++++++++--- css/70_fills.css | 5 +++ modules/svg/areas.js | 4 +-- modules/svg/data.js | 79 ++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 102 insertions(+), 13 deletions(-) diff --git a/css/65_data.css b/css/65_data.css index fcf5d3a66b..6be709a5f0 100644 --- a/css/65_data.css +++ b/css/65_data.css @@ -59,16 +59,33 @@ pointer-events: none; } -.layer-mapdata path { +.layer-mapdata path.shadow { + stroke: #f6634f; + stroke-width: 16; + stroke-opacity: 0; + fill: none; +} +.layer-mapdata path.shadow.related:not(.selected), +.layer-mapdata path.shadow.hover:not(.selected) { + stroke-opacity: 0.4; +} +.layer-mapdata path.shadow.selected { + stroke-opacity: 0.7; +} + +.layer-mapdata path.stroke { stroke: #ff26d4; stroke-width: 2; fill: none; } -.layer-mapdata path.MultiPolygon, -.layer-mapdata path.Polygon { - stroke-width: 1; + +.layer-mapdata path.fill { + stroke-width: 0; + stroke-opacity: 0.3; + stroke: #ff26d4; fill: #ff26d4; - fill-opacity: 0.2; + fill-opacity: 0.3; + fill-rule: evenodd; } .layer-mapdata text.label-halo, diff --git a/css/70_fills.css b/css/70_fills.css index 5b0996de80..fa6e58cf8a 100644 --- a/css/70_fills.css +++ b/css/70_fills.css @@ -7,6 +7,11 @@ stroke-dasharray: none !important; fill: none !important; } +.low-zoom.fill-wireframe .layer-mapdata path.stroke, +.fill-wireframe .layer-mapdata path.stroke { + stroke-width: 2 !important; + stroke-opacity: 1 !important; +} .low-zoom.fill-wireframe path.shadow, .fill-wireframe path.shadow { diff --git a/modules/svg/areas.js b/modules/svg/areas.js index 01a12de7f3..546919ecdb 100644 --- a/modules/svg/areas.js +++ b/modules/svg/areas.js @@ -132,7 +132,7 @@ export function svgAreas(projection, context) { fill: areas }; - var clipPaths = context.surface().selectAll('defs').selectAll('.clipPath') + var clipPaths = context.surface().selectAll('defs').selectAll('.clipPath-osm') .filter(filter) .data(data.clip, osmEntity.key); @@ -141,7 +141,7 @@ export function svgAreas(projection, context) { var clipPathsEnter = clipPaths.enter() .append('clipPath') - .attr('class', 'clipPath') + .attr('class', 'clipPath-osm') .attr('id', function(entity) { return entity.id + '-clippath'; }); clipPathsEnter diff --git a/modules/svg/data.js b/modules/svg/data.js index 8ae1ab4fcb..80f97b1d35 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -151,6 +151,21 @@ export function svgData(projection, context, dispatch) { } + function featureKey(d) { + return d.__featurehash__; + } + + + function isPolygon(d) { + return d.geometry.type === 'Polygon' || d.geometry.type === 'MultiPolygon'; + } + + + function clipPathID(d) { + return 'data-' + d.__featurehash__ + '-clippath'; + } + + function featureClasses(d) { return [ 'data' + d.__featurehash__, @@ -176,8 +191,12 @@ export function svgData(projection, context, dispatch) { .attr('class', 'layer-mapdata') .merge(layer); + var surface = context.surface(); + if (!surface || surface.empty()) return; // not ready to draw yet, starting up - var geoData; + + // Gather data + var geoData, polygonData; if (_template && vtService) { // fetch data from vector tile service var sourceID = _template; vtService.loadTiles(sourceID, _template, projection); @@ -186,11 +205,50 @@ export function svgData(projection, context, dispatch) { geoData = getFeatures(_geojson); } geoData = geoData.filter(getPath); + polygonData = geoData.filter(isPolygon); + + + // Draw clip paths for polygons + var clipPaths = surface.selectAll('defs').selectAll('.clipPath-data') + .data(polygonData, featureKey); + clipPaths.exit() + .remove(); - var paths = layer + var clipPathsEnter = clipPaths.enter() + .append('clipPath') + .attr('class', 'clipPath-data') + .attr('id', clipPathID); + + clipPathsEnter + .append('path'); + + clipPaths.merge(clipPathsEnter) + .selectAll('path') + .attr('d', getPath); + + + // Draw fill, shadow, stroke layers + var datagroups = layer + .selectAll('g.datagroup') + .data(['fill', 'shadow', 'stroke']); + + datagroups = datagroups.enter() + .append('g') + .attr('class', function(d) { return 'datagroup datagroup-' + d; }) + .merge(datagroups); + + + // Draw paths + var pathData = { + shadow: geoData, + stroke: geoData, + fill: polygonData + }; + + var paths = datagroups .selectAll('path') - .data(geoData, function(d) { return d.__featurehash__; }); + .data(function(layer) { return pathData[layer]; }, featureKey); // exit paths.exit() @@ -199,11 +257,20 @@ export function svgData(projection, context, dispatch) { // enter/update paths = paths.enter() .append('path') - .attr('class', function(d) { return 'pathdata ' + featureClasses(d); }) + .attr('class', function(d) { + var datagroup = this.parentNode.__data__; + var area = (datagroup === 'fill' ? 'area ' : ''); + return 'pathdata ' + area + datagroup + ' ' + featureClasses(d); + }) + .attr('clip-path', function(d) { + var datagroup = this.parentNode.__data__; + return datagroup === 'fill' ? ('url(#' + clipPathID(d) + ')') : null; + }) .merge(paths) .attr('d', getPath); + // Draw labels layer .call(drawLabels, 'label-halo', geoData) .call(drawLabels, 'label', geoData); @@ -216,7 +283,7 @@ export function svgData(projection, context, dispatch) { }); var labels = selection.selectAll('text.' + textClass) - .data(labelData, function(d) { return d.__featurehash__; }); + .data(labelData, featureKey); // exit labels.exit() @@ -312,7 +379,7 @@ export function svgData(projection, context, dispatch) { drawData.hasData = function() { - return !!(_template || _geojson); + return !!(_template || !_isEmpty(_geojson)); }; From a9aca707b61643f3770ff737ecb925059cc334c3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 25 Aug 2018 00:04:52 -0400 Subject: [PATCH 16/20] Support hovering on data features --- css/65_data.css | 12 ++++++++++- modules/behavior/hover.js | 42 ++++++++++++++++++++++----------------- modules/svg/data.js | 10 +++++----- modules/ui/sidebar.js | 26 ++++++++++++------------ 4 files changed, 53 insertions(+), 37 deletions(-) diff --git a/css/65_data.css b/css/65_data.css index 6be709a5f0..1cf8de1236 100644 --- a/css/65_data.css +++ b/css/65_data.css @@ -60,12 +60,18 @@ } .layer-mapdata path.shadow { + pointer-events: stroke; stroke: #f6634f; stroke-width: 16; stroke-opacity: 0; fill: none; } -.layer-mapdata path.shadow.related:not(.selected), +.layer-mapdata path.MultiPoint.shadow, +.layer-mapdata path.Point.shadow { + pointer-events: fill; + fill: #f6634f; + fill-opacity: 0; +} .layer-mapdata path.shadow.hover:not(.selected) { stroke-opacity: 0.4; } @@ -97,6 +103,10 @@ .layer-mapdata text.label { fill: #ff26d4; } +.layer-mapdata text.label.hover, +.layer-mapdata text.label.selected { + fill: #f6634f; +} .layer-mapdata text.label-halo { opacity: 0.7; stroke: #000; diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index d086ac72c3..9b2000e625 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -110,13 +110,32 @@ export function behaviorHover(context) { _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false); - var entity; - if (datum instanceof osmNote || datum instanceof osmEntity) { + // What are we hovering over? + var entity, selector; + if (datum && datum.__featurehash__) { entity = datum; - } else { - entity = datum && datum.properties && datum.properties.entity; + selector = '.data' + datum.__featurehash__; + + } else if (datum instanceof osmNote) { + entity = datum; + selector = '.note-' + datum.id; + + } else if (datum instanceof osmEntity) { + entity = datum; + selector = '.' + entity.id; + if (entity.type === 'relation') { + entity.members.forEach(function(member) { selector += ', .' + member.id; }); + } + + } else if (datum && datum.properties && (datum.properties.entity instanceof osmEntity)) { + entity = datum.properties.entity; + selector = '.' + entity.id; + if (entity.type === 'relation') { + entity.members.forEach(function(member) { selector += ', .' + member.id; }); + } } + // Update hover state and dispatch event if (entity && entity.id !== _newId) { // If drawing a way, don't hover on a node that was just placed. #3974 var mode = context.mode() && context.mode().id; @@ -125,24 +144,11 @@ export function behaviorHover(context) { return; } - var selector = (datum instanceof osmNote) ? 'note-' + entity.id : '.' + entity.id; - - if (entity.type === 'relation') { - entity.members.forEach(function(member) { - selector += ', .' + member.id; - }); - } - var suppressed = _altDisables && d3_event && d3_event.altKey; - _selection.selectAll(selector) .classed(suppressed ? 'hover-suppressed' : 'hover', true); - if (datum instanceof osmNote) { - dispatch.call('hover', this, !suppressed && entity); - } else { - dispatch.call('hover', this, !suppressed && entity.id); - } + dispatch.call('hover', this, !suppressed && entity); } else { dispatch.call('hover', this, null); diff --git a/modules/svg/data.js b/modules/svg/data.js index 80f97b1d35..b902a76482 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -170,8 +170,9 @@ export function svgData(projection, context, dispatch) { return [ 'data' + d.__featurehash__, d.geometry.type, + isPolygon(d) ? 'area' : '', d.__layerID__ || '' - ].join(' '); + ].filter(Boolean).join(' '); } @@ -228,7 +229,7 @@ export function svgData(projection, context, dispatch) { .attr('d', getPath); - // Draw fill, shadow, stroke layers + // Draw fill, shadow, stroke layers var datagroups = layer .selectAll('g.datagroup') .data(['fill', 'shadow', 'stroke']); @@ -239,7 +240,7 @@ export function svgData(projection, context, dispatch) { .merge(datagroups); - // Draw paths + // Draw paths var pathData = { shadow: geoData, stroke: geoData, @@ -259,8 +260,7 @@ export function svgData(projection, context, dispatch) { .append('path') .attr('class', function(d) { var datagroup = this.parentNode.__data__; - var area = (datagroup === 'fill' ? 'area ' : ''); - return 'pathdata ' + area + datagroup + ' ' + featureClasses(d); + return 'pathdata ' + datagroup + ' ' + featureClasses(d); }) .attr('clip-path', function(d) { var datagroup = this.parentNode.__data__; diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js index 02035d9057..cb972eb10e 100644 --- a/modules/ui/sidebar.js +++ b/modules/ui/sidebar.js @@ -2,7 +2,7 @@ import _throttle from 'lodash-es/throttle'; import { selectAll as d3_selectAll } from 'd3-selection'; -import { osmNote } from '../osm'; +import { osmEntity, osmNote } from '../osm'; import { uiFeatureList } from './feature_list'; import { uiInspector } from './inspector'; import { uiNoteEditor } from './note_editor'; @@ -13,7 +13,6 @@ export function uiSidebar(context) { var noteEditor = uiNoteEditor(context); var _current; var _wasNote = false; - // var layer = d3_select(null); function sidebar(selection) { @@ -22,26 +21,27 @@ export function uiSidebar(context) { .attr('class', 'feature-list-pane') .call(uiFeatureList(context)); - var inspectorWrap = selection .append('div') .attr('class', 'inspector-hidden inspector-wrap fr'); - function hover(what) { - if ((what instanceof osmNote) && (context.mode().id !== 'drag-note')) { - // TODO: figure out why `what` isn't an updated note. Won't hover since .loc doesn't match + function hover(datum) { + if (datum && datum.__featurehash__) { // hovering on data + console.log ('hover on data ' + datum.__featurehash__); + // show something + + } else if (datum instanceof osmNote) { + if (context.mode().id === 'drag-note') return; _wasNote = true; - var notes = d3_selectAll('.note'); - notes - .classed('hover', function(d) { return d === what; }); - sidebar.show(noteEditor.note(what)); + sidebar + .show(noteEditor.note(datum)); selection.selectAll('.sidebar-component') .classed('inspector-hover', true); - } else if (!_current && context.hasEntity(what)) { + } else if (!_current && (datum instanceof osmEntity)) { featureListWrap .classed('inspector-hidden', true); @@ -49,10 +49,10 @@ export function uiSidebar(context) { .classed('inspector-hidden', false) .classed('inspector-hover', true); - if (inspector.entityID() !== what || inspector.state() !== 'hover') { + if (inspector.entityID() !== datum.id || inspector.state() !== 'hover') { inspector .state('hover') - .entityID(what); + .entityID(datum.id); inspectorWrap .call(inspector); From 7241e073445458e73b184687cab04a46c3396cfc Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 25 Aug 2018 00:12:21 -0400 Subject: [PATCH 17/20] Return shallow clones of features, because the hashes may change If we return the original feature, and then change the hash later as the feature merges with another, d3 won't exit/enter because it is joining on the new hash, not the originally hash. --- modules/services/vector_tile.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/services/vector_tile.js b/modules/services/vector_tile.js index 1239dd06b8..d34a5181af 100644 --- a/modules/services/vector_tile.js +++ b/modules/services/vector_tile.js @@ -1,3 +1,4 @@ +import _clone from 'lodash-es/clone'; import _find from 'lodash-es/find'; import _isEqual from 'lodash-es/isEqual'; import _forEach from 'lodash-es/forEach'; @@ -172,7 +173,10 @@ export default { var hash = feature.__featurehash__; if (seen[hash]) continue; seen[hash] = true; - results.push(feature); + + // return a shallow clone, because the hash may change + // later if this feature gets merged with another + results.push(_clone(feature)); } } From 0a82ab125e24166dd7470894ac58c813aab5afc8 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 25 Aug 2018 10:10:04 -0400 Subject: [PATCH 18/20] Add uiDataEditor for inspecting custom map data --- css/80_app.css | 57 ++++++++++++++++++++++++++ modules/behavior/hover.js | 6 +-- modules/ui/data_editor.js | 79 ++++++++++++++++++++++++++++++++++++ modules/ui/data_header.js | 47 +++++++++++++++++++++ modules/ui/index.js | 2 + modules/ui/raw_tag_editor.js | 59 +++++++++++++-------------- modules/ui/sidebar.js | 31 ++++++++++---- 7 files changed, 237 insertions(+), 44 deletions(-) create mode 100644 modules/ui/data_editor.js create mode 100644 modules/ui/data_header.js diff --git a/css/80_app.css b/css/80_app.css index 0894a06fdd..958b0e24d6 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -717,6 +717,7 @@ button.add-note svg.icon { } .field-help-title button.close, +.sidebar-component .header button.data-editor-close, .sidebar-component .header button.note-editor-close, .entity-editor-pane .header button.preset-close, .preset-list-pane .header button.preset-choose { @@ -725,6 +726,7 @@ button.add-note svg.icon { top: 0; } [dir='rtl'] .field-help-title button.close, +[dir='rtl'] .sidebar-component .header button.data-editor-close, [dir='rtl'] .sidebar-component .header button.note-editor-close, [dir='rtl'] .entity-editor-pane .header button.preset-close, [dir='rtl'] .preset-list-pane .header button.preset-choose { @@ -1392,6 +1394,10 @@ a.hide-toggle { .inspector-hover .tag-row .form-field.input-wrap-position { width: 50%; } +.inspector-hover .tag-row .key-wrap, +.inspector-hover .tag-row .input-wrap-position { + height: 31px; +} .inspector-hover .tag-row:first-child input.value { border-top-right-radius: 4px; @@ -2541,6 +2547,57 @@ input.key-trap { } +/* Map Data Inspector */ +.data-header { + background-color: #f6f6f6; + border-radius: 5px; + border: 1px solid #ccc; + display: flex; + flex-flow: row nowrap; + align-items: center; +} + +.data-header-icon { + background-color: #fff; + padding: 10px; + flex: 0 0 62px; + position: relative; + width: 60px; + height: 60px; + border-right: 1px solid #ccc; + border-radius: 5px 0 0 5px; +} +[dir='rtl'] .data-header-icon { + border-right: unset; + border-left: 1px solid #ccc; + border-radius: 0 5px 5px 0; +} + +.data-header-icon .icon-wrap { + position: absolute; + top: 0px; +} + +.data-header-label { + background-color: #f6f6f6; + padding: 0 15px; + flex: 1 1 100%; + font-size: 14px; + font-weight: bold; + border-radius: 0 5px 5px 0; +} +[dir='rtl'] .data-header-label { + border-radius: 5px 0 0 5px; +} + +/* tag editor - no buttons */ +.data-editor.raw-tag-editor button { + display: none; +} +.data-editor.raw-tag-editor .tag-row .input-wrap-position { + width: 50%; +} + /* Fullscreen button */ div.full-screen { diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index 9b2000e625..99d12a9dc1 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -6,10 +6,7 @@ import { } from 'd3-selection'; import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; -import { - osmEntity, - osmNote -} from '../osm'; +import { osmEntity, osmNote } from '../osm'; import { utilRebind } from '../util/rebind'; @@ -154,7 +151,6 @@ export function behaviorHover(context) { dispatch.call('hover', this, null); } } - }; diff --git a/modules/ui/data_editor.js b/modules/ui/data_editor.js new file mode 100644 index 0000000000..9adf0a528f --- /dev/null +++ b/modules/ui/data_editor.js @@ -0,0 +1,79 @@ +import { t } from '../util/locale'; +import { modeBrowse } from '../modes'; +import { svgIcon } from '../svg'; + +import { + uiDataHeader, + uiRawTagEditor +} from './index'; + + +export function uiDataEditor(context) { + var dataHeader = uiDataHeader(); + var rawTagEditor = uiRawTagEditor(context); + var _datum; + + + function dataEditor(selection) { + var header = selection.selectAll('.header') + .data([0]); + + var headerEnter = header.enter() + .append('div') + .attr('class', 'header fillL'); + + headerEnter + .append('button') + .attr('class', 'fr data-editor-close') + .on('click', function() { + context.enter(modeBrowse(context)); + }) + .call(svgIcon('#iD-icon-close')); + + headerEnter + .append('h3') + .text(t('map_data.title')); + + + var body = selection.selectAll('.body') + .data([0]); + + body = body.enter() + .append('div') + .attr('class', 'body') + .merge(body); + + var editor = body.selectAll('.data-editor') + .data([0]); + + editor = editor.enter() + .append('div') + .attr('class', 'modal-section data-editor') + .merge(editor) + .call(dataHeader.datum(_datum)); + + var rte = body.selectAll('.raw-tag-editor') + .data([0]); + + rte.enter() + .append('div') + .attr('class', 'inspector-border raw-tag-editor inspector-inner data-editor') + .merge(rte) + .call(rawTagEditor + .expanded(true) + .readOnlyTags([/./]) + .tags((_datum && _datum.properties) || {}) + .state('hover') + ); + } + + + dataEditor.datum = function(val) { + if (!arguments.length) return _datum; + _datum = val; + return this; + }; + + + return dataEditor; +} diff --git a/modules/ui/data_header.js b/modules/ui/data_header.js new file mode 100644 index 0000000000..fec0026afb --- /dev/null +++ b/modules/ui/data_header.js @@ -0,0 +1,47 @@ +import { t } from '../util/locale'; +import { svgIcon } from '../svg'; + + +export function uiDataHeader() { + var _datum; + + + function dataHeader(selection) { + var header = selection.selectAll('.data-header') + .data( + (_datum ? [_datum] : []), + function(d) { return d.__featurehash__; } + ); + + header.exit() + .remove(); + + var headerEnter = header.enter() + .append('div') + .attr('class', 'data-header'); + + var iconEnter = headerEnter + .append('div') + .attr('class', 'data-header-icon'); + + iconEnter + .append('div') + .attr('class', 'preset-icon-28') + .call(svgIcon('#iD-icon-data', 'note-fill')); + + headerEnter + .append('div') + .attr('class', 'data-header-label') + .text(t('map_data.layers.custom.title')); + } + + + dataHeader.datum = function(val) { + if (!arguments.length) return _datum; + _datum = val; + return this; + }; + + + return dataHeader; +} diff --git a/modules/ui/index.js b/modules/ui/index.js index 35d9a85176..3c26415191 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -13,6 +13,8 @@ export { uiConfirm } from './confirm'; export { uiConflicts } from './conflicts'; export { uiContributors } from './contributors'; export { uiCurtain } from './curtain'; +export { uiDataEditor } from './data_editor'; +export { uiDataHeader } from './data_header'; export { uiDisclosure } from './disclosure'; export { uiEditMenu } from './edit_menu'; export { uiEntityEditor } from './entity_editor'; diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js index 85a7f786e6..d5a4fb23a5 100644 --- a/modules/ui/raw_tag_editor.js +++ b/modules/ui/raw_tag_editor.js @@ -24,17 +24,17 @@ import { export function uiRawTagEditor(context) { - var taginfo = services.taginfo, - dispatch = d3_dispatch('change'), - _readOnlyTags = [], - _showBlank = false, - _updatePreference = true, - _expanded = false, - _newRow, - _state, - _preset, - _tags, - _entityID; + var taginfo = services.taginfo; + var dispatch = d3_dispatch('change'); + var _readOnlyTags = []; + var _showBlank = false; + var _updatePreference = true; + var _expanded = false; + var _newRow; + var _state; + var _preset; + var _tags; + var _entityID; function rawTagEditor(selection) { @@ -148,16 +148,16 @@ export function uiRawTagEditor(context) { items .each(function(tag) { - var row = d3_select(this), - key = row.select('input.key'), // propagate bound data to child - value = row.select('input.value'); // propagate bound data to child + var row = d3_select(this); + var key = row.select('input.key'); // propagate bound data to child + var value = row.select('input.value'); // propagate bound data to child if (_entityID && taginfo) { bindTypeahead(key, value); } - var isRelation = (_entityID && context.entity(_entityID).type === 'relation'), - reference; + var isRelation = (_entityID && context.entity(_entityID).type === 'relation'); + var reference; if (isRelation && tag.key === 'type') { reference = uiTagReference({ rtype: tag.value }, context); @@ -239,8 +239,8 @@ export function uiRawTagEditor(context) { function sort(value, data) { - var sameletter = [], - other = []; + var sameletter = []; + var other = []; for (var i = 0; i < data.length; i++) { if (data[i].value.substring(0, value.length) === value) { sameletter.push(data[i]); @@ -265,10 +265,9 @@ export function uiRawTagEditor(context) { function keyChange(d) { - var kOld = d.key, - kNew = this.value.trim(), - tag = {}; - + var kOld = d.key; + var kNew = this.value.trim(); + var tag = {}; if (isReadOnly({ key: kNew })) { this.value = kOld; @@ -276,17 +275,17 @@ export function uiRawTagEditor(context) { } if (kNew && kNew !== kOld) { - var match = kNew.match(/^(.*?)(?:_(\d+))?$/), - base = match[1], - suffix = +(match[2] || 1); + var match = kNew.match(/^(.*?)(?:_(\d+))?$/); + var base = match[1]; + var suffix = +(match[2] || 1); while (_tags[kNew]) { // rename key if already in use kNew = base + '_' + suffix++; } if (_includes(kNew, '=')) { - var splitStr = kNew.split('=').map(function(str) { return str.trim(); }), - key = splitStr[0], - value = splitStr[1]; + var splitStr = kNew.split('=').map(function(str) { return str.trim(); }); + var key = splitStr[0]; + var value = splitStr[1]; kNew = key; d.value = value; @@ -295,9 +294,9 @@ export function uiRawTagEditor(context) { tag[kOld] = undefined; tag[kNew] = d.value; - d.key = kNew; // Maintain DOM identity through the subsequent update. + d.key = kNew; // Maintain DOM identity through the subsequent update. - if (_newRow === kOld) { // see if this row is still a new row + if (_newRow === kOld) { // see if this row is still a new row _newRow = ((d.value === '' || kNew === '') ? kNew : undefined); } diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js index cb972eb10e..33b3df5c03 100644 --- a/modules/ui/sidebar.js +++ b/modules/ui/sidebar.js @@ -2,16 +2,25 @@ import _throttle from 'lodash-es/throttle'; import { selectAll as d3_selectAll } from 'd3-selection'; -import { osmEntity, osmNote } from '../osm'; -import { uiFeatureList } from './feature_list'; -import { uiInspector } from './inspector'; -import { uiNoteEditor } from './note_editor'; +import { + osmEntity, + osmNote +} from '../osm'; + +import { + uiDataEditor, + uiFeatureList, + uiInspector, + uiNoteEditor +} from './index'; export function uiSidebar(context) { var inspector = uiInspector(context); + var dataEditor = uiDataEditor(context); var noteEditor = uiNoteEditor(context); var _current; + var _wasData = false; var _wasNote = false; @@ -28,8 +37,12 @@ export function uiSidebar(context) { function hover(datum) { if (datum && datum.__featurehash__) { // hovering on data - console.log ('hover on data ' + datum.__featurehash__); - // show something + _wasData = true; + sidebar + .show(dataEditor.datum(datum)); + + selection.selectAll('.sidebar-component') + .classed('inspector-hover', true); } else if (datum instanceof osmNote) { if (context.mode().id === 'drag-note') return; @@ -66,10 +79,10 @@ export function uiSidebar(context) { inspector .state('hide'); - } else if (_wasNote) { + } else if (_wasData || _wasNote) { _wasNote = false; - d3_selectAll('.note') - .classed('hover', false); + _wasData = false; + d3_selectAll('.note').classed('hover', false); sidebar.hide(); } } From cc938698e8bf13fdb5fa99255a59a26d327d4f5a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 25 Aug 2018 11:14:04 -0400 Subject: [PATCH 19/20] Add ability to select custom data --- css/65_data.css | 1 + css/80_app.css | 5 +- modules/behavior/select.js | 7 ++- modules/modes/browse.js | 8 +-- modules/modes/index.js | 1 + modules/modes/rotate.js | 8 +-- modules/modes/select.js | 9 +--- modules/modes/select_data.js | 97 ++++++++++++++++++++++++++++++++++++ modules/modes/select_note.js | 75 +++++++++++++--------------- modules/renderer/map.js | 2 +- modules/svg/data.js | 12 +++-- modules/ui/data_editor.js | 2 +- 12 files changed, 158 insertions(+), 69 deletions(-) create mode 100644 modules/modes/select_data.js diff --git a/css/65_data.css b/css/65_data.css index 1cf8de1236..24af906c48 100644 --- a/css/65_data.css +++ b/css/65_data.css @@ -8,6 +8,7 @@ } .mode-browse .layer-notes .note .note-fill, .mode-select .layer-notes .note .note-fill, +.mode-select-data .layer-notes .note .note-fill, .mode-select-note .layer-notes .note .note-fill { pointer-events: visible; cursor: pointer; /* Opera */ diff --git a/css/80_app.css b/css/80_app.css index 958b0e24d6..0f51f871be 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -2594,6 +2594,7 @@ input.key-trap { .data-editor.raw-tag-editor button { display: none; } +.data-editor.raw-tag-editor .tag-row .key-wrap, .data-editor.raw-tag-editor .tag-row .input-wrap-position { width: 50%; } @@ -4060,7 +4061,7 @@ svg.mouseclick use.right { /* Settings Modals ------------------------------------------------------- */ .settings-modal textarea { - height: 60px; + height: 70px; } .settings-modal .buttons .button.col3 { float: none; /* undo float left */ @@ -4071,7 +4072,7 @@ svg.mouseclick use.right { } -.settings-custom-data .instructions-file { +.settings-custom-data .instructions-url { margin-bottom: 10px; } .settings-custom-data .field-file, diff --git a/modules/behavior/select.js b/modules/behavior/select.js index 9e0b3fee96..1dd051e658 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -11,6 +11,7 @@ import { geoVecLength } from '../geo'; import { modeBrowse, modeSelect, + modeSelectData, modeSelectNote } from '../modes'; @@ -157,13 +158,17 @@ export function behaviorSelect(context) { } } + } else if (datum && datum.__featurehash__ && !isMultiselect) { // clicked Data.. + context + .selectedNoteID(null) + .enter(modeSelectData(context, datum)); + } else if (datum instanceof osmNote && !isMultiselect) { // clicked a Note.. context .selectedNoteID(datum.id) .enter(modeSelectNote(context, datum.id)); } else { // clicked nothing.. - context.selectedNoteID(null); if (!isMultiselect && mode.id !== 'browse') { context.enter(modeBrowse(context)); diff --git a/modules/modes/browse.js b/modules/modes/browse.js index 72484df62d..acd9cc6f38 100644 --- a/modules/modes/browse.js +++ b/modules/modes/browse.js @@ -30,9 +30,7 @@ export function modeBrowse(context) { mode.enter = function() { - behaviors.forEach(function(behavior) { - context.install(behavior); - }); + behaviors.forEach(context.install); // Get focus on the body. if (document.activeElement && document.activeElement.blur) { @@ -49,9 +47,7 @@ export function modeBrowse(context) { mode.exit = function() { context.ui().sidebar.hover.cancel(); - behaviors.forEach(function(behavior) { - context.uninstall(behavior); - }); + behaviors.forEach(context.uninstall); if (sidebar) { context.ui().sidebar.hide(); diff --git a/modules/modes/index.js b/modules/modes/index.js index 83838c4e16..af440c4c21 100644 --- a/modules/modes/index.js +++ b/modules/modes/index.js @@ -11,4 +11,5 @@ export { modeMove } from './move'; export { modeRotate } from './rotate'; export { modeSave } from './save'; export { modeSelect } from './select'; +export { modeSelectData } from './select_data'; export { modeSelectNote } from './select_note'; diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index 4addaed887..12cac0fddd 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -120,9 +120,7 @@ export function modeRotate(context, entityIDs) { mode.enter = function() { - behaviors.forEach(function(behavior) { - context.install(behavior); - }); + behaviors.forEach(context.install); context.surface() .on('mousemove.rotate', doRotate) @@ -141,9 +139,7 @@ export function modeRotate(context, entityIDs) { mode.exit = function() { - behaviors.forEach(function(behavior) { - context.uninstall(behavior); - }); + behaviors.forEach(context.uninstall); context.surface() .on('mousemove.rotate', null) diff --git a/modules/modes/select.js b/modules/modes/select.js index 25e12400fa..1d19eccc39 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -451,9 +451,7 @@ export function modeSelect(context, selectedIDs) { } }); - behaviors.forEach(function(behavior) { - context.install(behavior); - }); + behaviors.forEach(context.install); keybinding .on(['[', 'pgup'], previousVertex) @@ -522,10 +520,7 @@ export function modeSelect(context, selectedIDs) { if (timeout) window.clearTimeout(timeout); if (inspector) wrap.call(inspector.close); - behaviors.forEach(function(behavior) { - context.uninstall(behavior); - }); - + behaviors.forEach(context.uninstall); keybinding.off(); closeMenu(); editMenu = undefined; diff --git a/modules/modes/select_data.js b/modules/modes/select_data.js new file mode 100644 index 0000000000..1e433660d4 --- /dev/null +++ b/modules/modes/select_data.js @@ -0,0 +1,97 @@ +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; + +import { + behaviorBreathe, + behaviorHover, + behaviorLasso, + behaviorSelect +} from '../behavior'; + +import { + modeDragNode, + modeDragNote +} from '../modes'; + +import { modeBrowse } from './browse'; +import { uiDataEditor } from '../ui'; + + +export function modeSelectData(context, selectedDatum) { + var mode = { + id: 'select-data', + button: 'browse' + }; + + var keybinding = d3_keybinding('select-data'); + var dataEditor = uiDataEditor(context); + + var behaviors = [ + behaviorBreathe(context), + behaviorHover(context), + behaviorSelect(context), + behaviorLasso(context), + modeDragNode(context).behavior, + modeDragNote(context).behavior + ]; + + + // class the data as selected, or return to browse mode if the data is gone + function selectData(drawn) { + var selection = context.surface().selectAll('.layer-mapdata .data' + selectedDatum.__featurehash__); + + if (selection.empty()) { + // Return to browse mode if selected DOM elements have + // disappeared because the user moved them out of view.. + var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent; + if (drawn && source && (source.type === 'mousemove' || source.type === 'touchmove')) { + context.enter(modeBrowse(context)); + } + } else { + selection.classed('selected', true); + } + } + + + function esc() { + context.enter(modeBrowse(context)); + } + + + mode.enter = function() { + behaviors.forEach(context.install); + keybinding.on('⎋', esc, true); + d3_select(document).call(keybinding); + + selectData(); + + context.ui().sidebar + .show(dataEditor.datum(selectedDatum)); + + context.map() + .on('drawn.select-data', selectData); + }; + + + mode.exit = function() { + behaviors.forEach(context.uninstall); + keybinding.off(); + + context.surface() + .selectAll('.layer-mapdata .selected') + .classed('selected hover', false); + + context.map() + .on('drawn.select-data', null); + + context.ui().sidebar + .hide(); + }; + + + return mode; +} diff --git a/modules/modes/select_note.js b/modules/modes/select_note.js index c2eb8aba01..36246a207e 100644 --- a/modules/modes/select_note.js +++ b/modules/modes/select_note.js @@ -60,52 +60,48 @@ export function modeSelectNote(context, selectedNoteID) { return note; } + + // class the note as selected, or return to browse mode if the note is gone + function selectNote(drawn) { + if (!checkSelectedID()) return; + + var selection = context.surface().selectAll('.layer-notes .note-' + selectedNoteID); + + if (selection.empty()) { + // Return to browse mode if selected DOM elements have + // disappeared because the user moved them out of view.. + var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent; + if (drawn && source && (source.type === 'mousemove' || source.type === 'touchmove')) { + context.enter(modeBrowse(context)); + } + + } else { + selection + .classed('selected', true); + context.selectedNoteID(selectedNoteID); + } + } + + + function esc() { + context.enter(modeBrowse(context)); + } + + mode.newFeature = function(_) { if (!arguments.length) return newFeature; newFeature = _; return mode; }; - mode.enter = function() { - - // class the note as selected, or return to browse mode if the note is gone - function selectNote(drawn) { - if (!checkSelectedID()) return; - - var selection = context.surface() - .selectAll('.note-' + selectedNoteID); - - if (selection.empty()) { - // Return to browse mode if selected DOM elements have - // disappeared because the user moved them out of view.. - var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent; - if (drawn && source && (source.type === 'mousemove' || source.type === 'touchmove')) { - context.enter(modeBrowse(context)); - } - - } else { - selection - .classed('selected', true); - context.selectedNoteID(selectedNoteID); - } - } - - function esc() { - context.enter(modeBrowse(context)); - } + mode.enter = function() { var note = checkSelectedID(); if (!note) return; - behaviors.forEach(function(behavior) { - context.install(behavior); - }); - - keybinding - .on('⎋', esc, true); - - d3_select(document) - .call(keybinding); + behaviors.forEach(context.install); + keybinding.on('⎋', esc, true); + d3_select(document).call(keybinding); selectNote(); @@ -118,14 +114,11 @@ export function modeSelectNote(context, selectedNoteID) { mode.exit = function() { - behaviors.forEach(function(behavior) { - context.uninstall(behavior); - }); - + behaviors.forEach(context.uninstall); keybinding.off(); context.surface() - .selectAll('.note.selected') + .selectAll('.layer-notes .selected') .classed('selected hover', false); context.map() diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 727ae33234..2878d8a487 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -348,7 +348,7 @@ export function rendererMap(context) { surface.selectAll('.layer-osm *').remove(); var mode = context.mode(); - if (mode && mode.id !== 'save' && mode.id !== 'select-note') { + if (mode && mode.id !== 'save' && mode.id !== 'select-note' && mode.id !== 'select-data') { context.enter(modeBrowse(context)); } diff --git a/modules/svg/data.js b/modules/svg/data.js index b902a76482..8be1a551cf 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -179,6 +179,7 @@ export function svgData(projection, context, dispatch) { function drawData(selection) { var vtService = getService(); var getPath = svgPath(projection).geojson; + var getAreaPath = svgPath(projection, null, true).geojson; var hasData = drawData.hasData(); layer = selection.selectAll('.layer-mapdata') @@ -226,7 +227,7 @@ export function svgData(projection, context, dispatch) { clipPaths.merge(clipPathsEnter) .selectAll('path') - .attr('d', getPath); + .attr('d', getAreaPath); // Draw fill, shadow, stroke layers @@ -242,9 +243,9 @@ export function svgData(projection, context, dispatch) { // Draw paths var pathData = { + fill: polygonData, shadow: geoData, - stroke: geoData, - fill: polygonData + stroke: geoData }; var paths = datagroups @@ -267,7 +268,10 @@ export function svgData(projection, context, dispatch) { return datagroup === 'fill' ? ('url(#' + clipPathID(d) + ')') : null; }) .merge(paths) - .attr('d', getPath); + .attr('d', function(d) { + var datagroup = this.parentNode.__data__; + return datagroup === 'fill' ? getAreaPath(d) : getPath(d); + }); // Draw labels diff --git a/modules/ui/data_editor.js b/modules/ui/data_editor.js index 9adf0a528f..593517de6c 100644 --- a/modules/ui/data_editor.js +++ b/modules/ui/data_editor.js @@ -46,7 +46,7 @@ export function uiDataEditor(context) { var editor = body.selectAll('.data-editor') .data([0]); - editor = editor.enter() + editor.enter() .append('div') .attr('class', 'modal-section data-editor') .merge(editor) From 5368aa1972edcbc7a76ca876cc7535ec68a82c4d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 25 Aug 2018 11:30:06 -0400 Subject: [PATCH 20/20] Update data layer tests to check for both shadow and stroke paths --- test/spec/svg/data.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/test/spec/svg/data.js b/test/spec/svg/data.js index 323420756f..4268544096 100644 --- a/test/spec/svg/data.js +++ b/test/spec/svg/data.js @@ -91,7 +91,7 @@ describe('iD.svgData', function () { it('creates layer-mapdata', function () { - var render = iD.svgData(projection, context, dispatch); + var render = iD.svgData(projection, context, dispatch).geojson(gj); surface.call(render); var layers = surface.selectAll('g.layer-mapdata').nodes(); @@ -102,7 +102,11 @@ describe('iD.svgData', function () { var render = iD.svgData(projection, context, dispatch).geojson(gj); surface.call(render); - var path = surface.selectAll('path'); + var path; + path = surface.selectAll('path.shadow'); + expect(path.nodes().length).to.eql(1); + expect(path.attr('d')).to.match(/^M.*z$/); + path = surface.selectAll('path.stroke'); expect(path.nodes().length).to.eql(1); expect(path.attr('d')).to.match(/^M.*z$/); }); @@ -118,7 +122,11 @@ describe('iD.svgData', function () { window.setTimeout(function() { expect(spy).to.have.been.calledOnce; surface.call(render); - var path = surface.selectAll('path'); + var path; + path = surface.selectAll('path.shadow'); + expect(path.nodes().length).to.eql(1); + expect(path.attr('d')).to.match(/^M.*z$/); + path = surface.selectAll('path.stroke'); expect(path.nodes().length).to.eql(1); expect(path.attr('d')).to.match(/^M.*z$/); done(); @@ -135,7 +143,11 @@ describe('iD.svgData', function () { window.setTimeout(function() { expect(spy).to.have.been.calledOnce; surface.call(render); - var path = surface.selectAll('path'); + var path; + path = surface.selectAll('path.shadow'); + expect(path.nodes().length).to.eql(1); + expect(path.attr('d')).to.match(/^M.*z$/); + path = surface.selectAll('path.stroke'); expect(path.nodes().length).to.eql(1); expect(path.attr('d')).to.match(/^M.*z$/); done(); @@ -152,7 +164,11 @@ describe('iD.svgData', function () { window.setTimeout(function() { expect(spy).to.have.been.calledOnce; surface.call(render); - var path = surface.selectAll('path'); + var path; + path = surface.selectAll('path.shadow'); + expect(path.nodes().length).to.eql(1); + expect(path.attr('d')).to.match(/^M.*z$/); + path = surface.selectAll('path.stroke'); expect(path.nodes().length).to.eql(1); expect(path.attr('d')).to.match(/^M.*z$/); done();