diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index be802579e..e88be04cc 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -324,7 +324,7 @@ correspondence with entities: * `iD.svgLayers` - sets up a number of layers that ensure that map elements appear in an appropriate z-order. * `iD.svgOsm` - sets up the OSM-specific data layers -* `iD.svgGpx` - draws gpx traces +* `iD.svgData` - draws any other overlaid vector data (gpx, kml, geojson, mvt, pbf) * `iD.svgDebug` - draws debugging information ### Other UI diff --git a/build_src.js b/build_src.js index 425eb6120..347bb922b 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/css/20_map.css b/css/20_map.css index 6ec2927ba..13c72a12b 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -306,63 +306,3 @@ g.turn circle { stroke: #68f; } - -/* GPX Paths */ - -.layer-gpx { - pointer-events: none; -} - -path.gpx { - stroke: #ff26d4; - stroke-width: 2; - fill: none; -} - -text.gpxlabel-halo, -text.gpxlabel { - font-size: 10px; - font-weight: bold; - dominant-baseline: middle; -} - -text.gpxlabel { - 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 { - opacity: 0.7; - stroke: #000; - stroke-width: 5px; - stroke-miterlimit: 1; -} diff --git a/css/65_data.css b/css/65_data.css index 38346e8a2..24af906c4 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 */ @@ -41,140 +42,76 @@ .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; +/* Custom Map Data (geojson, gpx, kml, vector tile) */ + +.layer-mapdata { + pointer-events: none; } -.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; +.layer-mapdata path.shadow { + pointer-events: stroke; + stroke: #f6634f; + stroke-width: 16; + stroke-opacity: 0; + fill: none; } -[dir='rtl'] .note-header-icon { - border-right: unset; - border-left: 1px solid #ccc; - border-radius: 0 5px 5px 0; +.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; +} +.layer-mapdata path.shadow.selected { + stroke-opacity: 0.7; } -.note-header-icon .icon-wrap { - position: absolute; - top: 0px; +.layer-mapdata path.stroke { + stroke: #ff26d4; + stroke-width: 2; + fill: none; } -.note-header-label { - background-color: #f6f6f6; - padding: 0 15px; - flex: 1 1 100%; - font-size: 14px; +.layer-mapdata path.fill { + stroke-width: 0; + stroke-opacity: 0.3; + stroke: #ff26d4; + fill: #ff26d4; + fill-opacity: 0.3; + fill-rule: evenodd; +} + +.layer-mapdata text.label-halo, +.layer-mapdata text.label { + font-size: 10px; font-weight: bold; - border-radius: 0 5px 5px 0; + dominant-baseline: middle; } -[dir='rtl'] .note-header-label { - border-radius: 5px 0 0 5px; +.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; + stroke-width: 5px; + stroke-miterlimit: 1; } -.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; -} diff --git a/css/70_fills.css b/css/70_fills.css index 5b0996de8..fa6e58cf8 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/css/80_app.css b/css/80_app.css index 55bf424dc..0f51f871b 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 { @@ -1204,7 +1206,7 @@ a.hide-toggle { } -/* preset form basics */ +/* Preset Editor */ .preset-editor { overflow: hidden; @@ -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; @@ -2415,6 +2421,185 @@ 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; +} + + +/* 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 .key-wrap, +.data-editor.raw-tag-editor .tag-row .input-wrap-position { + width: 50%; +} + + /* Fullscreen button */ div.full-screen { float: right; @@ -2566,8 +2751,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); } @@ -3876,14 +4060,24 @@ svg.mouseclick use.right { /* Settings Modals ------------------------------------------------------- */ -.settings-custom-background .instructions { +.settings-modal textarea { + height: 70px; +} +.settings-modal .buttons .button.col3 { + float: none; /* undo float left */ +} + +.settings-custom-background .instructions-template { margin-bottom: 20px; } -.settings-custom-background textarea { - height: 60px; + + +.settings-custom-data .instructions-url { + margin-bottom: 10px; } -.settings-custom-background .buttons .button.col3 { - float: none; /* undo float left */ +.settings-custom-data .field-file, +.settings-custom-data .instructions-template { + margin-bottom: 20px; } diff --git a/data/core.yaml b/data/core.yaml index 62285567c..5472f81d1 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." @@ -519,6 +523,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 Map Data Settings + file: + instructions: "Choose a local data file. Supported types are:\n .gpx, .kml, .geojson, .json" + label: "Browse files" + or: "Or" + 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?" @@ -610,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 b2cf54e11..0a98578ff 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", @@ -638,6 +643,19 @@ "template": { "placeholder": "Enter a url template" } + }, + "custom_data": { + "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", + "label": "Browse files" + }, + "or": "Or", + "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": { @@ -741,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/behavior/hover.js b/modules/behavior/hover.js index d086ac72c..99d12a9dc 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'; @@ -110,13 +107,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,30 +141,16 @@ 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/behavior/select.js b/modules/behavior/select.js index 9e0b3fee9..1dd051e65 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 72484df62..acd9cc6f3 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 83838c4e1..af440c4c2 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 4addaed88..12cac0fdd 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 25e12400f..1d19eccc3 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 000000000..1e433660d --- /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 c2eb8aba0..36246a207 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)); - } - 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/background.js b/modules/renderer/background.js index 71325195f..2754c1001 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'; @@ -171,10 +170,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 +183,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'); @@ -219,7 +210,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 }); }; @@ -386,20 +377,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 @@ -464,19 +453,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/renderer/map.js b/modules/renderer/map.js index 727ae3323..2878d8a48 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/services/index.js b/modules/services/index.js index 789628ed5..83cc8114b 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 000000000..d34a5181a --- /dev/null +++ b/modules/services/vector_tile.js @@ -0,0 +1,215 @@ +import _clone from 'lodash-es/clone'; +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 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).margin(1); +var dispatch = d3_dispatch('loadedData'); +var _vtCache; + + +function abortRequest(i) { + i.abort(); +} + + +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]; } + + 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(tile.xyz[0], tile.xyz[1], tile.xyz[2]); + var geometry = feature.geometry; + + // 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 + } + + // 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); + + // 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]; + } + } + } + } + }); + + return features; +} + + +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] = []; + delete source.inflight[tile.id]; + if (err || !data) return; + + 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'); + }); +} + + +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: {}, canMerge: {} }; + return _vtCache[sourceID]; + }, + + + data: function(sourceID, projection) { + var source = _vtCache[sourceID]; + if (!source) return []; + + var tiles = tiler.getTiles(projection); + var seen = {}; + var results = []; + + for (var i = 0; i < tiles.length; i++) { + 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; + + // return a shallow clone, because the hash may change + // later if this feature gets merged with another + results.push(_clone(feature)); + } + } + + return results; + }, + + + 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; + } + +}; diff --git a/modules/svg/areas.js b/modules/svg/areas.js index 01a12de7f..546919ecd 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 new file mode 100644 index 000000000..8be1a551c --- /dev/null +++ b/modules/svg/data.js @@ -0,0 +1,512 @@ +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, + geoPath as d3_geoPath +} from 'd3-geo'; + +import { text as d3_text } from 'd3-request'; + +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import stringify from 'fast-json-stable-stringify'; +import toGeoJSON from '@mapbox/togeojson'; + +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; +var _enabled = false; +var _geojson; + + +export function svgData(projection, context, dispatch) { + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + var _showLabels = true; + var detected = utilDetect(); + var layer = d3_select(null); + var _vtService; + var _fileList; + var _template; + 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.svgData', function() { + d3_event.stopPropagation(); + d3_event.preventDefault(); + if (!detected.filedrop) return; + drawData.fileList(d3_event.dataTransfer.files); + }) + .on('dragenter.svgData', over) + .on('dragexit.svgData', over) + .on('dragover.svgData', over); + + _initialized = true; + } + + + 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'); + } + + + // 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.__featurehash__ = utilHashcode(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 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__, + d.geometry.type, + isPolygon(d) ? 'area' : '', + d.__layerID__ || '' + ].filter(Boolean).join(' '); + } + + + 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') + .data(_enabled && hasData ? [0] : []); + + layer.exit() + .remove(); + + layer = layer.enter() + .append('g') + .attr('class', 'layer-mapdata') + .merge(layer); + + var surface = context.surface(); + if (!surface || surface.empty()) return; // not ready to draw yet, starting up + + + // Gather data + var geoData, polygonData; + if (_template && vtService) { // fetch data from vector tile service + var sourceID = _template; + vtService.loadTiles(sourceID, _template, projection); + geoData = vtService.data(sourceID, projection); + } else { + 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 clipPathsEnter = clipPaths.enter() + .append('clipPath') + .attr('class', 'clipPath-data') + .attr('id', clipPathID); + + clipPathsEnter + .append('path'); + + clipPaths.merge(clipPathsEnter) + .selectAll('path') + .attr('d', getAreaPath); + + + // 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 = { + fill: polygonData, + shadow: geoData, + stroke: geoData + }; + + var paths = datagroups + .selectAll('path') + .data(function(layer) { return pathData[layer]; }, featureKey); + + // exit + paths.exit() + .remove(); + + // enter/update + paths = paths.enter() + .append('path') + .attr('class', function(d) { + var datagroup = this.parentNode.__data__; + return 'pathdata ' + datagroup + ' ' + featureClasses(d); + }) + .attr('clip-path', function(d) { + var datagroup = this.parentNode.__data__; + return datagroup === 'fill' ? ('url(#' + clipPathID(d) + ')') : null; + }) + .merge(paths) + .attr('d', function(d) { + var datagroup = this.parentNode.__data__; + return datagroup === 'fill' ? getAreaPath(d) : getPath(d); + }); + + + // Draw labels + 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 _showLabels && d.properties && (d.properties.desc || d.properties.name); + }); + + var labels = selection.selectAll('text.' + textClass) + .data(labelData, featureKey); + + // exit + labels.exit() + .remove(); + + // enter/update + labels = labels.enter() + .append('text') + .attr('class', function(d) { return textClass + ' ' + featureClasses(d); }) + .merge(labels) + .text(function(d) { + return d.properties.desc || d.properties.name; + }) + .attr('x', function(d) { + var centroid = labelPath.centroid(d); + return centroid[0] + 11; + }) + .attr('y', function(d) { + var centroid = labelPath.centroid(d); + return centroid[1]; + }); + } + } + + + function getExtension(fileName) { + if (!fileName) return; + + var lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex < 0) return; + + return fileName.substr(lastDotIndex); + } + + + function xmlToDom(textdata) { + return (new DOMParser()).parseFromString(textdata, 'text/xml'); + } + + + drawData.setFile = function(extension, data, src) { + _template = null; + _fileList = null; + _geojson = null; + _src = null; + + var gj; + switch (extension) { + case '.gpx': + gj = toGeoJSON.gpx(xmlToDom(data)); + break; + case '.kml': + gj = toGeoJSON.kml(xmlToDom(data)); + break; + case '.geojson': + case '.json': + gj = JSON.parse(data); + break; + } + + if (!_isEmpty(gj)) { + _geojson = ensureIDs(gj); + _src = src || 'unknown.geojson'; + this.fitZoom(); + } + + dispatch.call('change'); + return this; + }; + + + drawData.showLabels = function(val) { + if (!arguments.length) return _showLabels; + + _showLabels = val; + return this; + }; + + + drawData.enabled = function(val) { + if (!arguments.length) return _enabled; + + _enabled = val; + if (_enabled) { + showLayer(); + } else { + hideLayer(); + } + + dispatch.call('change'); + return this; + }; + + + drawData.hasData = function() { + return !!(_template || !_isEmpty(_geojson)); + }; + + + 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; + + _template = null; + _fileList = null; + _geojson = null; + _src = null; + + if (!_isEmpty(gj)) { + _geojson = ensureIDs(gj); + _src = src || 'unknown.geojson'; + } + + dispatch.call('change'); + return this; + }; + + + drawData.fileList = function(fileList) { + if (!arguments.length) return _fileList; + + _template = null; + _fileList = fileList; + _geojson = null; + _src = null; + + if (!fileList || !fileList.length) return this; + var f = fileList[0]; + 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); + + reader.readAsText(f); + + return this; + }; + + + drawData.url = function(url) { + _template = null; + _fileList = null; + _geojson = null; + _src = null; + + var extension = getExtension(url); + var re = /\.(gpx|kml|(geo)?json)$/i; + if (re.test(extension)) { + _template = null; + d3_text(url, function(err, data) { + if (err) return; + drawData.setFile(extension, data, url); + }); + } else { + drawData.template(url); + } + + return this; + }; + + + drawData.getSrc = function() { + return _src || ''; + }; + + + drawData.fitZoom = function() { + var features = getFeatures(_geojson); + if (!features.length) return; + + var map = context.map(); + var viewport = map.trimmedExtent().polygon(); + var coords = _reduce(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 drawData; +} diff --git a/modules/svg/gpx.js b/modules/svg/gpx.js deleted file mode 100644 index cfd785742..000000000 --- a/modules/svg/gpx.js +++ /dev/null @@ -1,266 +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 { text as d3_text } from 'd3-request'; -import { - event as d3_event, - select as d3_select -} from 'd3-selection'; - -import { geoExtent, geoPolygonIntersectsPolygon } from '../geo'; -import { svgPath } from './index'; -import { utilDetect } from '../util/detect'; -import toGeoJSON from '@mapbox/togeojson'; - - -var _initialized = false; -var _enabled = false; -var _geojson; - - -export function svgGpx(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.localgpx', function() { - d3_event.stopPropagation(); - d3_event.preventDefault(); - if (!detected.filedrop) return; - drawGpx.files(d3_event.dataTransfer.files); - }) - .on('dragenter.localgpx', over) - .on('dragexit.localgpx', over) - .on('dragover.localgpx', over); - - _initialized = true; - } - - - function drawGpx(selection) { - var getPath = svgPath(projection).geojson; - - layer = selection.selectAll('.layer-gpx') - .data(_enabled ? [0] : []); - - layer.exit() - .remove(); - - layer = layer.enter() - .append('g') - .attr('class', 'layer-gpx') - .merge(layer); - - - var paths = layer - .selectAll('path') - .data([_geojson]); - - paths.exit() - .remove(); - - paths = paths.enter() - .append('path') - .attr('class', 'gpx') - .merge(paths); - - paths - .attr('d', getPath); - - - var labelData = _showLabels && _geojson.features ? _geojson.features : []; - labelData = labelData.filter(getPath); - - layer - .call(drawLabels, 'gpxlabel-halo', labelData) - .call(drawLabels, 'gpxlabel', 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 toDom(x) { - return (new DOMParser()).parseFromString(x, 'text/xml'); - } - - - function getExtension(fileName) { - if (fileName === undefined) { - return ''; - } - - var lastDotIndex = fileName.lastIndexOf('.'); - if (lastDotIndex < 0) { - return ''; - } - - return fileName.substr(lastDotIndex); - } - - - function parseSaveAndZoom(extension, data, src) { - switch (extension) { - default: - drawGpx.geojson(toGeoJSON.gpx(toDom(data)), src).fitZoom(); - break; - case '.kml': - drawGpx.geojson(toGeoJSON.kml(toDom(data)), src).fitZoom(); - break; - case '.geojson': - case '.json': - drawGpx.geojson(JSON.parse(data), src).fitZoom(); - break; - } - } - - - drawGpx.showLabels = function(_) { - if (!arguments.length) return _showLabels; - _showLabels = _; - return this; - }; - - - drawGpx.enabled = function(_) { - if (!arguments.length) return _enabled; - _enabled = _; - dispatch.call('change'); - return this; - }; - - - drawGpx.hasGpx = function() { - return (!(_isEmpty(_geojson) || _isEmpty(_geojson.features))); - }; - - - drawGpx.geojson = function(gj, src) { - if (!arguments.length) return _geojson; - if (_isEmpty(gj) || _isEmpty(gj.features)) return this; - _geojson = gj; - _src = src || 'unknown.geojson'; - dispatch.call('change'); - return this; - }; - - - drawGpx.url = function(url) { - d3_text(url, function(err, data) { - if (!err) { - var extension = getExtension(url); - parseSaveAndZoom(extension, data, url); - } - }); - return this; - }; - - - drawGpx.files = function(fileList) { - if (!fileList.length) return this; - var f = fileList[0]; - var reader = new FileReader(); - - 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 () { - return _src; - }; - - - drawGpx.fitZoom = function() { - if (!this.hasGpx()) 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 drawGpx; -} diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index fe57f3926..6c9bd344a 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.__featurehash__ !== undefined) { + if (d.__featurehash__ in cache) { + return cache[d.__featurehash__]; + } else { + return cache[d.__featurehash__] = path(d); + } + } else { + return path(d); + } + }; return svgpath; } diff --git a/modules/svg/index.js b/modules/svg/index.js index c7760b2aa..4b1bd37a3 100644 --- a/modules/svg/index.js +++ b/modules/svg/index.js @@ -1,8 +1,7 @@ export { svgAreas } from './areas.js'; +export { svgData } from './data.js'; export { svgDebug } from './debug.js'; export { svgDefs } from './defs.js'; -export { svgGpx } from './gpx.js'; -export { svgMvt } from './mvt.js'; export { svgIcon } from './icon.js'; export { svgLabels } from './labels.js'; export { svgLayers } from './layers.js'; diff --git a/modules/svg/layers.js b/modules/svg/layers.js index 31067aea2..7493cc695 100644 --- a/modules/svg/layers.js +++ b/modules/svg/layers.js @@ -7,10 +7,9 @@ import _reject from 'lodash-es/reject'; import { dispatch as d3_dispatch } from 'd3-dispatch'; import { select as d3_select } from 'd3-selection'; +import { svgData } from './data'; import { svgDebug } from './debug'; -import { svgGpx } from './gpx'; import { svgStreetside } from './streetside'; -import { svgMvt } from './mvt'; import { svgMapillaryImages } from './mapillary_images'; import { svgMapillarySigns } from './mapillary_signs'; import { svgOpenstreetcamImages } from './openstreetcam_images'; @@ -26,8 +25,7 @@ export function svgLayers(projection, context) { var layers = [ { id: 'osm', layer: svgOsm(projection, context, dispatch) }, { id: 'notes', layer: svgNotes(projection, context, dispatch) }, - { id: 'gpx', layer: svgGpx(projection, context, dispatch) }, - { id: 'mvt', layer: svgMvt(projection, context, dispatch) }, + { id: 'data', layer: svgData(projection, context, dispatch) }, { id: 'streetside', layer: svgStreetside(projection, context, dispatch)}, { id: 'mapillary-images', layer: svgMapillaryImages(projection, context, dispatch) }, { id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) }, diff --git a/modules/svg/mvt.js b/modules/svg/mvt.js deleted file mode 100644 index ce6a8a540..000000000 --- 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/modules/ui/data_editor.js b/modules/ui/data_editor.js new file mode 100644 index 000000000..593517de6 --- /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.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 000000000..fec0026af --- /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 35d9a8517..3c2641519 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/map_data.js b/modules/ui/map_data.js index 974e1c0ba..d76cc1c4d 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,14 +211,14 @@ export function uiMapData(context) { } - function drawGpxItem(selection) { - var gpx = layers.layer('gpx'); - var hasGpx = gpx && gpx.hasGpx(); - var showsGpx = hasGpx && gpx.enabled(); + function drawCustomDataItems(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,154 +227,82 @@ 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') .call(tooltip() - .title(t('gpx.zoom')) + .title(t('settings.custom_data.tooltip')) + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .on('click', editCustom) + .call(svgIcon('#iD-icon-more')); + + liEnter + .append('button') + .call(tooltip() + .title(t('map_data.layers.custom.zoom')) .placement((textDirection === 'rtl') ? 'right' : 'left') ) .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') - .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() { - gpx.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') ); labelEnter .append('input') .attr('type', 'checkbox') - .on('change', function() { toggleLayer('gpx'); }); + .on('change', function() { toggleLayer('data'); }); labelEnter .append('span') - .text(t('gpx.local_layer')); + .text(t('map_data.layers.custom.title')); // Update 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 editCustom() { + d3_event.preventDefault(); + context.container() + .call(settingsCustomData); } + + function customChanged(d) { + var dataLayer = layers.layer('data'); + + if (d && d.url) { + dataLayer.url(d.url); + } else if (d && d.fileList) { + dataLayer.fileList(d.fileList); + } + } + + function drawListItems(selection, data, type, name, change, active) { var items = selection.selectAll('li') .data(data); @@ -462,8 +394,7 @@ export function uiMapData(context) { _dataLayerContainer .call(drawOsmItems) .call(drawPhotoItems) - .call(drawGpxItem); - // .call(drawMvtItem); + .call(drawCustomDataItems); _fillList .call(drawListItems, fills, 'radio', 'area_fill', setFill, showsFill); diff --git a/modules/ui/map_in_map.js b/modules/ui/map_in_map.js index 3e056e4b0..139f41f6c 100644 --- a/modules/ui/map_in_map.js +++ b/modules/ui/map_in_map.js @@ -22,7 +22,7 @@ import { } from '../geo'; import { rendererTileLayer } from '../renderer'; -import { svgDebug, svgGpx } from '../svg'; +import { svgDebug, svgData } from '../svg'; import { utilSetTransform } from '../util'; import { utilGetDimensions } from '../util/dimensions'; @@ -33,7 +33,7 @@ export function uiMapInMap(context) { var backgroundLayer = rendererTileLayer(context); var overlayLayers = {}; var projection = geoRawMercator(); - var gpxLayer = svgGpx(projection, context).showLabels(false); + var dataLayer = svgData(projection, context).showLabels(false); var debugLayer = svgDebug(projection, context); var zoom = d3_zoom() .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(24)]) @@ -242,7 +242,7 @@ export function uiMapInMap(context) { .append('svg') .attr('class', 'map-in-map-data') .merge(dataLayers) - .call(gpxLayer) + .call(dataLayer) .call(debugLayer); diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js index 85a7f786e..d5a4fb23a 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/settings/custom_background.js b/modules/ui/settings/custom_background.js index e69176053..9b2fc4e05 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 000000000..090a9ec42 --- /dev/null +++ b/modules/ui/settings/custom_data.js @@ -0,0 +1,121 @@ +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 dataLayer = context.layers().layer('data'); + var _origSettings = { + fileList: (dataLayer && dataLayer.fileList()) || null, + url: context.storage('settings-custom-data-url') + }; + 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') + .property('files', _currSettings.fileList) // works for all except IE11 + .on('change', function() { + var files = d3_event.target.files; + if (files && files.length) { + _currSettings.url = ''; + textSection.select('.field-url').property('value', ''); + _currSettings.fileList = files; + } else { + _currSettings.fileList = null; + } + }); + + textSection + .append('h4') + .text(t('settings.custom_data.or')); + + textSection + .append('pre') + .attr('class', 'instructions-url') + .text(t('settings.custom_data.url.instructions')); + + textSection + .append('textarea') + .attr('class', 'field-url') + .attr('placeholder', t('settings.custom_data.url.placeholder')) + .call(utilNoAuto) + .property('value', _currSettings.url); + + + // 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 url + function clickCancel() { + textSection.select('.field-url').property('value', _origSettings.url); + context.storage('settings-custom-data-url', _origSettings.url); + this.blur(); + modal.close(); + } + + // accept the current url + function clickSave() { + _currSettings.url = textSection.select('.field-url').property('value').trim(); + + // one or the other but not both + if (_currSettings.url) { _currSettings.fileList = null; } + if (_currSettings.fileList) { _currSettings.url = ''; } + + context.storage('settings-custom-data-url', _currSettings.url); + 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 52579d5d5..b8a047b26 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'; diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js index 02035d905..33b3df5c0 100644 --- a/modules/ui/sidebar.js +++ b/modules/ui/sidebar.js @@ -2,18 +2,26 @@ 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'; +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; - // var layer = d3_select(null); function sidebar(selection) { @@ -22,26 +30,31 @@ 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 - _wasNote = true; - var notes = d3_selectAll('.note'); - notes - .classed('hover', function(d) { return d === what; }); - - sidebar.show(noteEditor.note(what)); + function hover(datum) { + if (datum && datum.__featurehash__) { // hovering on data + _wasData = true; + sidebar + .show(dataEditor.datum(datum)); selection.selectAll('.sidebar-component') .classed('inspector-hover', true); - } else if (!_current && context.hasEntity(what)) { + } else if (datum instanceof osmNote) { + if (context.mode().id === 'drag-note') return; + _wasNote = true; + + sidebar + .show(noteEditor.note(datum)); + + selection.selectAll('.sidebar-component') + .classed('inspector-hover', true); + + } else if (!_current && (datum instanceof osmEntity)) { featureListWrap .classed('inspector-hidden', true); @@ -49,10 +62,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); @@ -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(); } } diff --git a/modules/util/index.js b/modules/util/index.js index bc06a0a41..16460cd14 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 cf226f5ee..6b03c9f4c 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; +} diff --git a/package.json b/package.json index 312868496..a037d7b76 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,12 @@ "@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", + "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", diff --git a/test/data/gpxtest.gpx b/test/data/gpxtest.gpx deleted file mode 100644 index 0df3c35d5..000000000 --- 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 4982aa6c3..000000000 --- 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 5acf1ad16..000000000 --- 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 63858a7a9..000000000 Binary files a/test/data/mvttest.pbf and /dev/null differ diff --git a/test/index.html b/test/index.html index 6832766ee..abcf0ed83 100644 --- a/test/index.html +++ b/test/index.html @@ -115,12 +115,11 @@ - + - @@ -149,4 +148,4 @@ - \ No newline at end of file + diff --git a/test/spec/svg/data.js b/test/spec/svg/data.js new file mode 100644 index 000000000..426854409 --- /dev/null +++ b/test/spec/svg/data.js @@ -0,0 +1,204 @@ +describe('iD.svgData', 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 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(); + d3.select(document.createElement('div')) + .attr('id', 'map') + .call(context.map().centerZoom([-74.389286, 40.1502754], 17)); + + surface = context.surface(); + }); + + + it('creates layer-mapdata', function () { + var render = iD.svgData(projection, context, dispatch).geojson(gj); + surface.call(render); + + var layers = surface.selectAll('g.layer-mapdata').nodes(); + expect(layers.length).to.eql(1); + }); + + it('draws geojson', function () { + var render = iD.svgData(projection, context, dispatch).geojson(gj); + surface.call(render); + + 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$/); + }); + + 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; + 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(); + }, 200); + }); + + 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; + 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(); + }, 200); + }); + + 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; + 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(); + }, 200); + }); + }); + + + describe('#showLabels', function() { + it('shows labels by default', function () { + var render = iD.svgData(projection, context, dispatch).geojson(gj); + surface.call(render); + + 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.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.svgData(projection, context, dispatch).geojson(gj).showLabels(false); + surface.call(render); + + 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/gpx.js b/test/spec/svg/gpx.js deleted file mode 100644 index cb101af22..000000000 --- a/test/spec/svg/gpx.js +++ /dev/null @@ -1,118 +0,0 @@ -describe('iD.svgGpx', 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-gpx', function () { - var render = iD.svgGpx(projection, context, dispatch); - surface.call(render); - - var layers = surface.selectAll('g.layer-gpx').nodes(); - expect(layers.length).to.eql(1); - }); - - it('draws geojson', function () { - var render = iD.svgGpx(projection, context, dispatch).geojson(gj); - surface.call(render); - - var path = surface.selectAll('path.gpx'); - expect(path.nodes().length).to.eql(1); - expect(path.attr('d')).to.match(/^M.*z$/); - }); - - describe('#files', function() { - it('handles gpx files', function () { - var files = '../../data/gpxtest.gpx'; - var render = iD.svgGpx(projection, context, dispatch).files(files); - surface.call(render); - - var path = surface.selectAll('path.gpx'); - 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); - surface.call(render); - - var path = surface.selectAll('path.gpx'); - 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); - surface.call(render); - - var path = surface.selectAll('path.gpx'); - 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.svgGpx(projection, context, dispatch).geojson(gj); - surface.call(render); - - var label = surface.selectAll('text.gpxlabel'); - expect(label.nodes().length).to.eql(1); - expect(label.text()).to.eql('New Jersey'); - - var halo = surface.selectAll('text.gpxlabel-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); - surface.call(render); - - expect(surface.selectAll('text.gpxlabel').empty()).to.be.ok; - expect(surface.selectAll('text.gpxlabel-halo').empty()).to.be.ok; - }); - }); - -}); diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index 3feba3472..cc327edf1 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; }); }); diff --git a/test/spec/svg/mvt.js b/test/spec/svg/mvt.js deleted file mode 100644 index e5ab522aa..000000000 --- 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; - }); - }); - -});