diff --git a/css/65_data.css b/css/65_data.css
index 8de9ca09c..837cc9688 100644
--- a/css/65_data.css
+++ b/css/65_data.css
@@ -3,7 +3,8 @@
.error-header-icon .qa_error-fill,
.layer-keepRight .qa_error .qa_error-fill,
-.layer-improveOSM .qa_error .qa_error-fill {
+.layer-improveOSM .qa_error .qa_error-fill,
+.layer-osmose .qa_error .qa_error-fill {
stroke: #333;
stroke-width: 1.3px; /* NOTE: likely a better way to scale the icon stroke */
}
@@ -152,6 +153,11 @@
color: #EC1C24;
}
+/* Osmose Errors
+------------------------------------------------------- */
+.osmose {
+ color: #FFFFFF;
+}
/* Custom Map Data (geojson, gpx, kml, vector tile) */
.layer-mapdata {
@@ -211,4 +217,4 @@
stroke: #000;
stroke-width: 5px;
stroke-miterlimit: 1;
-}
+}
\ No newline at end of file
diff --git a/data/core.yaml b/data/core.yaml
index d68008f9a..e030339e9 100644
--- a/data/core.yaml
+++ b/data/core.yaml
@@ -630,6 +630,9 @@ en:
improveOSM:
tooltip: Missing data automatically detected by improveosm.org
title: ImproveOSM Issues
+ osmose:
+ tooltip: Automatically detected map issues from osmose.openstreetmap.fr
+ title: Osmose Issues
custom:
tooltip: "Drag and drop a data file onto the page, or click the button to setup"
title: Custom Map Data
@@ -1343,7 +1346,7 @@ en:
title: Quality Assurance
intro: "*Quality Assurance* (Q/A) tools can find improper tags, disconnected roads, and other issues with OpenStreetMap, which mappers can then fix. To view existing Q/A issues, click the {data} **Map data** panel to enable a specific Q/A layer."
tools_h: "Tools"
- tools: "The following tools are currently supported: [KeepRight](https://www.keepright.at/) and [ImproveOSM](https://improveosm.org/en/). Expect iD to support [Osmose](https://osmose.openstreetmap.fr/) and more Q/A tools in the future."
+ tools: "The following tools are currently supported: [KeepRight](https://www.keepright.at/), [ImproveOSM](https://improveosm.org/en/) and [Osmose](https://osmose.openstreetmap.fr/). Expect iD to support more Q/A tools in the future."
issues_h: "Handling Issues"
issues: "Handling Q/A issues is similar to handling notes. Click on a marker to view the issue details in the sidebar. Each tool has its own capabilities, but generally you can comment and/or close an issue."
field:
@@ -2058,4 +2061,4 @@ en:
wikidata:
identifier: "Identifier"
label: "Label"
- description: "Description"
+ description: "Description"
\ No newline at end of file
diff --git a/dist/locales/en.json b/dist/locales/en.json
index 1aa7361e5..31a21fdba 100644
--- a/dist/locales/en.json
+++ b/dist/locales/en.json
@@ -782,6 +782,10 @@
"tooltip": "Missing data automatically detected by improveosm.org",
"title": "ImproveOSM Issues"
},
+ "osmose": {
+ "tooltip": "Automatically detected map issues from osmose.openstreetmap.fr",
+ "title": "Osmose Issues"
+ },
"custom": {
"tooltip": "Drag and drop a data file onto the page, or click the button to setup",
"title": "Custom Map Data",
@@ -1653,7 +1657,7 @@
"title": "Quality Assurance",
"intro": "*Quality Assurance* (Q/A) tools can find improper tags, disconnected roads, and other issues with OpenStreetMap, which mappers can then fix. To view existing Q/A issues, click the {data} **Map data** panel to enable a specific Q/A layer.",
"tools_h": "Tools",
- "tools": "The following tools are currently supported: [KeepRight](https://www.keepright.at/) and [ImproveOSM](https://improveosm.org/en/). Expect iD to support [Osmose](https://osmose.openstreetmap.fr/) and more Q/A tools in the future.",
+ "tools": "The following tools are currently supported: [KeepRight](https://www.keepright.at/), [ImproveOSM](https://improveosm.org/en/) and [Osmose](https://osmose.openstreetmap.fr/). Expect iD to support more Q/A tools in the future.",
"issues_h": "Handling Issues",
"issues": "Handling Q/A issues is similar to handling notes. Click on a marker to view the issue details in the sidebar. Each tool has its own capabilities, but generally you can comment and/or close an issue."
},
diff --git a/modules/modes/select_error.js b/modules/modes/select_error.js
index 774822b68..0af8b3acb 100644
--- a/modules/modes/select_error.js
+++ b/modules/modes/select_error.js
@@ -15,6 +15,7 @@ import { modeDragNode } from './drag_node';
import { modeDragNote } from './drag_note';
import { uiImproveOsmEditor } from '../ui/improveOSM_editor';
import { uiKeepRightEditor } from '../ui/keepRight_editor';
+import { uiOsmoseEditor } from '../ui/osmose_editor';
import { utilKeybinding } from '../util';
@@ -49,6 +50,16 @@ export function modeSelectError(context, selectedErrorID, selectedErrorService)
.show(errorEditor.error(error));
});
break;
+ case 'osmose':
+ errorEditor = uiOsmoseEditor(context)
+ .on('change', function() {
+ context.map().pan([0,0]); // trigger a redraw
+ var error = checkSelectedID();
+ if (!error) return;
+ context.ui().sidebar
+ .show(errorEditor.error(error));
+ });
+ break;
}
@@ -154,4 +165,4 @@ export function modeSelectError(context, selectedErrorID, selectedErrorService)
return mode;
-}
+}
\ No newline at end of file
diff --git a/modules/services/index.js b/modules/services/index.js
index 838ed435f..ab9aa5503 100644
--- a/modules/services/index.js
+++ b/modules/services/index.js
@@ -1,5 +1,6 @@
import serviceKeepRight from './keepRight';
import serviceImproveOSM from './improveOSM';
+import serviceOsmose from './osmose';
import serviceMapillary from './mapillary';
import serviceMapRules from './maprules';
import serviceNominatim from './nominatim';
@@ -17,6 +18,7 @@ export var services = {
geocoder: serviceNominatim,
keepRight: serviceKeepRight,
improveOSM: serviceImproveOSM,
+ osmose: serviceOsmose,
mapillary: serviceMapillary,
openstreetcam: serviceOpenstreetcam,
osm: serviceOsm,
@@ -32,6 +34,7 @@ export var services = {
export {
serviceKeepRight,
serviceImproveOSM,
+ serviceOsmose,
serviceMapillary,
serviceMapRules,
serviceNominatim,
@@ -43,4 +46,4 @@ export {
serviceVectorTile,
serviceWikidata,
serviceWikipedia
-};
+};
\ No newline at end of file
diff --git a/modules/services/osmose.js b/modules/services/osmose.js
new file mode 100644
index 000000000..36181b579
--- /dev/null
+++ b/modules/services/osmose.js
@@ -0,0 +1,238 @@
+import RBush from 'rbush';
+
+import { dispatch as d3_dispatch } from 'd3-dispatch';
+import { json as d3_json } from 'd3-fetch';
+
+import { geoExtent, geoVecAdd, geoVecScale } from '../geo';
+import { qaError } from '../osm';
+import { t } from '../util/locale';
+import { utilRebind, utilTiler, utilQsString } from '../util';
+
+
+var tiler = utilTiler();
+var dispatch = d3_dispatch('loaded');
+
+var _erCache;
+var _erZoom = 14;
+
+var _osmoseUrlRoot = 'https://osmose.openstreetmap.fr/en/api/0.3beta/';
+
+function abortRequest(controller) {
+ if (controller) {
+ controller.abort();
+ }
+}
+
+function abortUnwantedRequests(cache, tiles) {
+ Object.keys(cache.inflightTile).forEach(function(k) {
+ var wanted = tiles.find(function(tile) { return k === tile.id; });
+ if (!wanted) {
+ abortRequest(cache.inflightTile[k]);
+ delete cache.inflightTile[k];
+ }
+ });
+}
+
+
+function encodeErrorRtree(d) {
+ return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d };
+}
+
+
+// replace or remove error from rtree
+function updateRtree(item, replace) {
+ _erCache.rtree.remove(item, function isEql(a, b) {
+ return a.data.id === b.data.id;
+ });
+
+ if (replace) {
+ _erCache.rtree.insert(item);
+ }
+}
+
+function linkErrorObject(d) {
+ return '' + d + '';
+}
+
+function linkEntity(d) {
+ return '' + d + '';
+}
+
+// Errors shouldn't obscure eachother
+function preventCoincident(loc, bumpUp) {
+ var coincident = false;
+ do {
+ // first time, move marker up. after that, move marker right.
+ var delta = coincident ? [0.00001, 0] : (bumpUp ? [0, 0.00001] : [0, 0]);
+ loc = geoVecAdd(loc, delta);
+ var bbox = geoExtent(loc).bbox();
+ coincident = _erCache.rtree.search(bbox).length;
+ } while (coincident);
+
+ return loc;
+}
+
+export default {
+ init: function() {
+ if (!_erCache) {
+ this.reset();
+ }
+
+ this.event = utilRebind(this, dispatch, 'on');
+ },
+
+ reset: function() {
+ if (_erCache) {
+ Object.values(_erCache.inflightTile).forEach(abortRequest);
+ }
+ _erCache = {
+ data: {},
+ loadedTile: {},
+ inflightTile: {},
+ inflightPost: {},
+ closed: {},
+ rtree: new RBush()
+ };
+ },
+
+ loadErrors: function(projection) {
+ var options = {
+ full: 'true', // Returns element IDs
+ level: '1,2,3',
+ zoom: '19'
+ };
+
+ // determine the needed tiles to cover the view
+ var tiles = tiler
+ .zoomExtent([_erZoom, _erZoom])
+ .getTiles(projection);
+
+ // abort inflight requests that are no longer needed
+ abortUnwantedRequests(_erCache, tiles);
+
+ // issue new requests..
+ tiles.forEach(function(tile) {
+ if (_erCache.loadedTile[tile.id] || _erCache.inflightTile[tile.id]) return;
+
+ var rect = tile.extent.rectangle(); // E, N, W, S
+ var params = Object.assign({}, options, { bbox: [rect[0], rect[1], rect[2], rect[3]].join() });
+
+ var url = _osmoseUrlRoot + 'issues?' + utilQsString(params);
+
+ var controller = new AbortController();
+ _erCache.inflightTile[tile.id] = controller;
+
+ d3_json(url, { signal: controller.signal })
+ .then(function(data) {
+ delete _erCache.inflightTile[tile.id];
+ _erCache.loadedTile[tile.id] = true;
+
+ if (data.issues) {
+ data.issues.forEach(function(issue) {
+ // Elements provided as string, separated by _ character
+ var elems = issue.elems.split('_');
+ var loc = [issue.lon, issue.lat];
+
+ loc = preventCoincident(loc, true);
+
+ var d = new qaError({
+ // Info required for every error
+ loc: loc,
+ service: 'osmose',
+ error_type: [issue.item, issue.classs].join('-'),
+ // Extra details needed for this service
+ identifier: issue.id, // this is used to post changes to the error
+ elems: elems
+ //object_id: elems[0],
+ //object_type: elems[0].substring(0,1)
+ });
+
+ // Variables used in the description
+ d.replacements = {
+ };
+
+ _erCache.data[d.id] = d;
+ _erCache.rtree.insert(encodeErrorRtree(d));
+ });
+ }
+ })
+ .catch(function() {
+ delete _erCache.inflightTile[tile.id];
+ _erCache.loadedTile[tile.id] = true;
+ });
+ });
+ },
+
+ postUpdate: function(d, callback) {
+ if (_erCache.inflightPost[d.id]) {
+ return callback({ message: 'Error update already inflight', status: -2 }, d);
+ }
+
+ var that = this;
+
+ if (err) { return callback(err, d); }
+
+ // UI sets the status to either '/done' or '/false'
+ var url = _osmoseUrlRoot + 'issue/' + d.identifier + d.newStatus;
+
+ var controller = new AbortController();
+ _erCache.inflightPost[d.id] = controller;
+
+ fetch(url, { method: 'POST', signal: controller.signal })
+ .then(function() {
+ delete _erCache.inflightPost[d.id];
+
+ that.removeError(d);
+ if (d.newStatus === '/done') {
+ // No pretty identifier, so we just use coordinates
+ var closedID = d.loc[1].toFixed(5) + '/' + d.loc[0].toFixed(5);
+ _erCache.closed[key + ':' + closedID] = true;
+ }
+ if (callback) callback(null, d);
+ })
+ .catch(function(err) {
+ delete _erCache.inflightPost[d.id];
+ if (callback) callback(err.message);
+ });
+ },
+
+
+ // get all cached errors covering the viewport
+ getErrors: function(projection) {
+ var viewport = projection.clipExtent();
+ var min = [viewport[0][0], viewport[1][1]];
+ var max = [viewport[1][0], viewport[0][1]];
+ var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();
+
+ return _erCache.rtree.search(bbox).map(function(d) {
+ return d.data;
+ });
+ },
+
+ // get a single error from the cache
+ getError: function(id) {
+ return _erCache.data[id];
+ },
+
+ // replace a single error in the cache
+ replaceError: function(error) {
+ if (!(error instanceof qaError) || !error.id) return;
+
+ _erCache.data[error.id] = error;
+ updateRtree(encodeErrorRtree(error), true); // true = replace
+ return error;
+ },
+
+ // remove a single error from the cache
+ removeError: function(error) {
+ if (!(error instanceof qaError) || !error.id) return;
+
+ delete _erCache.data[error.id];
+ updateRtree(encodeErrorRtree(error), false); // false = remove
+ },
+
+ // Used to populate `closed:osmose` changeset tag
+ getClosedIDs: function() {
+ return Object.keys(_erCache.closed).sort();
+ }
+};
\ No newline at end of file
diff --git a/modules/svg/layers.js b/modules/svg/layers.js
index 12595a7d7..d0c46df53 100644
--- a/modules/svg/layers.js
+++ b/modules/svg/layers.js
@@ -6,6 +6,7 @@ import { svgDebug } from './debug';
import { svgGeolocate } from './geolocate';
import { svgKeepRight } from './keepRight';
import { svgImproveOSM } from './improveOSM';
+import { svgOsmose } from './osmose';
import { svgStreetside } from './streetside';
import { svgMapillaryImages } from './mapillary_images';
import { svgMapillarySigns } from './mapillary_signs';
@@ -27,6 +28,7 @@ export function svgLayers(projection, context) {
{ id: 'data', layer: svgData(projection, context, dispatch) },
{ id: 'keepRight', layer: svgKeepRight(projection, context, dispatch) },
{ id: 'improveOSM', layer: svgImproveOSM(projection, context, dispatch) },
+ { id: 'osmose', layer: svgOsmose(projection, context, dispatch) },
{ id: 'streetside', layer: svgStreetside(projection, context, dispatch)},
{ id: 'mapillary', layer: svgMapillaryImages(projection, context, dispatch) },
{ id: 'mapillary-map-features', layer: svgMapillaryMapFeatures(projection, context, dispatch) },
@@ -116,4 +118,4 @@ export function svgLayers(projection, context) {
return utilRebind(drawLayers, dispatch, 'on');
-}
+}
\ No newline at end of file
diff --git a/modules/svg/osmose.js b/modules/svg/osmose.js
new file mode 100644
index 000000000..1126087d6
--- /dev/null
+++ b/modules/svg/osmose.js
@@ -0,0 +1,261 @@
+import _throttle from 'lodash-es/throttle';
+import { select as d3_select } from 'd3-selection';
+
+import { modeBrowse } from '../modes/browse';
+import { svgPointTransform } from './helpers';
+import { services } from '../services';
+
+var _osmoseEnabled = false;
+var _errorService;
+
+
+export function svgOsmose(projection, context, dispatch) {
+ var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
+ var minZoom = 12;
+ var touchLayer = d3_select(null);
+ var drawLayer = d3_select(null);
+ var _osmoseVisible = false;
+
+ function markerPath(selection, klass) {
+ selection
+ .attr('class', klass)
+ .attr('transform', 'translate(-10, -28)')
+ .attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6');
+ }
+
+
+ // Loosely-coupled osmose service for fetching errors.
+ function getService() {
+ if (services.osmose && !_errorService) {
+ _errorService = services.osmose;
+ _errorService.on('loaded', throttledRedraw);
+ } else if (!services.osmose && _errorService) {
+ _errorService = null;
+ }
+
+ return _errorService;
+ }
+
+
+ // Show the errors
+ function editOn() {
+ if (!_osmoseVisible) {
+ _osmoseVisible = true;
+ drawLayer
+ .style('display', 'block');
+ }
+ }
+
+
+ // Immediately remove the errors and their touch targets
+ function editOff() {
+ if (_osmoseVisible) {
+ _osmoseVisible = false;
+ drawLayer
+ .style('display', 'none');
+ drawLayer.selectAll('.qa_error.osmose')
+ .remove();
+ touchLayer.selectAll('.qa_error.osmose')
+ .remove();
+ }
+ }
+
+
+ // Enable the layer. This shows the errors and transitions them to visible.
+ function layerOn() {
+ editOn();
+
+ drawLayer
+ .style('opacity', 0)
+ .transition()
+ .duration(250)
+ .style('opacity', 1)
+ .on('end interrupt', function () {
+ dispatch.call('change');
+ });
+ }
+
+
+ // Disable the layer. This transitions the layer invisible and then hides the errors.
+ function layerOff() {
+ throttledRedraw.cancel();
+ drawLayer.interrupt();
+ touchLayer.selectAll('.qa_error.osmose')
+ .remove();
+
+ drawLayer
+ .transition()
+ .duration(250)
+ .style('opacity', 0)
+ .on('end interrupt', function () {
+ editOff();
+ dispatch.call('change');
+ });
+ }
+
+
+ // Update the error markers
+ function updateMarkers() {
+ if (!_osmoseVisible || !_osmoseEnabled) return;
+
+ var service = getService();
+ var selectedID = context.selectedErrorID();
+ var data = (service ? service.getErrors(projection) : []);
+ var getTransform = svgPointTransform(projection);
+
+ // Draw markers..
+ var markers = drawLayer.selectAll('.qa_error.osmose')
+ .data(data, function(d) { return d.id; });
+
+ // exit
+ markers.exit()
+ .remove();
+
+ // enter
+ var markersEnter = markers.enter()
+ .append('g')
+ .attr('class', function(d) {
+ return [
+ 'qa_error',
+ d.service,
+ 'error_id-' + d.id,
+ 'error_type-' + d.error_type,
+ 'category-' + d.category
+ ].join(' ');
+ });
+
+ markersEnter
+ .append('polygon')
+ .call(markerPath, 'shadow');
+
+ markersEnter
+ .append('ellipse')
+ .attr('cx', 0)
+ .attr('cy', 0)
+ .attr('rx', 4.5)
+ .attr('ry', 2)
+ .attr('class', 'stroke');
+
+ markersEnter
+ .append('polygon')
+ .attr('fill', 'currentColor')
+ .call(markerPath, 'qa_error-fill');
+
+ markersEnter
+ .append('use')
+ .attr('transform', 'translate(-5.5, -21)')
+ .attr('class', 'icon-annotation')
+ .attr('width', '11px')
+ .attr('height', '11px')
+ .attr('xlink:href', function(d) {
+ var picon = d.icon;
+
+ if (!picon) {
+ return '';
+ } else {
+ var isMaki = /^maki-/.test(picon);
+ return '#' + picon + (isMaki ? '-11' : '');
+ }
+ });
+
+ // update
+ markers
+ .merge(markersEnter)
+ .sort(sortY)
+ .classed('selected', function(d) { return d.id === selectedID; })
+ .attr('transform', getTransform);
+
+
+ // Draw targets..
+ if (touchLayer.empty()) return;
+ var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
+
+ var targets = touchLayer.selectAll('.qa_error.osmose')
+ .data(data, function(d) { return d.id; });
+
+ // exit
+ targets.exit()
+ .remove();
+
+ // enter/update
+ targets.enter()
+ .append('rect')
+ .attr('width', '20px')
+ .attr('height', '30px')
+ .attr('x', '-10px')
+ .attr('y', '-28px')
+ .merge(targets)
+ .sort(sortY)
+ .attr('class', function(d) {
+ return 'qa_error ' + d.service + ' target error_id-' + d.id + ' ' + fillClass;
+ })
+ .attr('transform', getTransform);
+
+
+ function sortY(a, b) {
+ return (a.id === selectedID) ? 1
+ : (b.id === selectedID) ? -1
+ : b.loc[1] - a.loc[1];
+ }
+ }
+
+
+ // Draw the Osmose layer and schedule loading errors and updating markers.
+ function drawOsmose(selection) {
+ var service = getService();
+
+ var surface = context.surface();
+ if (surface && !surface.empty()) {
+ touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');
+ }
+
+ drawLayer = selection.selectAll('.layer-osmose')
+ .data(service ? [0] : []);
+
+ drawLayer.exit()
+ .remove();
+
+ drawLayer = drawLayer.enter()
+ .append('g')
+ .attr('class', 'layer-osmose')
+ .style('display', _osmoseEnabled ? 'block' : 'none')
+ .merge(drawLayer);
+
+ if (_osmoseEnabled) {
+ if (service && ~~context.map().zoom() >= minZoom) {
+ editOn();
+ service.loadErrors(projection);
+ updateMarkers();
+ } else {
+ editOff();
+ }
+ }
+ }
+
+
+ // Toggles the layer on and off
+ drawOsmose.enabled = function(val) {
+ if (!arguments.length) return _osmoseEnabled;
+
+ _osmoseEnabled = val;
+ if (_osmoseEnabled) {
+ layerOn();
+ } else {
+ layerOff();
+ if (context.selectedErrorID()) {
+ context.enter(modeBrowse(context));
+ }
+ }
+
+ dispatch.call('change');
+ return this;
+ };
+
+
+ drawOsmose.supported = function() {
+ return !!getService();
+ };
+
+
+ return drawOsmose;
+}
\ No newline at end of file
diff --git a/modules/ui/commit.js b/modules/ui/commit.js
index 3713705df..6f5e293d8 100644
--- a/modules/ui/commit.js
+++ b/modules/ui/commit.js
@@ -149,6 +149,12 @@ export function uiCommit(context) {
tags['closed:improveosm'] = iOsmClosed.join(';').substr(0, tagCharLimit);
}
}
+ if (services.osmose) {
+ var osmoseClosed = services.osmose.getClosedIDs();
+ if (osmoseClosed.length) {
+ tags['closed:osmose'] = osmoseClosed.join(';').substr(0, 255);
+ }
+ }
// remove existing issue counts
for (var key in tags) {
@@ -585,4 +591,4 @@ export function uiCommit(context) {
return utilRebind(commit, dispatch, 'on');
-}
+}
\ No newline at end of file
diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js
index 2f1fa57bd..d6362a518 100644
--- a/modules/ui/map_data.js
+++ b/modules/ui/map_data.js
@@ -341,7 +341,7 @@ export function uiMapData(context) {
function drawQAItems(selection) {
- var qaKeys = ['keepRight', 'improveOSM'];
+ var qaKeys = ['keepRight', 'improveOSM', 'osmose'];
var qaLayers = layers.all().filter(function(obj) { return qaKeys.indexOf(obj.id) !== -1; });
var ul = selection
@@ -916,4 +916,4 @@ export function uiMapData(context) {
};
return uiMapData;
-}
+}
\ No newline at end of file
diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js
index 09b2b85ce..4a24eaacb 100644
--- a/test/spec/svg/layers.js
+++ b/test/spec/svg/layers.js
@@ -26,20 +26,21 @@ 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(13);
+ expect(nodes.length).to.eql(14);
expect(d3.select(nodes[0]).classed('osm')).to.be.true;
expect(d3.select(nodes[1]).classed('notes')).to.be.true;
expect(d3.select(nodes[2]).classed('data')).to.be.true;
expect(d3.select(nodes[3]).classed('keepRight')).to.be.true;
expect(d3.select(nodes[4]).classed('improveOSM')).to.be.true;
- expect(d3.select(nodes[5]).classed('streetside')).to.be.true;
- expect(d3.select(nodes[6]).classed('mapillary')).to.be.true;
- expect(d3.select(nodes[7]).classed('mapillary-map-features')).to.be.true;
- expect(d3.select(nodes[8]).classed('mapillary-signs')).to.be.true;
- expect(d3.select(nodes[9]).classed('openstreetcam')).to.be.true;
- expect(d3.select(nodes[10]).classed('debug')).to.be.true;
- expect(d3.select(nodes[11]).classed('geolocate')).to.be.true;
- expect(d3.select(nodes[12]).classed('touch')).to.be.true;
+ expect(d3.select(nodes[5]).classed('osmose')).to.be.true;
+ expect(d3.select(nodes[6]).classed('streetside')).to.be.true;
+ expect(d3.select(nodes[7]).classed('mapillary')).to.be.true;
+ expect(d3.select(nodes[8]).classed('mapillary-map-features')).to.be.true;
+ expect(d3.select(nodes[9]).classed('mapillary-signs')).to.be.true;
+ expect(d3.select(nodes[10]).classed('openstreetcam')).to.be.true;
+ expect(d3.select(nodes[11]).classed('debug')).to.be.true;
+ expect(d3.select(nodes[12]).classed('geolocate')).to.be.true;
+ expect(d3.select(nodes[13]).classed('touch')).to.be.true;
});
-});
+});
\ No newline at end of file