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