WIP merge 'gpx' and 'mvt' layers into single 'data' layer

This commit is contained in:
Bryan Housel
2018-08-17 13:03:07 -04:00
parent 52c3858b48
commit 4b446e9e78
12 changed files with 191 additions and 247 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -4,16 +4,24 @@ import _reduce from 'lodash-es/reduce';
import _union from 'lodash-es/union';
import { geoBounds as d3_geoBounds } from 'd3-geo';
import { text as d3_text } from 'd3-request';
import {
request as d3_request,
text as d3_text
} from 'd3-request';
import {
event as d3_event,
select as d3_select
} from 'd3-selection';
import toGeoJSON from '@mapbox/togeojson';
import vt from '@mapbox/vector-tile';
import Protobuf from 'pbf';
import { geoExtent, geoPolygonIntersectsPolygon } from '../geo';
import { svgPath } from './index';
import { utilDetect } from '../util/detect';
import toGeoJSON from '@mapbox/togeojson';
var _initialized = false;
@@ -21,7 +29,7 @@ var _enabled = false;
var _geojson;
export function svgGpx(projection, context, dispatch) {
export function svgData(projection, context, dispatch) {
var _showLabels = true;
var detected = utilDetect();
var layer;
@@ -42,24 +50,24 @@ export function svgGpx(projection, context, dispatch) {
d3_select('body')
.attr('dropzone', 'copy')
.on('drop.localgpx', function() {
.on('drop.svgData', function() {
d3_event.stopPropagation();
d3_event.preventDefault();
if (!detected.filedrop) return;
drawGpx.files(d3_event.dataTransfer.files);
drawData.files(d3_event.dataTransfer.files);
})
.on('dragenter.localgpx', over)
.on('dragexit.localgpx', over)
.on('dragover.localgpx', over);
.on('dragenter.svgData', over)
.on('dragexit.svgData', over)
.on('dragover.svgData', over);
_initialized = true;
}
function drawGpx(selection) {
function drawData(selection) {
var getPath = svgPath(projection).geojson;
layer = selection.selectAll('.layer-gpx')
layer = selection.selectAll('.layer-geojson')
.data(_enabled ? [0] : []);
layer.exit()
@@ -67,7 +75,7 @@ export function svgGpx(projection, context, dispatch) {
layer = layer.enter()
.append('g')
.attr('class', 'layer-gpx')
.attr('class', 'layer-geojson')
.merge(layer);
@@ -80,7 +88,7 @@ export function svgGpx(projection, context, dispatch) {
paths = paths.enter()
.append('path')
.attr('class', 'gpx')
.attr('class', 'pathdata')
.merge(paths);
paths
@@ -91,8 +99,8 @@ export function svgGpx(projection, context, dispatch) {
labelData = labelData.filter(getPath);
layer
.call(drawLabels, 'gpxlabel-halo', labelData)
.call(drawLabels, 'gpxlabel', labelData);
.call(drawLabels, 'label-halo', labelData)
.call(drawLabels, 'label', labelData);
function drawLabels(selection, textClass, data) {
@@ -126,11 +134,6 @@ export function svgGpx(projection, context, dispatch) {
}
function toDom(x) {
return (new DOMParser()).parseFromString(x, 'text/xml');
}
function getExtension(fileName) {
if (fileName === undefined) {
return '';
@@ -145,43 +148,76 @@ export function svgGpx(projection, context, dispatch) {
}
function parseSaveAndZoom(extension, data, src) {
function toDom(textdata) {
return (new DOMParser()).parseFromString(textdata, 'text/xml');
}
function vtToGeoJSON(bufferdata) {
var tile = new vt.VectorTile(new Protobuf(bufferdata.data.response));
var layers = Object.keys(tile.layers);
if (!Array.isArray(layers)) { layers = [layers]; }
var collection = {type: 'FeatureCollection', features: []};
layers.forEach(function (layerID) {
var layer = tile.layers[layerID];
if (layer) {
for (var i = 0; i < layer.length; i++) {
var feature = layer.feature(i).toGeoJSON(bufferdata.zxy[2], bufferdata.zxy[3], bufferdata.zxy[1]);
if (layers.length > 1) feature.properties.vt_layer = layerID;
collection.features.push(feature);
}
}
});
return collection;
}
function parseSaveAndZoom(extension, data, name) {
switch (extension) {
default:
drawGpx.geojson(toGeoJSON.gpx(toDom(data)), src).fitZoom();
case '.gpx':
drawData.geojson(toGeoJSON.gpx(toDom(data)), name).fitZoom();
break;
case '.kml':
drawGpx.geojson(toGeoJSON.kml(toDom(data)), src).fitZoom();
drawData.geojson(toGeoJSON.kml(toDom(data)), name).fitZoom();
break;
case '.pbf':
drawData.geojson(vtToGeoJSON(data), name).fitZoom();
break;
case '.mvt':
drawData.geojson(vtToGeoJSON(data), name).fitZoom();
break;
case '.geojson':
case '.json':
drawGpx.geojson(JSON.parse(data), src).fitZoom();
drawData.geojson(JSON.parse(data), name).fitZoom();
break;
}
}
drawGpx.showLabels = function(_) {
drawData.showLabels = function(val) {
if (!arguments.length) return _showLabels;
_showLabels = _;
_showLabels = val;
return this;
};
drawGpx.enabled = function(_) {
drawData.enabled = function(val) {
if (!arguments.length) return _enabled;
_enabled = _;
_enabled = val;
dispatch.call('change');
return this;
};
drawGpx.hasGpx = function() {
drawData.hasData = function() {
return (!(_isEmpty(_geojson) || _isEmpty(_geojson.features)));
};
drawGpx.geojson = function(gj, src) {
drawData.geojson = function(gj, src) {
if (!arguments.length) return _geojson;
if (_isEmpty(gj) || _isEmpty(gj.features)) return this;
_geojson = gj;
@@ -191,41 +227,82 @@ export function svgGpx(projection, context, dispatch) {
};
drawGpx.url = function(url) {
d3_text(url, function(err, data) {
if (!err) {
var extension = getExtension(url);
parseSaveAndZoom(extension, data, url);
}
});
drawData.url = function(url) {
var extension = getExtension(url);
if (extension === 'mvt' || extension === 'pbf') {
d3_request(url)
.responseType('arraybuffer')
.get(function(err, data) {
if (err || !data) return;
_src = url;
var match = url.match(/(pbf|mvt)/i);
var extension = match ? ('.' + match[0].toLowerCase()) : '';
var zxy = url.match(/\/(\d+)\/(\d+)\/(\d+)/);
var bufferdata = {
data : data,
zxy : zxy
};
parseSaveAndZoom(extension, bufferdata);
});
} else {
d3_text(url, function(err, data) {
if (!err) {
parseSaveAndZoom(extension, data, url);
}
});
}
return this;
};
drawGpx.files = function(fileList) {
drawData.files = function(fileList) {
if (!fileList.length) return this;
var f = fileList[0];
var reader = new FileReader();
var extension = getExtension(f.name);
reader.onload = (function(file) {
var extension = getExtension(file.name);
return function (e) {
parseSaveAndZoom(extension, e.target.result, file.name);
};
})(f);
if (extension === 'mvt' || extension === 'pbf') {
reader.onload = (function(file) {
return; // todo find x,y,z
var data = [];
var zxy = [0,0,0];
_src = file.name;
var extension = getExtension(file.name);
var bufferdata = {
data: data,
zxy: zxy
};
return function (e) {
bufferdata.data = e.target.result;
parseSaveAndZoom(extension, bufferdata);
};
})(f);
reader.readAsArrayBuffer(f);
} else {
reader.onload = (function(file) {
return function (e) {
parseSaveAndZoom(extension, e.target.result, file.name);
};
})(f);
reader.readAsText(f);
}
reader.readAsText(f);
return this;
};
drawGpx.getSrc = function () {
drawData.getSrc = function() {
return _src;
};
drawGpx.fitZoom = function() {
if (!this.hasGpx()) return this;
drawData.fitZoom = function() {
if (!this.hasData()) return this;
var map = context.map();
var viewport = map.trimmedExtent().polygon();
@@ -262,5 +339,5 @@ export function svgGpx(projection, context, dispatch) {
init();
return drawGpx;
return drawData;
}

View File

@@ -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';

View File

@@ -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) },

View File

@@ -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);

View File

@@ -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);

View File

@@ -115,12 +115,11 @@
<script src='spec/services/taginfo.js'></script>
<script src='spec/svg/areas.js'></script>
<script src='spec/svg/gpx.js'></script>
<script src='spec/svg/data.js'></script>
<script src='spec/svg/icon.js'></script>
<script src='spec/svg/layers.js'></script>
<script src='spec/svg/lines.js'></script>
<script src='spec/svg/midpoints.js'></script>
<script src='spec/svg/mvt.js'></script>
<script src='spec/svg/osm.js'></script>
<script src='spec/svg/points.js'></script>
<script src='spec/svg/svg.js'></script>
@@ -149,4 +148,4 @@
</script>
</body>
</html>
</html>

View File

@@ -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;
});
});

View File

@@ -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;
});
});