Merge branch 'notes'

This commit is contained in:
Bryan Housel
2018-07-16 16:59:35 -04:00
35 changed files with 1934 additions and 332 deletions

2
.gitignore vendored
View File

@@ -19,3 +19,5 @@ land.html
/css/img
/test/css
/test/img
\.vscode/

View File

@@ -352,7 +352,7 @@ Drawing is then accomplished with
.merge(footer);
footer
.call(uiViewOnOSM(context).entityID(entityID));
.call(uiViewOnOSM(context).what(entity));
```
Some components are reconfigurable, and some provide functionality beyond

162
css/65_data.css Normal file
View File

@@ -0,0 +1,162 @@
/* OSM Notes Layer */
.layer-notes {
pointer-events: none;
}
.layer-notes .note * {
pointer-events: none;
}
.layer-notes .note .note-fill {
pointer-events: visible;
cursor: pointer; /* Opera */
cursor: url(img/cursor-select-point.png), pointer; /* FF */
}
.note-header-icon .note-shadow,
.layer-notes .note .note-shadow {
color: #000;
}
.note-header-icon .note-fill,
.layer-notes .note .note-fill {
color: #ff3300;
stroke: #333;
}
.note-header-icon.closed .note-fill,
.layer-notes .note.closed .note-fill {
color: #55dd00;
stroke: #333;
}
.layer-notes .note.hovered .note-fill {
color: #eebb00;
stroke: #333;
}
.layer-notes .note.selected .note-fill {
color: #ffee00;
stroke: #333;
}
/* slight adjustments to preset icon for note icons */
.note-header-icon .preset-icon-28 {
top: 18px;
}
.note-header-icon .note-icon-annotation {
position: absolute;
top: 21px;
left: 21px;
margin: auto;
}
.note-header-icon .note-icon-annotation .icon {
width: 18px;
height: 18px;
}
/* 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;
}
.comments-container {
background: #ececec;
padding: 1px 10px;
margin: 10px 0;
border-radius: 8px;
}
.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;
flex: 1 1 100%;
flex-flow: column nowrap;
overflow: hidden;
overflow-wrap: break-word;
}
.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;
}
#new-comment-input {
width: 100%;
height: 100px;
max-height: 300px;
min-height: 100px;
}
.note-report {
float: right;
}

View File

@@ -689,6 +689,7 @@ button.save.has-count .count::before {
}
.field-help-title button.close,
.sidebar-component .header button.note-editor-close,
.entity-editor-pane .header button.preset-close,
.preset-list-pane .header button.preset-choose {
position: absolute;
@@ -696,6 +697,7 @@ button.save.has-count .count::before {
top: 0;
}
[dir='rtl'] .field-help-title button.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 {
left: 0;
@@ -733,11 +735,21 @@ button.save.has-count .count::before {
.footer {
position: absolute;
bottom: 0;
margin: 0;
padding: 5px 20px 5px 20px;
border-top: 1px solid #ccc;
background-color: #fafafa;
width: 100%;
z-index: 1;
flex-wrap: wrap;
justify-content: space-between;
list-style: none;
display: flex;
}
.footer > a {
justify-content: center;
}
.sidebar-component .body {
@@ -1290,6 +1302,7 @@ a.hide-toggle {
.inspector-hover .preset-input-wrap .label,
.inspector-hover .form-field-multicombo,
.inspector-hover .structure-extras-wrap,
.inspector-hover .comments-container .comment,
.inspector-hover input,
.inspector-hover textarea,
.inspector-hover label {
@@ -1329,14 +1342,14 @@ a.hide-toggle {
/* hide but preserve in layout */
.inspector-hover .entity-editor-pane button.minor,
.inspector-hover .combobox-caret,
.inspector-hover .entity-editor-pane .header button,
.inspector-hover .header button,
.inspector-hover .spin-control,
.inspector-hover .form-field-multicombo .chips .remove,
.inspector-hover .hide-toggle:before,
.inspector-hover .more-fields,
.inspector-hover .form-label-button-wrap,
.inspector-hover .tag-reference-button,
.inspector-hover .view-on-osm {
.inspector-hover .footer * {
opacity: 0;
}
@@ -3564,10 +3577,17 @@ img.tile-debug {
vertical-align: middle;
}
.save-section .buttons {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
margin-bottom: 30px;
}
.save-section .buttons .action,
.save-section .buttons .secondary-action {
display: inline-block;
margin: 0 20px 0 0;
width: 45%;
margin: 10px auto;
text-align: center;
vertical-align: middle;
}

View File

@@ -441,6 +441,9 @@ en:
osm:
tooltip: Map data from OpenStreetMap
title: OpenStreetMap data
notes:
tooltip: Note data from OpenStreetMap
title: OpenStreetMap notes
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."
@@ -613,6 +616,20 @@ en:
title: "Photo Overlay (OpenStreetCam)"
openstreetcam:
view_on_openstreetcam: "View this image on OpenStreetCam"
note:
note: Note
title: Edit note
anonymous: anonymous
closed: "(Closed)"
commentTitle: Comments
newComment: New Comment
inputPlaceholder: Enter a comment to share with other users.
close: Close Note
open: Reopen Note
comment: Comment
close_comment: Close and Comment
open_comment: Reopen and Comment
report: Report
help:
title: Help
key: H

19
dist/locales/en.json vendored
View File

@@ -536,6 +536,10 @@
"osm": {
"tooltip": "Map data from OpenStreetMap",
"title": "OpenStreetMap data"
},
"notes": {
"tooltip": "Note data from OpenStreetMap",
"title": "OpenStreetMap notes"
}
},
"fill_area": "Fill Areas",
@@ -746,6 +750,21 @@
"openstreetcam": {
"view_on_openstreetcam": "View this image on OpenStreetCam"
},
"note": {
"note": "Note",
"title": "Edit note",
"anonymous": "anonymous",
"closed": "(Closed)",
"commentTitle": "Comments",
"newComment": "New Comment",
"inputPlaceholder": "Enter a comment to share with other users.",
"close": "Close Note",
"open": "Reopen Note",
"comment": "Comment",
"close_comment": "Close and Comment",
"open_comment": "Reopen and Comment",
"report": "Report"
},
"help": {
"title": "Help",
"key": "H",

View File

@@ -6,7 +6,10 @@ import {
} from 'd3-selection';
import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';
import { osmEntity } from '../osm';
import {
osmEntity,
osmNote
} from '../osm';
import { utilRebind } from '../util/rebind';
@@ -108,7 +111,7 @@ export function behaviorHover(context) {
.classed('hover-suppressed', false);
var entity;
if (datum instanceof osmEntity) {
if (datum instanceof osmNote || datum instanceof osmEntity) {
entity = datum;
} else {
entity = datum && datum.properties && datum.properties.entity;
@@ -122,7 +125,7 @@ export function behaviorHover(context) {
return;
}
var selector = '.' + entity.id;
var selector = (datum instanceof osmNote) ? 'note-' + entity.id : '.' + entity.id;
if (entity.type === 'relation') {
entity.members.forEach(function(member) {
@@ -135,7 +138,11 @@ export function behaviorHover(context) {
_selection.selectAll(selector)
.classed(suppressed ? 'hover-suppressed' : 'hover', true);
dispatch.call('hover', this, !suppressed && entity.id);
if (datum instanceof osmNote) {
dispatch.call('hover', this, !suppressed && entity);
} else {
dispatch.call('hover', this, !suppressed && entity.id);
}
} else {
dispatch.call('hover', this, null);

View File

@@ -10,10 +10,14 @@ import { geoVecLength } from '../geo';
import {
modeBrowse,
modeSelect
modeSelect,
modeSelectNote
} from '../modes';
import { osmEntity } from '../osm';
import {
osmEntity,
osmNote
} from '../osm';
export function behaviorSelect(context) {
@@ -122,15 +126,9 @@ export function behaviorSelect(context) {
datum = datum.parents[0];
}
if (!(datum instanceof osmEntity)) {
// clicked nothing..
if (!isMultiselect && mode.id !== 'browse') {
context.enter(modeBrowse(context));
}
} else {
// clicked an entity..
if (datum instanceof osmEntity) { // clicked an entity..
var selectedIDs = context.selectedIDs();
context.selectedNoteID(null);
if (!isMultiselect) {
if (selectedIDs.length > 1 && (!suppressMenu && !isShowAlways)) {
@@ -158,6 +156,18 @@ export function behaviorSelect(context) {
context.enter(modeSelect(context, selectedIDs).suppressMenu(suppressMenu));
}
}
} 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));
}
}
// reset for next time..

View File

@@ -255,10 +255,18 @@ export function coreContext() {
return [];
}
};
context.activeID = function() {
return mode && mode.activeID && mode.activeID();
};
var _selectedNoteID;
context.selectedNoteID = function(noteID) {
if (!arguments.length) return _selectedNoteID;
_selectedNoteID = noteID;
return context;
};
/* Behaviors */
context.install = function(behavior) {

View File

@@ -9,3 +9,4 @@ export { modeMove } from './move';
export { modeRotate } from './rotate';
export { modeSave } from './save';
export { modeSelect } from './select';
export { modeSelectNote } from './select_note';

View File

@@ -513,7 +513,6 @@ export function modeSelect(context, selectedIDs) {
showMenu();
}
}, 270); /* after any centerEase completes */
};

View File

@@ -0,0 +1,123 @@
import {
event as d3_event,
select as d3_select
} from 'd3-selection';
import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';
import {
behaviorHover,
behaviorLasso,
behaviorSelect
} from '../behavior';
import { services } from '../services';
import { modeBrowse } from './browse';
import { uiNoteEditor } from '../ui';
export function modeSelectNote(context, selectedNoteID) {
var mode = {
id: 'select_note',
button: 'browse'
};
var osm = services.osm;
var keybinding = d3_keybinding('select-note');
var noteEditor = uiNoteEditor(context)
.on('change', function() {
context.map().pan([0,0]); // trigger a redraw
var note = checkSelectedID();
if (!note) return;
context.ui().sidebar
.show(noteEditor.note(note));
});
var behaviors = [
behaviorHover(context),
behaviorSelect(context),
behaviorLasso(context),
];
function checkSelectedID() {
if (!osm) return;
var note = osm.getNote(selectedNoteID);
if (!note) {
context.enter(modeBrowse(context));
}
return note;
}
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);
}
}
function esc() {
context.enter(modeBrowse(context));
}
var note = checkSelectedID();
if (!note) return;
behaviors.forEach(function(behavior) {
context.install(behavior);
});
keybinding
.on('⎋', esc, true);
d3_select(document)
.call(keybinding);
context.ui().sidebar
.show(noteEditor.note(note));
context.map()
.on('drawn.select', selectNote);
selectNote();
};
mode.exit = function() {
behaviors.forEach(function(behavior) {
context.uninstall(behavior);
});
keybinding.off();
context.surface()
.selectAll('.note.selected')
.classed('selected hovered', false);
context.map()
.on('drawn.select', null);
context.ui().sidebar
.hide();
};
return mode;
}

View File

@@ -1,6 +1,7 @@
export { osmChangeset } from './changeset';
export { osmEntity } from './entity';
export { osmNode } from './node';
export { osmNote } from './note';
export { osmRelation } from './relation';
export { osmWay } from './way';

60
modules/osm/note.js Normal file
View File

@@ -0,0 +1,60 @@
import _extend from 'lodash-es/extend';
import { geoExtent } from '../geo';
export function osmNote() {
if (!(this instanceof osmNote)) {
return (new osmNote()).initialize(arguments);
} else if (arguments.length) {
this.initialize(arguments);
}
}
osmNote.id = function() {
return osmNote.id.next--;
};
osmNote.id.next = -1;
_extend(osmNote.prototype, {
type: 'note',
initialize: function(sources) {
for (var i = 0; i < sources.length; ++i) {
var source = sources[i];
for (var prop in source) {
if (Object.prototype.hasOwnProperty.call(source, prop)) {
if (source[prop] === undefined) {
delete this[prop];
} else {
this[prop] = source[prop];
}
}
}
}
if (!this.id) {
this.id = osmNote.id();
}
return this;
},
extent: function() {
return new geoExtent(this.loc);
},
update: function(attrs) {
return osmNote(this, attrs, {v: 1 + (this.v || 0)});
},
isNew: function() {
return this.id < 0;
}
});

View File

@@ -350,7 +350,7 @@ export function rendererMap(context) {
surface.selectAll('.layer-osm *').remove();
var mode = context.mode();
if (mode && mode.id !== 'save') {
if (mode && mode.id !== 'save' && mode.id !== 'select_note') {
context.enter(modeBrowse(context));
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@ export { svgLines } from './lines.js';
export { svgMapillaryImages } from './mapillary_images.js';
export { svgMapillarySigns } from './mapillary_signs.js';
export { svgMidpoints } from './midpoints.js';
export { svgNotes } from './notes.js';
export { svgOneWaySegments } from './helpers.js';
export { svgOpenstreetcamImages } from './openstreetcam_images.js';
export { svgOsm } from './osm.js';

View File

@@ -15,6 +15,7 @@ import { svgMapillaryImages } from './mapillary_images';
import { svgMapillarySigns } from './mapillary_signs';
import { svgOpenstreetcamImages } from './openstreetcam_images';
import { svgOsm } from './osm';
import { svgNotes } from './notes';
import { utilRebind } from '../util/rebind';
import { utilGetDimensions, utilSetDimensions } from '../util/dimensions';
@@ -24,6 +25,7 @@ export function svgLayers(projection, context) {
var svg = d3_select(null);
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: 'streetside', layer: svgStreetside(projection, context, dispatch)},

173
modules/svg/notes.js Normal file
View File

@@ -0,0 +1,173 @@
import _throttle from 'lodash-es/throttle';
import { select as d3_select } from 'd3-selection';
import { svgPointTransform } from './index';
import { services } from '../services';
export function svgNotes(projection, context, dispatch) {
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
var minZoom = 12;
var layer = d3_select(null);
var _notes;
function init() {
if (svgNotes.initialized) return; // run once
svgNotes.enabled = false;
svgNotes.initialized = true;
}
function editOn() {
layer.style('display', 'block');
}
function editOff() {
layer.selectAll('.note').remove();
layer.style('display', 'none');
}
function getService() {
if (services.osm && !_notes) {
_notes = services.osm;
_notes.on('loadedNotes', throttledRedraw);
} else if (!services.osm && _notes) {
_notes = null;
}
return _notes;
}
function showLayer() {
editOn();
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', editOff);
}
function update() {
var service = getService();
var selectedID = context.selectedNoteID();
var data = (service ? service.notes(projection) : []);
var transform = svgPointTransform(projection);
var notes = layer.selectAll('.note')
.data(data, function(d) { return d.status + d.id; });
// exit
notes.exit()
.remove();
// enter
var notesEnter = notes.enter()
.append('g')
.attr('class', function(d) { return 'note note-' + d.id + ' ' + d.status; });
// notesEnter
// .append('use')
// .attr('class', 'note-shadow')
// .attr('width', '24px')
// .attr('height', '24px')
// .attr('x', '-12px')
// .attr('y', '-24px')
// .attr('xlink:href', '#iD-icon-note');
notesEnter
.append('use')
.attr('class', 'note-fill')
.attr('width', '20px')
.attr('height', '20px')
.attr('x', '-10px')
.attr('y', '-22px')
.attr('xlink:href', '#iD-icon-note');
// add dots if there's a comment thread
notesEnter.selectAll('.note-annotation')
.data(function(d) { return d.comments.length > 1 ? [0] : []; })
.enter()
.append('use')
.attr('class', 'note-annotation thread')
.attr('width', '14px')
.attr('height', '14px')
.attr('x', '-7px')
.attr('y', '-20px')
.attr('xlink:href', '#iD-icon-more');
// update
notes
.merge(notesEnter)
.sort(function(a, b) {
return (a.id === selectedID) ? 1
: (b.id === selectedID) ? -1
: b.loc[1] - a.loc[1]; // sort Y
})
.classed('selected', function(d) { return d.id === selectedID; })
.attr('transform', transform);
}
function drawNotes(selection) {
var enabled = svgNotes.enabled;
var service = getService();
layer = selection.selectAll('.layer-notes')
.data(service ? [0] : []);
layer.exit()
.remove();
layer.enter()
.append('g')
.attr('class', 'layer-notes')
.style('display', enabled ? 'block' : 'none')
.merge(layer);
function dimensions() {
return [window.innerWidth, window.innerHeight];
}
if (enabled) {
if (service && ~~context.map().zoom() >= minZoom) {
editOn();
service.loadNotes(projection, dimensions());
update();
} else {
editOff();
}
}
}
drawNotes.enabled = function(_) {
if (!arguments.length) return svgNotes.enabled;
svgNotes.enabled = _;
if (svgNotes.enabled) {
showLayer();
} else {
hideLayer();
}
dispatch.call('change');
return this;
};
init();
return drawNotes;
}

View File

@@ -220,14 +220,14 @@ export function uiCommit(context) {
buttonEnter
.append('button')
.attr('class', 'secondary-action col5 button cancel-button')
.attr('class', 'secondary-action button cancel-button')
.append('span')
.attr('class', 'label')
.text(t('commit.cancel'));
buttonEnter
.append('button')
.attr('class', 'action col5 button save-button')
.attr('class', 'action button save-button')
.append('span')
.attr('class', 'label')
.text(t('commit.save'));

View File

@@ -34,6 +34,10 @@ export { uiMapInMap } from './map_in_map';
export { uiModal } from './modal';
export { uiModes } from './modes';
export { uiNotice } from './notice';
export { uiNoteComments } from './note_comments';
export { uiNoteEditor } from './note_editor';
export { uiNoteHeader } from './note_header';
export { uiNoteReport } from './note_report';
export { uiPresetEditor } from './preset_editor';
export { uiPresetIcon } from './preset_icon';
export { uiPresetList } from './preset_list';

View File

@@ -44,11 +44,12 @@ export function uiInspector(context) {
var presetPane = wrap.selectAll('.preset-list-pane');
var editorPane = wrap.selectAll('.entity-editor-pane');
var graph = context.graph(),
entity = context.entity(_entityID),
showEditor = _state === 'hover' ||
entity.isUsed(graph) ||
entity.isHighwayIntersection(graph);
var graph = context.graph();
var entity = context.entity(_entityID);
var showEditor = _state === 'hover' ||
entity.isUsed(graph) ||
entity.isHighwayIntersection(graph);
if (showEditor) {
wrap.style('right', '0%');
@@ -67,7 +68,9 @@ export function uiInspector(context) {
.merge(footer);
footer
.call(uiViewOnOSM(context).entityID(_entityID));
.call(uiViewOnOSM(context)
.what(context.hasEntity(_entityID))
);
function showList(preset) {

View File

@@ -71,7 +71,7 @@ export function uiIntro(context) {
var background = context.background().baseLayerSource();
var overlays = context.background().overlayLayerSources();
var opacity = d3_selectAll('#map .layer-background').style('opacity');
var loadedTiles = osm && osm.loadedTiles();
var caches = osm && osm.caches();
var baseEntities = context.history().graph().base().entities;
var countryCode = services.geocoder.countryCode;
@@ -147,7 +147,7 @@ export function uiIntro(context) {
curtain.remove();
navwrap.remove();
d3_selectAll('#map .layer-background').style('opacity', opacity);
if (osm) { osm.toggle(true).reset().loadedTiles(loadedTiles); }
if (osm) { osm.toggle(true).reset().caches(caches); }
context.history().reset().merge(_values(baseEntities));
context.background().baseLayerSource(background);
overlays.forEach(function (d) { context.background().toggleOverlayLayer(d); });

View File

@@ -86,7 +86,7 @@ export function uiMapData(context) {
function drawPhotoItems(selection) {
var photoKeys = ['streetside','mapillary-images', 'mapillary-signs', 'openstreetcam-images'];
var photoKeys = ['streetside', 'mapillary-images', 'mapillary-signs', 'openstreetcam-images'];
var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; });
var data = photoLayers.filter(function(obj) { return obj.layer.supported(); });
@@ -147,58 +147,64 @@ export function uiMapData(context) {
}
function drawOsmItem(selection) {
var osm = layers.layer('osm'),
showsOsm = osm.enabled();
function drawOsmItems(selection) {
var osmKeys = ['osm', 'notes'];
var osmLayers = layers.all().filter(function(obj) { return osmKeys.indexOf(obj.id) !== -1; });
var ul = selection
.selectAll('.layer-list-osm')
.data(osm ? [0] : []);
.data([0]);
// Exit
ul.exit()
ul = ul.enter()
.append('ul')
.attr('class', 'layer-list layer-list-osm')
.merge(ul);
var li = ul.selectAll('.list-item')
.data(osmLayers);
li.exit()
.remove();
// Enter
var ulEnter = ul.enter()
.append('ul')
.attr('class', 'layer-list layer-list-osm');
var liEnter = ulEnter
var liEnter = li.enter()
.append('li')
.attr('class', 'list-item-osm');
.attr('class', function(d) { return 'list-item list-item-' + d.id; });
var labelEnter = liEnter
.append('label')
.call(tooltip()
.title(t('map_data.layers.osm.tooltip'))
.placement('bottom')
);
.each(function(d) {
d3_select(this)
.call(tooltip()
.title(t('map_data.layers.' + d.id + '.tooltip'))
.placement('bottom')
);
});
labelEnter
.append('input')
.attr('type', 'checkbox')
.on('change', function() { toggleLayer('osm'); });
.on('change', function(d) { toggleLayer(d.id); });
labelEnter
.append('span')
.text(t('map_data.layers.osm.title'));
.text(function(d) { return t('map_data.layers.' + d.id + '.title'); });
// Update
ul = ul
.merge(ulEnter);
li = li
.merge(liEnter);
ul.selectAll('.list-item-osm')
.classed('active', showsOsm)
li
.classed('active', function (d) { return d.layer.enabled(); })
.selectAll('input')
.property('checked', showsOsm);
.property('checked', function (d) { return d.layer.enabled(); });
}
function drawGpxItem(selection) {
var gpx = layers.layer('gpx'),
hasGpx = gpx && gpx.hasGpx(),
showsGpx = hasGpx && gpx.enabled();
var gpx = layers.layer('gpx');
var hasGpx = gpx && gpx.hasGpx();
var showsGpx = hasGpx && gpx.enabled();
var ul = selection
.selectAll('.layer-list-gpx')
@@ -448,7 +454,7 @@ export function uiMapData(context) {
function update() {
_dataLayerContainer
.call(drawOsmItem)
.call(drawOsmItems)
.call(drawPhotoItems)
.call(drawGpxItem);
// .call(drawMvtItem);

116
modules/ui/note_comments.js Normal file
View File

@@ -0,0 +1,116 @@
import { select as d3_select } from 'd3-selection';
import { t } from '../util/locale';
import { svgIcon } from '../svg';
import { services } from '../services';
import { utilDetect } from '../util/detect';
export function uiNoteComments() {
var _note;
function noteComments(selection) {
var comments = selection.selectAll('.comments-container')
.data([0]);
comments = comments.enter()
.append('div')
.attr('class', 'comments-container')
.merge(comments);
var commentEnter = comments.selectAll('.comment')
.data(_note.comments)
.enter()
.append('div')
.attr('class', 'comment');
commentEnter
.append('div')
.attr('class', function(d) { return 'comment-avatar user-' + d.uid; })
.call(svgIcon('#iD-icon-avatar', 'comment-avatar-icon'));
var mainEnter = commentEnter
.append('div')
.attr('class', 'comment-main');
var metadataEnter = mainEnter
.append('div')
.attr('class', 'comment-metadata');
metadataEnter
.append('div')
.attr('class', 'comment-author')
.each(function(d) {
var selection = d3_select(this);
var osm = services.osm;
if (osm && d.user) {
selection = selection
.append('a')
.attr('class', 'comment-author-link')
.attr('href', osm.userURL(d.user))
.attr('tabindex', -1)
.attr('target', '_blank');
}
selection
.text(function(d) { return d.user || t('note.anonymous'); });
});
metadataEnter
.append('div')
.attr('class', 'comment-date')
.text(function(d) { return d.action + ' ' + localeDateString(d.date); });
mainEnter
.append('div')
.attr('class', 'comment-text')
.text(function(d) { return d.text; });
comments
.call(replaceAvatars);
}
function replaceAvatars(selection) {
var osm = services.osm;
if (!osm) return;
var uids = {}; // gather uids in the comment thread
_note.comments.forEach(function(d) {
if (d.uid) uids[d.uid] = true;
});
Object.keys(uids).forEach(function(uid) {
osm.loadUser(uid, function(err, user) {
if (!user || !user.image_url) return;
selection.selectAll('.comment-avatar.user-' + uid)
.html('')
.append('img')
.attr('class', 'icon comment-avatar-icon')
.attr('src', user.image_url)
.attr('alt', user.display_name);
});
});
}
function localeDateString(s) {
if (!s) return null;
var detected = utilDetect();
var options = { day: 'numeric', month: 'short', year: 'numeric' };
var d = new Date(s);
if (isNaN(d.getTime())) return null;
return d.toLocaleDateString(detected.locale, options);
}
noteComments.note = function(_) {
if (!arguments.length) return _note;
_note = _;
return noteComments;
};
return noteComments;
}

201
modules/ui/note_editor.js Normal file
View File

@@ -0,0 +1,201 @@
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { select as d3_select } from 'd3-selection';
import { t } from '../util/locale';
import { services } from '../services';
import { modeBrowse } from '../modes';
import { svgIcon } from '../svg';
import {
uiNoteComments,
uiNoteHeader,
uiNoteReport,
uiViewOnOSM,
} from './index';
import {
utilNoAuto,
utilRebind
} from '../util';
export function uiNoteEditor(context) {
var dispatch = d3_dispatch('change');
var noteComments = uiNoteComments();
var noteHeader = uiNoteHeader();
var _note;
function noteEditor(selection) {
var header = selection.selectAll('.header')
.data([0]);
var headerEnter = header.enter()
.append('div')
.attr('class', 'header fillL');
headerEnter
.append('button')
.attr('class', 'fr note-editor-close')
.on('click', function() { context.enter(modeBrowse(context)); })
.call(svgIcon('#iD-icon-close'));
headerEnter
.append('h3')
.text(t('note.title'));
var body = selection.selectAll('.body')
.data([0]);
body = body.enter()
.append('div')
.attr('class', 'body')
.merge(body);
body.selectAll('.note-editor')
.data([0])
.enter()
.append('div')
.attr('class', 'modal-section note-editor')
.call(noteHeader.note(_note))
.call(noteComments.note(_note))
.call(noteSave);
selection.selectAll('.footer')
.data([0])
.enter()
.append('div')
.attr('class', 'footer')
.call(uiViewOnOSM(context).what(_note))
.call(uiNoteReport(context).note(_note));
}
function noteSave(selection) {
var isSelected = (_note && _note.id === context.selectedNoteID());
var noteSave = selection.selectAll('.note-save-section')
.data((isSelected ? [_note] : []), function(d) { return d.status + d.id; });
// exit
noteSave.exit()
.remove();
// enter
var noteSaveEnter = noteSave.enter()
.append('div')
.attr('class', 'note-save-section save-section cf');
noteSaveEnter
.append('h4')
.attr('class', '.note-save-header')
.text(t('note.newComment'));
noteSaveEnter
.append('textarea')
.attr('id', 'new-comment-input')
.attr('placeholder', t('note.inputPlaceholder'))
.attr('maxlength', 1000)
.property('value', function(d) { return d.newComment; })
.call(utilNoAuto)
.on('input', change)
.on('blur', change);
// update
noteSave = noteSaveEnter
.merge(noteSave)
.call(noteSaveButtons);
function change() {
var input = d3_select(this);
var val = input.property('value').trim() || undefined;
// store the unsaved comment with the note itself
_note = _note.update({ newComment: val });
var osm = services.osm;
if (osm) {
osm.replaceNote(_note); // update note cache
}
noteSave
.call(noteSaveButtons);
}
}
function noteSaveButtons(selection) {
var isSelected = (_note && _note.id === context.selectedNoteID());
var buttonSection = selection.selectAll('.buttons')
.data((isSelected ? [_note] : []), function(d) { return d.status + d.id; });
// exit
buttonSection.exit()
.remove();
// enter
var buttonEnter = buttonSection.enter()
.append('div')
.attr('class', 'buttons');
buttonEnter
.append('button')
.attr('class', 'button status-button action')
.append('span')
.attr('class', 'label');
buttonEnter
.append('button')
.attr('class', 'button comment-button action')
.append('span')
.attr('class', 'label')
.text(t('note.comment'));
// update
buttonSection = buttonSection
.merge(buttonEnter);
buttonSection.select('.status-button') // select and propagate data
.text(function(d) {
var action = (d.status === 'open' ? 'close' : 'open');
var andComment = (d.newComment ? '_comment' : '');
return t('note.' + action + andComment);
})
.on('click.status', function(d) {
this.blur(); // avoid keeping focus on the button - #4641
var osm = services.osm;
if (osm) {
var setStatus = (d.status === 'open' ? 'closed' : 'open');
osm.postNoteUpdate(d, setStatus, function(err, note) {
dispatch.call('change', note);
});
}
});
buttonSection.select('.comment-button') // select and propagate data
.attr('disabled', function(d) {
return (d.status === 'open' && d.newComment) ? null : true;
})
.on('click.save', function(d) {
this.blur(); // avoid keeping focus on the button - #4641
var osm = services.osm;
if (osm) {
osm.postNoteUpdate(d, d.status, function(err, note) {
dispatch.call('change', note);
});
}
});
}
noteEditor.note = function(_) {
if (!arguments.length) return _note;
_note = _;
return noteEditor;
};
return utilRebind(noteEditor, dispatch, 'on');
}

59
modules/ui/note_header.js Normal file
View File

@@ -0,0 +1,59 @@
import { t } from '../util/locale';
import { svgIcon } from '../svg';
export function uiNoteHeader() {
var _note;
function noteHeader(selection) {
var header = selection.selectAll('.note-header')
.data(
(_note ? [_note] : []),
function(d) { return d.status + d.id; }
);
header.exit()
.remove();
var headerEnter = header.enter()
.append('div')
.attr('class', 'note-header');
var iconEnter = headerEnter
.append('div')
.attr('class', function(d) { return 'note-header-icon ' + d.status; });
iconEnter
.append('div')
.attr('class', 'preset-icon-28')
.call(svgIcon('#iD-icon-note', 'note-fill'));
iconEnter.each(function(d) {
if (d.comments.length > 1) {
iconEnter
.append('div')
.attr('class', 'note-icon-annotation')
.call(svgIcon('#iD-icon-more', 'note-annotation'));
}
});
headerEnter
.append('div')
.attr('class', 'note-header-label')
.text(function(d) {
return t('note.note') + ' ' + d.id + ' ' +
(d.status === 'closed' ? t('note.closed') : '');
});
}
noteHeader.note = function(_) {
if (!arguments.length) return _note;
_note = _;
return noteHeader;
};
return noteHeader;
}

47
modules/ui/note_report.js Normal file
View File

@@ -0,0 +1,47 @@
import { t } from '../util/locale';
import { svgIcon } from '../svg';
import {
osmNote
} from '../osm';
export function uiNoteReport() {
var _note;
var url = 'https://www.openstreetmap.org/reports/new?reportable_id=';
function noteReport(selection) {
if (!(_note instanceof osmNote)) return;
url += _note.id + '&reportable_type=Note';
var data = ((!_note || _note.isNew()) ? [] : [_note]);
var link = selection.selectAll('.note-report')
.data(data, function(d) { return d.id; });
// exit
link.exit()
.remove();
// enter
var linkEnter = link.enter()
.append('a')
.attr('class', 'note-report')
.attr('target', '_blank')
.attr('href', url)
.call(svgIcon('#iD-icon-out-link', 'inline'));
linkEnter
.append('span')
.text(t('note.report'));
}
noteReport.note = function(_) {
if (!arguments.length) return _note;
_note = _;
return noteReport;
};
return noteReport;
}

View File

@@ -1,11 +1,19 @@
import _throttle from 'lodash-es/throttle';
import { selectAll as d3_selectAll } from 'd3-selection';
import { osmNote } from '../osm';
import { uiFeatureList } from './feature_list';
import { uiInspector } from './inspector';
import { uiNoteEditor } from './note_editor';
export function uiSidebar(context) {
var inspector = uiInspector(context),
current;
var inspector = uiInspector(context);
var noteEditor = uiNoteEditor(context);
var _current;
var _wasNote = false;
// var layer = d3_select(null);
function sidebar(selection) {
@@ -20,8 +28,19 @@ export function uiSidebar(context) {
.attr('class', 'inspector-hidden inspector-wrap fr');
function hover(id) {
if (!current && context.hasEntity(id)) {
function hover(what) {
if ((what instanceof osmNote)) {
_wasNote = true;
var notes = d3_selectAll('.note');
notes
.classed('hovered', function(d) { return d === what; });
sidebar.show(noteEditor.note(what));
selection.selectAll('.sidebar-component')
.classed('inspector-hover', true);
} else if (!_current && context.hasEntity(what)) {
featureListWrap
.classed('inspector-hidden', true);
@@ -29,22 +48,28 @@ export function uiSidebar(context) {
.classed('inspector-hidden', false)
.classed('inspector-hover', true);
if (inspector.entityID() !== id || inspector.state() !== 'hover') {
if (inspector.entityID() !== what || inspector.state() !== 'hover') {
inspector
.state('hover')
.entityID(id);
.entityID(what);
inspectorWrap
.call(inspector);
}
} else if (!current) {
} else if (!_current) {
featureListWrap
.classed('inspector-hidden', false);
inspectorWrap
.classed('inspector-hidden', true);
inspector
.state('hide');
} else if (_wasNote) {
_wasNote = false;
d3_selectAll('.note')
.classed('hovered', false);
sidebar.hide();
}
}
@@ -53,7 +78,7 @@ export function uiSidebar(context) {
sidebar.select = function(id, newFeature) {
if (!current && id) {
if (!_current && id) {
featureListWrap
.classed('inspector-hidden', true);
@@ -71,7 +96,7 @@ export function uiSidebar(context) {
.call(inspector);
}
} else if (!current) {
} else if (!_current) {
featureListWrap
.classed('inspector-hidden', false);
inspectorWrap
@@ -82,17 +107,17 @@ export function uiSidebar(context) {
};
sidebar.show = function(component) {
sidebar.show = function(component, element) {
featureListWrap
.classed('inspector-hidden', true);
inspectorWrap
.classed('inspector-hidden', true);
if (current) current.remove();
current = selection
if (_current) _current.remove();
_current = selection
.append('div')
.attr('class', 'sidebar-component')
.call(component);
.call(component, element);
};
@@ -102,8 +127,8 @@ export function uiSidebar(context) {
inspectorWrap
.classed('inspector-hidden', true);
if (current) current.remove();
current = null;
if (_current) _current.remove();
_current = null;
};
}

View File

@@ -1,37 +1,48 @@
import { t } from '../util/locale';
import { svgIcon } from '../svg';
import {
osmEntity,
osmNote
} from '../osm';
export function uiViewOnOSM(context) {
var id;
var _what; // an osmEntity or osmNote
function viewOnOSM(selection) {
var entity = context.entity(id);
selection.style('display', entity.isNew() ? 'none' : null);
var url;
if (_what instanceof osmEntity) {
url = context.connection().entityURL(_what);
} else if (_what instanceof osmNote) {
url = context.connection().noteURL(_what);
}
var data = ((!_what || _what.isNew()) ? [] : [_what]);
var link = selection.selectAll('.view-on-osm')
.data([0]);
.data(data, function(d) { return d.id; });
var enter = link.enter()
// exit
link.exit()
.remove();
// enter
var linkEnter = link.enter()
.append('a')
.attr('class', 'view-on-osm')
.attr('target', '_blank')
.attr('href', url)
.call(svgIcon('#iD-icon-out-link', 'inline'));
enter
linkEnter
.append('span')
.text(t('inspector.view_on_osm'));
link
.merge(enter)
.attr('href', context.connection().entityURL(entity));
}
viewOnOSM.entityID = function(_) {
if (!arguments.length) return id;
id = _;
viewOnOSM.what = function(_) {
if (!arguments.length) return _what;
_what = _;
return viewOnOSM;
};

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-20 -20 552 552">
<path stroke="inherit" stroke-width="40px" fill="currentColor" d="M448 0H64C28.7 0 0 28.7 0 64v288c0 35.3 28.7 64 64 64h96v84c0 9.8 11.2 15.5 19.1 9.7L304 416h144c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64z"/>
</svg>

After

Width:  |  Height:  |  Size: 428 B

View File

@@ -89,6 +89,7 @@
<script src='spec/osm/multipolygon.js'></script>
<script src='spec/osm/lanes.js'></script>
<script src='spec/osm/node.js'></script>
<script src='spec/osm/note.js'></script>
<script src='spec/osm/relation.js'></script>
<script src='spec/osm/way.js'></script>

15
test/spec/osm/note.js Normal file
View File

@@ -0,0 +1,15 @@
describe('iD.osmNote', function () {
it('returns a note', function () {
expect(iD.osmNote()).to.be.an.instanceOf(iD.osmNote);
expect(iD.osmNote().type).to.equal('note');
});
describe('#extent', function() {
it('returns a note extent', function() {
expect(iD.osmNote({loc: [5, 10]}).extent().equals([[5, 10], [5, 10]])).to.be.ok;
});
});
// TODO: add tests for #update, or remove function
});

View File

@@ -137,8 +137,8 @@ describe('iD.serviceOsm', function () {
});
describe('#loadFromAPI', function () {
var path = '/api/0.6/map?bbox=-74.542,40.655,-74.541,40.656',
response = '<?xml version="1.0" encoding="UTF-8"?>' +
var path = '/api/0.6/map?bbox=-74.542,40.655,-74.541,40.656';
var response = '<?xml version="1.0" encoding="UTF-8"?>' +
'<osm version="0.6">' +
' <bounds minlat="40.655" minlon="-74.542" maxlat="40.656" maxlon="-74.541' +
' <node id="105340439" visible="true" version="2" changeset="2880013" timestamp="2009-10-18T07:47:39Z" user="woodpeck_fixbot" uid="147510" lat="40.6555" lon="-74.5415"/>' +
@@ -290,14 +290,14 @@ describe('iD.serviceOsm', function () {
);
server.respond();
});
});
describe('#loadEntity', function () {
var nodeXML = '<?xml version="1.0" encoding="UTF-8"?><osm>' +
'<node id="1" version="1" changeset="1" lat="0" lon="0" visible="true" timestamp="2009-03-07T03:26:33Z"></node>' +
'</osm>',
wayXML = '<?xml version="1.0" encoding="UTF-8"?><osm>' +
'</osm>';
var wayXML = '<?xml version="1.0" encoding="UTF-8"?><osm>' +
'<node id="1" version="1" changeset="2817006" lat="0" lon="0" visible="true" timestamp="2009-10-11T18:03:23Z"/>' +
'<way id="1" visible="true" timestamp="2008-01-03T05:24:43Z" version="1" changeset="522559"><nd ref="1"/></way>' +
'</osm>';
@@ -355,11 +355,12 @@ describe('iD.serviceOsm', function () {
});
});
describe('#loadEntityVersion', function () {
var nodeXML = '<?xml version="1.0" encoding="UTF-8"?><osm>' +
'<node id="1" version="1" changeset="1" lat="0" lon="0" visible="true" timestamp="2009-03-07T03:26:33Z"></node>' +
'</osm>',
wayXML = '<?xml version="1.0" encoding="UTF-8"?><osm>' +
'</osm>';
var wayXML = '<?xml version="1.0" encoding="UTF-8"?><osm>' +
'<way id="1" visible="true" timestamp="2008-01-03T05:24:43Z" version="1" changeset="522559"><nd ref="1"/></way>' +
'</osm>';
@@ -416,6 +417,7 @@ describe('iD.serviceOsm', function () {
});
});
describe('#loadMultiple', function () {
beforeEach(function() {
server = sinon.fakeServer.create();
@@ -428,7 +430,6 @@ describe('iD.serviceOsm', function () {
it('loads nodes');
it('loads ways');
it('does not ignore repeat requests');
});
@@ -536,6 +537,146 @@ describe('iD.serviceOsm', function () {
});
describe('#caches', function() {
it('loads reset caches', function (done) {
var resetCaches = {
tile: {
inflight: {}, loaded: {}, seen: {}
},
note: {
loaded: {}, inflight: {}, inflightPost: {}, note: {} // not including rtree
},
user: {
toLoad: {}, user: {}
}
};
var caches = connection.caches();
expect(caches.tile).to.eql(resetCaches.tile);
expect(caches.note.loaded).to.eql(resetCaches.note.loaded);
expect(caches.user).to.eql(resetCaches.user);
done();
});
describe('sets/gets caches', function() {
it('sets/gets a tile', function (done) {
var obj = {
tile: { loaded: { '1,2,16': true, '3,4,16': true } }
};
connection.caches(obj);
expect(connection.caches().tile.loaded['1,2,16']).to.eql(true);
expect(Object.keys(connection.caches().tile.loaded).length).to.eql(2);
done();
});
it('sets/gets a note', function (done) {
var note = iD.osmNote({ id: 1, loc: [0, 0] });
var note2 = iD.osmNote({ id: 2, loc: [0, 0] });
var obj = {
note: { note: { 1: note, 2: note2 } }
};
connection.caches(obj);
expect(connection.caches().note.note[note.id]).to.eql(note);
expect(Object.keys(connection.caches().note.note).length).to.eql(2);
done();
});
it('sets/gets a user', function (done) {
var user = { id: 1, display_name: 'Name' };
var user2 = { id: 2, display_name: 'Name' };
var obj = {
user: { user: { 1: user, 2: user2 } }
};
connection.caches(obj);
expect(connection.caches().user.user[user.id]).to.eql(user);
expect(Object.keys(connection.caches().user.user).length).to.eql(2);
done();
});
});
});
describe('#loadNotes', function() {
beforeEach(function() {
context.projection
.scale(116722210.56960216)
.translate([244505613.61327893, 74865520.92230521])
.clipExtent([[0,0], [609.34375, 826]]);
});
it('fires loadedNotes when notes are loaded', function() {
connection.on('loadedNotes', spy);
connection.loadNotes(context.projection, [64, 64], {});
var url = 'http://www.openstreetmap.org/api/0.6/notes?limit=10000&closed=7&bbox=-120.05859375,34.45221847282654,-119.970703125,34.52466147177173';
var notesXML = ''; // TODO: determine output even though this test note is closed and will be gone soon
server.respondWith('GET', url,
[200, { 'Content-Type': 'text/xml' }, notesXML ]);
server.respond();
expect(spy).to.have.been.calledOnce;
});
});
describe('#notes', function() {
beforeEach(function() {
var dimensions = [64, 64];
context.projection
.scale(667544.214430109) // z14
.translate([-116508, 0]) // 10,0
.clipExtent([[0,0], dimensions]);
});
it('returns notes in the visible map area', function() {
var notes = [
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '0', loc: [10,0] } },
{ minX: 10, minY: 0, maxX: 10, maxY: 0, data: { key: '1', loc: [10,0] } },
{ minX: 10, minY: 1, maxX: 10, maxY: 1, data: { key: '2', loc: [10,1] } }
];
connection.caches('get').note.rtree.load(notes);
var res = connection.notes(context.projection);
expect(res).to.deep.eql([
{ key: '0', loc: [10,0] },
{ key: '1', loc: [10,0] }
]);
});
});
describe('#getNote', function() {
it('returns a note', function (done) {
var note = iD.osmNote({ id: 1, loc: [0, 0], });
var obj = {
note: { note: { 1: note } }
};
connection.caches(obj);
var result = connection.getNote(1);
expect(result).to.deep.equal(note);
done();
});
});
describe('#replaceNote', function() {
it('returns a new note', function (done) {
var note = iD.osmNote({ id: 2, loc: [0, 0], });
var result = connection.replaceNote(note);
expect(result.id).to.eql(2);
done();
});
it('replaces a note', function (done) {
var note = iD.osmNote({ id: 2, loc: [0, 0], });
connection.replaceNote(note);
note.status = 'closed';
var result = connection.replaceNote(note);
expect(result.status).to.eql('closed');
done();
});
});
describe('API capabilities', function() {
var capabilitiesXML = '<?xml version="1.0" encoding="UTF-8"?><osm>' +

View File

@@ -26,15 +26,16 @@ 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(8);
expect(nodes.length).to.eql(9);
expect(d3.select(nodes[0]).classed('data-layer-osm')).to.be.true;
expect(d3.select(nodes[1]).classed('data-layer-gpx')).to.be.true;
expect(d3.select(nodes[2]).classed('data-layer-mvt')).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;
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;
});
});