diff --git a/build_data.js b/build_data.js
index deb949b89..2d72591d8 100644
--- a/build_data.js
+++ b/build_data.js
@@ -59,7 +59,9 @@ module.exports = function buildData() {
};
// Font Awesome icons used
- var faIcons = {};
+ var faIcons = {
+ 'fas-long-arrow-alt-right': {}
+ };
// Start clean
shell.rm('-f', [
diff --git a/css/20_map.css b/css/20_map.css
index 6c9ed5c4b..559b3ae72 100644
--- a/css/20_map.css
+++ b/css/20_map.css
@@ -33,7 +33,8 @@
/* No interactivity except what we specifically allow */
.data-layer.osm *,
.data-layer.notes *,
-.data-layer.keepRight * {
+.data-layer.keepRight *,
+.data-layer.improveOSM * {
pointer-events: none;
}
@@ -44,7 +45,7 @@
/* `.target` objects are interactive */
/* They can be picked up, clicked, hovered, or things can connect to them */
-.kr_error.target,
+.qa_error.target,
.note.target,
.node.target,
.turn .target {
@@ -79,11 +80,10 @@
pointer-events: none !important;
}
-/* NOTE: when more QA layers are added, replace kr_error with generic QA layer selector */
/* points, notes & QA */
/* points, notes, markers */
-g.kr_error .stroke,
+g.qa_error .stroke,
g.note .stroke {
stroke: #222;
stroke-width: 1;
@@ -91,7 +91,7 @@ g.note .stroke {
opacity: 0.6;
}
-g.kr_error.active .stroke,
+g.qa_error.active .stroke,
g.note.active .stroke {
stroke: #222;
stroke-width: 1;
@@ -105,7 +105,7 @@ g.point .stroke {
fill: #fff;
}
-g.kr_error .shadow,
+g.qa_error .shadow,
g.point .shadow,
g.note .shadow {
fill: none;
@@ -114,14 +114,14 @@ g.note .shadow {
stroke-opacity: 0;
}
-g.kr_error.hover:not(.selected) .shadow,
+g.qa_error.hover:not(.selected) .shadow,
g.note.hover:not(.selected) .shadow,
g.point.related:not(.selected) .shadow,
g.point.hover:not(.selected) .shadow {
stroke-opacity: 0.5;
}
-g.kr_error.selected .shadow,
+g.qa_error.selected .shadow,
g.note.selected .shadow,
g.point.selected .shadow {
stroke-opacity: 0.7;
diff --git a/css/25_areas.css b/css/25_areas.css
index ca964e54e..7e6d9f41a 100644
--- a/css/25_areas.css
+++ b/css/25_areas.css
@@ -31,7 +31,6 @@ path.stroke.tag-leisure-track,
path.stroke.tag-leisure-golf_course,
path.stroke.tag-leisure-garden,
path.stroke.tag-leisure-park,
-path.stroke.tag-barrier-hedge,
path.stroke.tag-landuse-forest,
path.stroke.tag-landuse-wood,
path.stroke.tag-landuse-grass {
diff --git a/css/50_misc.css b/css/50_misc.css
index 0e3e726e2..265d641bb 100644
--- a/css/50_misc.css
+++ b/css/50_misc.css
@@ -1,4 +1,56 @@
+/* narrow width miscellanous things */
+path.line.shadow.tag-aerialway,
+path.line.shadow.tag-attraction-summer_toboggan,
+path.line.shadow.tag-attraction-water_slide,
+path.line.shadow.tag-man_made-pipeline,
+path.line.shadow.tag-natural-tree_row,
+path.line.shadow.tag-piste {
+ stroke-width: 16;
+}
+path.line.casing.tag-aerialway,
+path.line.casing.tag-attraction-summer_toboggan,
+path.line.casing.tag-attraction-water_slide,
+path.line.casing.tag-man_made-pipeline,
+path.line.casing.tag-natural-tree_row,
+path.line.casing.tag-piste {
+ stroke-width: 7;
+}
+path.line.stroke.tag-aerialway,
+path.line.stroke.tag-attraction-summer_toboggan,
+path.line.stroke.tag-attraction-water_slide,
+path.line.stroke.tag-man_made-pipeline,
+path.line.stroke.tag-natural-tree_row,
+path.line.stroke.tag-piste {
+ stroke-width: 5;
+}
+
+.low-zoom path.line.shadow.tag-aerialway,
+.low-zoom path.line.shadow.tag-attraction-summer_toboggan,
+.low-zoom path.line.shadow.tag-attraction-water_slide,
+.low-zoom path.line.shadow.tag-man_made-pipeline,
+.low-zoom path.line.shadow.tag-natural-tree_row,
+.low-zoom path.line.shadow.tag-piste {
+ stroke-width: 12;
+}
+.low-zoom path.line.casing.tag-aerialway,
+.low-zoom path.line.casing.tag-attraction-summer_toboggan,
+.low-zoom path.line.casing.tag-attraction-water_slide,
+.low-zoom path.line.casing.tag-man_made-pipeline,
+.low-zoom path.line.casing.tag-natural-tree_row,
+.low-zoom path.line.casing.tag-piste {
+ stroke-width: 5;
+}
+.low-zoom path.line.stroke.tag-aerialway,
+.low-zoom path.line.stroke.tag-attraction-summer_toboggan,
+.low-zoom path.line.stroke.tag-attraction-water_slide,
+.low-zoom path.line.stroke.tag-man_made-pipeline,
+.low-zoom path.line.stroke.tag-natural-tree_row,
+.low-zoom path.line.stroke.tag-piste {
+ stroke-width: 3;
+}
+
+
/* ferry routes */
.preset-icon .icon.tag-route-ferry {
color: #58a9ed;
@@ -24,6 +76,22 @@ path.line.stroke.tag-route-ferry {
}
+/* aerialways */
+path.line.stroke.tag-aerialway {
+ stroke: #c55;
+}
+path.line.casing.tag-aerialway {
+ stroke: #444;
+}
+
+/* pistes */
+path.line.stroke.tag-piste {
+ stroke: #9ac;
+}
+path.line.casing.tag-piste {
+ stroke: #444;
+}
+
/* power and pipeline */
.preset-icon .icon.tag-man_made-pipeline,
.preset-icon .icon.tag-power {
@@ -31,6 +99,7 @@ path.line.stroke.tag-route-ferry {
fill: #939393;
}
+
/* power */
path.line.stroke.tag-power {
stroke: #939393;
@@ -40,21 +109,21 @@ path.line.casing.tag-power {
stroke: none;
}
+
/* pipeline */
path.line.stroke.tag-man_made-pipeline {
stroke: #cbd0d8;
stroke-linecap: butt;
- stroke-width: 3;
stroke-dasharray: 80, 1.25;
}
path.line.casing.tag-man_made-pipeline {
stroke: #666;
- stroke-width: 4.5;
}
.low-zoom path.line.stroke.tag-man_made-pipeline {
stroke-dasharray: 40, 1;
}
+
/* boundaries */
path.line.stroke.tag-boundary {
stroke: #fff;
@@ -73,31 +142,14 @@ path.line.casing.tag-boundary-national_park {
}
-/* Tree Rows */
-path.line.shadow.tag-natural-tree_row {
- stroke-width: 16;
-}
-path.line.casing.tag-natural-tree_row {
- stroke-width: 7;
-}
-path.line.stroke.tag-natural-tree_row {
- stroke-width: 5;
-}
-.low-zoom path.line.shadow.tag-natural-tree_row {
- stroke-width: 12;
-}
-.low-zoom path.line.casing.tag-natural-tree_row {
- stroke-width: 5;
-}
-.low-zoom path.line.stroke.tag-natural-tree_row {
- stroke-width: 3;
-}
-
-
/* barriers and similar */
-path.line.stroke.tag-barrier:not(.tag-barrier-hedge) {
+path.line.stroke.tag-barrier {
stroke: #ddd;
}
+path.line.stroke.tag-barrier-hedge {
+ stroke: rgb(140, 208, 95);
+}
+
path.line.stroke.tag-barrier,
path.line.stroke.tag-man_made-groyne,
path.line.stroke.tag-man_made-breakwater {
@@ -412,30 +464,6 @@ path.line.stroke.tag-crossing.tag-crossing-zebra {
}
/* Attractions */
-path.line.shadow.tag-attraction-summer_toboggan,
-path.line.shadow.tag-attraction-water_slide {
- stroke-width: 16;
-}
-path.line.casing.tag-attraction-summer_toboggan,
-path.line.casing.tag-attraction-water_slide {
- stroke-width: 7;
-}
-path.line.stroke.tag-attraction-summer_toboggan,
-path.line.stroke.tag-attraction-water_slide {
- stroke-width: 5;
-}
-.low-zoom path.line.shadow.tag-attraction-summer_toboggan,
-.low-zoom path.line.shadow.tag-attraction-water_slide {
- stroke-width: 12;
-}
-.low-zoom path.line.casing.tag-attraction-summer_toboggan,
-.low-zoom path.line.casing.tag-attraction-water_slide {
- stroke-width: 5;
-}
-.low-zoom path.line.stroke.tag-attraction-summer_toboggan,
-.low-zoom path.line.stroke.tag-attraction-water_slide {
- stroke-width: 3;
-}
path.line.stroke.tag-attraction-summer_toboggan {
stroke: #9e9e9e;
}
diff --git a/css/55_cursors.css b/css/55_cursors.css
index f473301c4..1a4c8cf7f 100644
--- a/css/55_cursors.css
+++ b/css/55_cursors.css
@@ -97,9 +97,9 @@
}
.mode-browse .note,
-.mode-browse .kr_error,
+.mode-browse .qa_error,
.mode-select .note,
-.mode-select .kr_error,
+.mode-select .qa_error,
.turn rect,
.turn circle {
cursor: pointer;
diff --git a/css/65_data.css b/css/65_data.css
index 71aafab9c..2686d940d 100644
--- a/css/65_data.css
+++ b/css/65_data.css
@@ -1,8 +1,9 @@
/* OSM Notes and KeepRight Layers */
-.kr_error-header-icon .kr_error-fill,
-.layer-keepRight .kr_error .kr_error-fill {
+.kr_error-header-icon .qa_error-fill,
+.layer-keepRight .qa_error .qa_error-fill,
+.layer-improveOSM .qa_error .qa_error-fill {
stroke: #333;
stroke-width: 1.3px; /* NOTE: likely a better way to scale the icon stroke */
}
@@ -41,80 +42,115 @@
height: 15px;
}
+/* adjustment for error icon */
+
+.kr_error-header-icon .preset-icon-28 {
+ top: auto;
+ left: auto;
+}
+
+.kr_error-header-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
/* Keep Right Errors
------------------------------------------------------- */
-.kr_error_type_20, /* multiple nodes on same spot */
-.kr_error_type_40, /* impossible oneways */
-.kr_error_type_210, /* self intersecting ways */
-.kr_error_type_270, /* unusual motorway connection */
-.kr_error_type_310, /* roundabout issues */
-.kr_error_type_320, /* improper _link */
-.kr_error_type_350 { /* improper bridge tag */
+.kr.error_type-20, /* multiple nodes on same spot */
+.kr.error_type-40, /* impossible oneways */
+.kr.error_type-210, /* self intersecting ways */
+.kr.error_type-270, /* unusual motorway connection */
+.kr.error_type-310, /* roundabout issues */
+.kr.error_type-320, /* improper _link */
+.kr.error_type-350 { /* improper bridge tag */
color: #ff9;
}
-.kr_error_type_50 { /* almost junctions */
+.kr.error_type-50 { /* almost junctions */
color: #88f;
}
-.kr_error_type_60, /* deprecated tags */
-.kr_error_type_70, /* tagging issues */
-.kr_error_type_90, /* motorway without ref */
-.kr_error_type_100, /* place of worship without religion */
-.kr_error_type_110, /* poi without name */
-.kr_error_type_150, /* railway crossing without tag */
-.kr_error_type_220, /* misspelled tag */
-.kr_error_type_380 { /* non-physical sport tag */
+.kr.error_type-60, /* deprecated tags */
+.kr.error_type-70, /* tagging issues */
+.kr.error_type-90, /* motorway without ref */
+.kr.error_type-100, /* place of worship without religion */
+.kr.error_type-110, /* poi without name */
+.kr.error_type-150, /* railway crossing without tag */
+.kr.error_type-220, /* misspelled tag */
+.kr.error_type-380 { /* non-physical sport tag */
color: #5d0;
}
-.kr_error_type_130 { /* disconnected ways */
+.kr.error_type-130 { /* disconnected ways */
color: #fa3;
}
-.kr_error_type_170 { /* FIXME tag */
+.kr.error_type-170 { /* FIXME tag */
color: #ff0;
}
-.kr_error_type_190 { /* intersection without junction */
+.kr.error_type-190 { /* intersection without junction */
color: #f33;
}
-.kr_error_type_200 { /* overlapping ways */
+.kr.error_type-200 { /* overlapping ways */
color: #fdbf6f;
}
-.kr_error_type_160, /* railway layer conflict */
-.kr_error_type_230 { /* layer conflict */
+.kr.error_type-160, /* railway layer conflict */
+.kr.error_type-230 { /* layer conflict */
color: #b60;
}
-.kr_error_type_280 { /* boundary issues */
+.kr.error_type-280 { /* boundary issues */
color: #5f47a0;
}
-.kr_error_type_180, /* relation without type */
-.kr_error_type_290 { /* turn restriction issues */
+.kr.error_type-180, /* relation without type */
+.kr.error_type-290 { /* turn restriction issues */
color: #ace;
}
-.kr_error_type_300, /* missing maxspeed */
-.kr_error_type_390 { /* missing tracktype */
+.kr.error_type-300, /* missing maxspeed */
+.kr.error_type-390 { /* missing tracktype */
color: #090;
}
-.kr_error_type_360, /* language unknown */
-.kr_error_type_370, /* doubled places */
-.kr_error_type_410 { /* website issues */
+.kr.error_type-360, /* language unknown */
+.kr.error_type-370, /* doubled places */
+.kr.error_type-410 { /* website issues */
color: #f9b;
}
-.kr_error_type_120, /* way without nodes */
-.kr_error_type_400 { /* geometry / turn angles */
+.kr.error_type-120, /* way without nodes */
+.kr.error_type-400 { /* geometry / turn angles */
color: #c35;
}
+/* ImproveOSM Errors
+------------------------------------------------------- */
+
+.iOSM.error_type-ow- { /* missing one way */
+ color: #1E90FF;
+}
+
+.iOSM.error_type-mr-road { /* missing road */
+ color: #B452CD;
+}
+.iOSM.error_type-mr-path { /* missing path */
+ color: #A0522D;
+}
+.iOSM.error_type-mr-parking { /* missing parking */
+ color: #EEEE00;
+}
+.iOSM.error_type-mr-both { /* missing road+parking */
+ color: #FFA500;
+}
+
+.iOSM.error_type-tr- { /* missing turn restriction */
+ color: #EC1C24;
+}
/* Custom Map Data (geojson, gpx, kml, vector tile) */
.layer-mapdata {
diff --git a/data/core.yaml b/data/core.yaml
index 14d8c70a7..b0b05c196 100644
--- a/data/core.yaml
+++ b/data/core.yaml
@@ -493,6 +493,9 @@ en:
keepRight:
tooltip: Automatically detected map issues from keepright.at
title: KeepRight Issues
+ improveOSM:
+ tooltip: Missing data automatically detected by improveosm.org
+ title: ImproveOSM Issues
custom:
tooltip: "Drag and drop a data file onto the page, or click the button to setup"
title: Custom Map Data
@@ -642,6 +645,32 @@ en:
cannot_zoom: "Cannot zoom out further in current mode."
full_screen: Toggle Full Screen
QA:
+ improveOSM:
+ title: ImproveOSM Detection
+ geometry_types:
+ path: paths
+ parking: parking
+ road: roads
+ both: roads and parking
+ directions:
+ east: east
+ north: north
+ northeast: northeast
+ northwest: northwest
+ south: south
+ southeast: southeast
+ southwest: southwest
+ west: west
+ error_types:
+ ow:
+ title: Missing One-way
+ description: 'Along this section of {highway}, {percentage}% of {num_trips} recorded trips travel from {from_node} to {to_node}. There may be missing a "oneway" tag.'
+ mr:
+ title: Missing Geometry
+ description: '{num_trips} recorded trips in this area suggest there may be unmapped {geometry_type} here.'
+ tr:
+ title: Missing Turn Restriction
+ description: '{num_passed} of {num_trips} recorded trips (travelling {travel_direction}) make a turn from {from_way} to {to_way} at {junction}. There may be a missing "{turn_restriction}" restriction.'
keepRight:
title: KeepRight Error
detail_title: Error
@@ -1098,7 +1127,7 @@ en:
title: Quality Assurance
intro: "*Quality Assurance* (Q/A) tools can find improper tags, disconnected roads, and other issues with OpenStreetMap, which mappers can then fix. To view existing Q/A issues, click the {data} **Map data** panel to enable a specific Q/A layer."
tools_h: "Tools"
- tools: "The following tools are currently supported: [KeepRight](https://www.keepright.at/). Expect iD to support [Osmose](https://osmose.openstreetmap.fr/), [ImproveOSM](https://improveosm.org/en/), and more Q/A tools in the future."
+ tools: "The following tools are currently supported: [KeepRight](https://www.keepright.at/) and [ImproveOSM](https://improveosm.org/en/). Expect iD to support [Osmose](https://osmose.openstreetmap.fr/) and more Q/A tools in the future."
issues_h: "Handling Issues"
issues: "Handling Q/A issues is similar to handling notes. Click on a marker to view the issue details in the sidebar. Each tool has its own capabilities, but generally you can comment and/or close an issue."
field:
diff --git a/data/presets/fields.json b/data/presets/fields.json
index 4080e533d..c578d98a1 100644
--- a/data/presets/fields.json
+++ b/data/presets/fields.json
@@ -84,8 +84,8 @@
"denotation": {"key": "denotation", "type": "combo", "label": "Denotation"},
"description": {"key": "description", "type": "textarea", "label": "Description", "universal": true},
"design": {"key": "design", "type": "combo", "label": "Design"},
- "destination_oneway": {"key": "destination", "type": "semiCombo", "label": "Destinations", "prerequisiteTag": {"key": "oneway", "value": "yes"}},
- "destination/ref_oneway": {"key": "destination:ref", "type": "semiCombo", "label": "Destination Road Numbers", "prerequisiteTag": {"key": "oneway", "value": "yes"}},
+ "destination_oneway": {"key": "destination", "type": "semiCombo", "label": "Destinations", "prerequisiteTag": {"key": "oneway", "value": "yes"}, "snake_case": false},
+ "destination/ref_oneway": {"key": "destination:ref", "type": "semiCombo", "label": "Destination Road Numbers", "prerequisiteTag": {"key": "oneway", "value": "yes"}, "snake_case": false},
"destination/symbol_oneway": {"key": "destination:symbol", "type": "semiCombo", "label": "Destination Symbols", "prerequisiteTag": {"key": "oneway", "value": "yes"}},
"devices": {"key": "devices", "type": "number", "minValue": 0, "label": "Devices", "placeholder": "1, 2, 3..."},
"diaper": {"key": "diaper", "type": "combo", "label": "Diaper Changing Available", "options": ["yes", "no", "room", "1", "2", "3", "4", "5"]},
diff --git a/data/presets/fields/destination/ref_oneway.json b/data/presets/fields/destination/ref_oneway.json
index fc360f211..71fe8dec1 100644
--- a/data/presets/fields/destination/ref_oneway.json
+++ b/data/presets/fields/destination/ref_oneway.json
@@ -5,5 +5,6 @@
"prerequisiteTag": {
"key": "oneway",
"value": "yes"
- }
+ },
+ "snake_case": false
}
diff --git a/data/presets/fields/destination_oneway.json b/data/presets/fields/destination_oneway.json
index 2f71c079b..1d5bc9db8 100644
--- a/data/presets/fields/destination_oneway.json
+++ b/data/presets/fields/destination_oneway.json
@@ -5,5 +5,6 @@
"prerequisiteTag": {
"key": "oneway",
"value": "yes"
- }
+ },
+ "snake_case": false
}
diff --git a/dist/locales/en.json b/dist/locales/en.json
index e11a3b639..300183fcb 100644
--- a/dist/locales/en.json
+++ b/dist/locales/en.json
@@ -598,6 +598,10 @@
"tooltip": "Automatically detected map issues from keepright.at",
"title": "KeepRight Issues"
},
+ "improveOSM": {
+ "tooltip": "Missing data automatically detected by improveosm.org",
+ "title": "ImproveOSM Issues"
+ },
"custom": {
"tooltip": "Drag and drop a data file onto the page, or click the button to setup",
"title": "Custom Map Data",
@@ -782,6 +786,39 @@
"cannot_zoom": "Cannot zoom out further in current mode.",
"full_screen": "Toggle Full Screen",
"QA": {
+ "improveOSM": {
+ "title": "ImproveOSM Detection",
+ "geometry_types": {
+ "path": "paths",
+ "parking": "parking",
+ "road": "roads",
+ "both": "roads and parking"
+ },
+ "directions": {
+ "east": "east",
+ "north": "north",
+ "northeast": "northeast",
+ "northwest": "northwest",
+ "south": "south",
+ "southeast": "southeast",
+ "southwest": "southwest",
+ "west": "west"
+ },
+ "error_types": {
+ "ow": {
+ "title": "Missing One-way",
+ "description": "Along this section of {highway}, {percentage}% of {num_trips} recorded trips travel from {from_node} to {to_node}. There may be missing a \"oneway\" tag."
+ },
+ "mr": {
+ "title": "Missing Geometry",
+ "description": "{num_trips} recorded trips in this area suggest there may be unmapped {geometry_type} here."
+ },
+ "tr": {
+ "title": "Missing Turn Restriction",
+ "description": "{num_passed} of {num_trips} recorded trips (travelling {travel_direction}) make a turn from {from_way} to {to_way} at {junction}. There may be a missing \"{turn_restriction}\" restriction."
+ }
+ }
+ },
"keepRight": {
"title": "KeepRight Error",
"detail_title": "Error",
@@ -1329,7 +1366,7 @@
"title": "Quality Assurance",
"intro": "*Quality Assurance* (Q/A) tools can find improper tags, disconnected roads, and other issues with OpenStreetMap, which mappers can then fix. To view existing Q/A issues, click the {data} **Map data** panel to enable a specific Q/A layer.",
"tools_h": "Tools",
- "tools": "The following tools are currently supported: [KeepRight](https://www.keepright.at/). Expect iD to support [Osmose](https://osmose.openstreetmap.fr/), [ImproveOSM](https://improveosm.org/en/), and more Q/A tools in the future.",
+ "tools": "The following tools are currently supported: [KeepRight](https://www.keepright.at/) and [ImproveOSM](https://improveosm.org/en/). Expect iD to support [Osmose](https://osmose.openstreetmap.fr/) and more Q/A tools in the future.",
"issues_h": "Handling Issues",
"issues": "Handling Q/A issues is similar to handling notes. Click on a marker to view the issue details in the sidebar. Each tool has its own capabilities, but generally you can comment and/or close an issue."
},
diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js
index 58b9187a9..9645b83e2 100644
--- a/modules/behavior/hover.js
+++ b/modules/behavior/hover.js
@@ -5,7 +5,7 @@ import {
select as d3_select
} from 'd3-selection';
-import { osmEntity, osmNote, krError } from '../osm';
+import { osmEntity, osmNote, krError, iOsmError } from '../osm';
import { utilKeybinding, utilRebind } from '../util';
@@ -112,9 +112,12 @@ export function behaviorHover(context) {
entity = datum;
selector = '.data' + datum.__featurehash__;
- } else if (datum instanceof krError) {
+ } else if (
+ datum instanceof iOsmError ||
+ datum instanceof krError
+ ) {
entity = datum;
- selector = '.kr_error-' + datum.id;
+ selector = '.' + datum.source + '.error_id-' + datum.id;
} else if (datum instanceof osmNote) {
entity = datum;
diff --git a/modules/behavior/select.js b/modules/behavior/select.js
index 8c445930d..8043648d3 100644
--- a/modules/behavior/select.js
+++ b/modules/behavior/select.js
@@ -19,6 +19,7 @@ import {
import {
osmEntity,
osmNote,
+ iOsmError,
krError
} from '../osm';
@@ -170,10 +171,14 @@ export function behaviorSelect(context) {
context
.selectedNoteID(datum.id)
.enter(modeSelectNote(context, datum.id));
+ } else if (datum instanceof iOsmError & !isMultiselect) { // clicked an improveOSM error
+ context
+ .selectedErrorID(datum.id)
+ .enter(modeSelectError(context, datum.id, datum.source));
} else if (datum instanceof krError & !isMultiselect) { // clicked a krError error
context
.selectedErrorID(datum.id)
- .enter(modeSelectError(context, datum.id));
+ .enter(modeSelectError(context, datum.id, datum.source));
} else { // clicked nothing..
context.selectedNoteID(null);
context.selectedErrorID(null);
diff --git a/modules/modes/select_error.js b/modules/modes/select_error.js
index dbdae8aff..4f7645644 100644
--- a/modules/modes/select_error.js
+++ b/modules/modes/select_error.js
@@ -13,26 +13,44 @@ import {
import { t } from '../util/locale';
import { services } from '../services';
import { modeBrowse, modeDragNode, modeDragNote } from '../modes';
-import { uiKeepRightEditor } from '../ui';
+import { uiImproveOsmEditor, uiKeepRightEditor } from '../ui';
import { utilKeybinding } from '../util';
-export function modeSelectError(context, selectedErrorID) {
+export function modeSelectError(context, selectedErrorID, selectedErrorSource) {
var mode = {
id: 'select-error',
button: 'browse'
};
- var keepRight = services.keepRight;
var keybinding = utilKeybinding('select-error');
- var keepRightEditor = uiKeepRightEditor(context)
- .on('change', function() {
- context.map().pan([0,0]); // trigger a redraw
- var error = checkSelectedID();
- if (!error) return;
- context.ui().sidebar
- .show(keepRightEditor.error(error));
- });
+
+ var errorService, errorEditor;
+ switch (selectedErrorSource) {
+ case 'iOSM':
+ errorService = services.improveOSM;
+ errorEditor = uiImproveOsmEditor(context)
+ .on('change', function() {
+ context.map().pan([0,0]); // trigger a redraw
+ var error = checkSelectedID();
+ if (!error) return;
+ context.ui().sidebar
+ .show(errorEditor.error(error));
+ });
+ break;
+ case 'kr':
+ errorService = services.keepRight;
+ errorEditor = uiKeepRightEditor(context)
+ .on('change', function() {
+ context.map().pan([0,0]); // trigger a redraw
+ var error = checkSelectedID();
+ if (!error) return;
+ context.ui().sidebar
+ .show(errorEditor.error(error));
+ });
+ break;
+ }
+
var behaviors = [
behaviorBreathe(context),
@@ -45,8 +63,8 @@ export function modeSelectError(context, selectedErrorID) {
function checkSelectedID() {
- if (!keepRight) return;
- var error = keepRight.getError(selectedErrorID);
+ if (!errorService) return;
+ var error = errorService.getError(selectedErrorID);
if (!error) {
context.enter(modeBrowse(context));
}
@@ -55,8 +73,8 @@ export function modeSelectError(context, selectedErrorID) {
mode.zoomToSelected = function() {
- if (!keepRight) return;
- var error = keepRight.getError(selectedErrorID);
+ if (!errorService) return;
+ var error = errorService.getError(selectedErrorID);
if (error) {
context.map().centerZoomEase(error.loc, 20);
}
@@ -78,7 +96,7 @@ export function modeSelectError(context, selectedErrorID) {
selectError();
var sidebar = context.ui().sidebar;
- sidebar.show(keepRightEditor.error(error));
+ sidebar.show(errorEditor.error(error));
context.map()
.on('drawn.select-error', selectError);
@@ -89,7 +107,7 @@ export function modeSelectError(context, selectedErrorID) {
if (!checkSelectedID()) return;
var selection = context.surface()
- .selectAll('.kr_error-' + selectedErrorID);
+ .selectAll('.error_id-' + selectedErrorID + '.' + selectedErrorSource);
if (selection.empty()) {
// Return to browse mode if selected DOM elements have
@@ -121,7 +139,7 @@ export function modeSelectError(context, selectedErrorID) {
.call(keybinding.unbind);
context.surface()
- .selectAll('.kr_error.selected')
+ .selectAll('.qa_error.selected')
.classed('selected hover', false);
context.map()
diff --git a/modules/osm/improveOSM.js b/modules/osm/improveOSM.js
new file mode 100644
index 000000000..fc438ee53
--- /dev/null
+++ b/modules/osm/improveOSM.js
@@ -0,0 +1,51 @@
+import _extend from 'lodash-es/extend';
+
+
+export function iOsmError() {
+ if (!(this instanceof iOsmError)) {
+ return (new iOsmError()).initialize(arguments);
+ } else if (arguments.length) {
+ this.initialize(arguments);
+ }
+}
+
+// ImproveOSM has no error IDs unfortunately
+// So no way to explicitly refer to each error in their DB
+iOsmError.id = function() {
+ return iOsmError.id.next--;
+};
+
+
+iOsmError.id.next = -1;
+
+
+_extend(iOsmError.prototype, {
+
+ type: 'iOsmError',
+ source: 'iOSM',
+
+ initialize: function(sources) {
+ for (var i = 0; i < sources.length; ++i) {
+ var source = sources[i];
+ for (var prop in source) {
+ if (Object.prototype.hasOwnProperty.call(source, prop)) {
+ if (source[prop] === undefined) {
+ delete this[prop];
+ } else {
+ this[prop] = source[prop];
+ }
+ }
+ }
+ }
+
+ if (!this.id) {
+ this.id = iOsmError.id() + ''; // as string
+ }
+
+ return this;
+ },
+
+ update: function(attrs) {
+ return iOsmError(this, attrs); // {v: 1 + (this.v || 0)}
+ }
+});
diff --git a/modules/osm/index.js b/modules/osm/index.js
index 3a9783b1e..bc19af0f9 100644
--- a/modules/osm/index.js
+++ b/modules/osm/index.js
@@ -1,6 +1,7 @@
export { osmChangeset } from './changeset';
export { osmEntity } from './entity';
export { krError } from './keepRight';
+export { iOsmError } from './improveOSM';
export { osmNode } from './node';
export { osmNote } from './note';
export { osmRelation } from './relation';
diff --git a/modules/osm/keepRight.js b/modules/osm/keepRight.js
index 1ea2d4b25..838382e65 100644
--- a/modules/osm/keepRight.js
+++ b/modules/osm/keepRight.js
@@ -21,6 +21,7 @@ krError.id.next = -1;
_extend(krError.prototype, {
type: 'krError',
+ source: 'kr',
initialize: function(sources) {
for (var i = 0; i < sources.length; ++i) {
@@ -46,4 +47,4 @@ _extend(krError.prototype, {
update: function(attrs) {
return krError(this, attrs); // {v: 1 + (this.v || 0)}
}
-});
\ No newline at end of file
+});
diff --git a/modules/osm/tags.js b/modules/osm/tags.js
index 3b7824ec4..0e08dfa19 100644
--- a/modules/osm/tags.js
+++ b/modules/osm/tags.js
@@ -10,13 +10,14 @@ export function osmIsInterestingTag(key) {
export var osmOneWayTags = {
'aerialway': {
'chair_lift': true,
- 'mixed_lift': true,
- 't-bar': true,
+ 'drag_lift': true,
'j-bar': true,
+ 'magic_carpet': true,
+ 'mixed_lift': true,
'platter': true,
'rope_tow': true,
- 'magic_carpet': true,
- 'yes': true
+ 't-bar': true,
+ 'zipline': true
},
'highway': {
'motorway': true
diff --git a/modules/services/improveOSM.js b/modules/services/improveOSM.js
new file mode 100644
index 000000000..2031c4720
--- /dev/null
+++ b/modules/services/improveOSM.js
@@ -0,0 +1,439 @@
+import _extend from 'lodash-es/extend';
+import _find from 'lodash-es/find';
+import _forEach from 'lodash-es/forEach';
+
+import rbush from 'rbush';
+
+import { dispatch as d3_dispatch } from 'd3-dispatch';
+import { json as d3_json } from 'd3-request';
+import { request as d3_request } from 'd3-request';
+
+import { geoExtent, geoVecAdd } from '../geo';
+import { iOsmError } from '../osm';
+import { services } from './index';
+import { t } from '../util/locale';
+import { utilRebind, utilTiler, utilQsString } from '../util';
+
+
+var tiler = utilTiler();
+var dispatch = d3_dispatch('loaded');
+
+var _erCache;
+var _erZoom = 14;
+
+var _impOsmUrls = {
+ ow: 'http://directionofflow.skobbler.net/directionOfFlowService',
+ mr: 'http://missingroads.skobbler.net/missingGeoService',
+ tr: 'http://turnrestrictionservice.skobbler.net/turnRestrictionService'
+};
+
+function abortRequest(i) {
+ _forEach(i, function(v) {
+ if (v) {
+ v.abort();
+ }
+ });
+}
+
+function abortUnwantedRequests(cache, tiles) {
+ _forEach(cache.inflightTile, function(v, k) {
+ var wanted = _find(tiles, function(tile) {
+ return k === tile.id;
+ });
+ if (!wanted) {
+ abortRequest(v);
+ delete cache.inflightTile[k];
+ }
+ });
+}
+
+
+function encodeErrorRtree(d) {
+ return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d };
+}
+
+
+// replace or remove error from rtree
+function updateRtree(item, replace) {
+ _erCache.rtree.remove(item, function isEql(a, b) {
+ return a.data.id === b.data.id;
+ });
+
+ if (replace) {
+ _erCache.rtree.insert(item);
+ }
+}
+
+function linkErrorObject(d) {
+ return '' + d + '';
+}
+
+function linkEntity(d) {
+ return '' + d + '';
+}
+
+function pointAverage(points) {
+ var x = 0;
+ var y = 0;
+
+ _forEach(points, function(v) {
+ x += v.lon;
+ y += v.lat;
+ });
+
+ x /= points.length;
+ y /= points.length;
+
+ return [x, y];
+}
+
+function relativeBearing(p1, p2) {
+ var angle = Math.atan2(p2.lon - p1.lon, p2.lat - p1.lat);
+ if (angle < 0) {
+ angle += 2 * Math.PI;
+ }
+
+ // Return degrees
+ return angle * 180 / Math.PI;
+}
+
+// Assuming range [0,360)
+function cardinalDirection(bearing) {
+ var dir = 45 * Math.round(bearing / 45);
+ var compass = {
+ 0: 'north',
+ 45: 'northeast',
+ 90: 'east',
+ 135: 'southeast',
+ 180: 'south',
+ 225: 'southwest',
+ 270: 'west',
+ 315: 'northwest',
+ 360: 'north'
+ };
+
+ return t('QA.improveOSM.directions.' + compass[dir]);
+}
+
+// Errors shouldn't obscure eachother
+function preventCoincident(loc, bumpUp) {
+ var coincident = false;
+ do {
+ // first time, move marker up. after that, move marker right.
+ var delta = coincident ? [0.00001, 0] : (bumpUp ? [0, 0.00001] : [0, 0]);
+ loc = geoVecAdd(loc, delta);
+ var bbox = geoExtent(loc).bbox();
+ coincident = _erCache.rtree.search(bbox).length;
+ } while (coincident);
+
+ return loc;
+}
+
+export default {
+ init: function() {
+ if (!_erCache) {
+ this.reset();
+ }
+
+ this.event = utilRebind(this, dispatch, 'on');
+ },
+
+ reset: function() {
+ if (_erCache) {
+ _forEach(_erCache.inflightTile, abortRequest);
+ }
+ _erCache = {
+ data: {},
+ loadedTile: {},
+ inflightTile: {},
+ inflightPost: {},
+ closed: {},
+ rtree: rbush()
+ };
+ },
+
+ loadErrors: function(projection) {
+ var options = {
+ client: 'iD',
+ status: 'OPEN',
+ zoom: '19' // Use a high zoom so that clusters aren't returned
+ };
+
+ // determine the needed tiles to cover the view
+ var tiles = tiler
+ .zoomExtent([_erZoom, _erZoom])
+ .getTiles(projection);
+
+ // abort inflight requests that are no longer needed
+ abortUnwantedRequests(_erCache, tiles);
+
+ // issue new requests..
+ tiles.forEach(function(tile) {
+ if (_erCache.loadedTile[tile.id] || _erCache.inflightTile[tile.id]) return;
+
+ var rect = tile.extent.rectangle();
+ var params = _extend({}, options, { east: rect[0], south: rect[3], west: rect[2], north: rect[1] });
+
+ // 3 separate requests to store for each tile
+ var requests = {};
+
+ _forEach(_impOsmUrls, function(v, k) {
+ // We exclude WATER from missing geometry as it doesn't seem useful
+ // We use most confident one-way and turn restrictions only, still have false positives
+ var kParams = _extend({}, params, (k === 'mr') ? { type: 'PARKING,ROAD,BOTH,PATH' } : { confidenceLevel: 'C1' });
+ var url = v + '/search?' + utilQsString(kParams);
+
+ requests[k] = d3_json(url,
+ function(err, data) {
+ delete _erCache.inflightTile[tile.id];
+
+ if (err) return;
+ _erCache.loadedTile[tile.id] = true;
+
+ // Road segments at high zoom == oneways
+ if (data.roadSegments) {
+ data.roadSegments.forEach(function(feature) {
+ // Position error at the approximate middle of the segment
+ var points = feature.points;
+ var mid = points.length / 2;
+ var loc;
+
+ // Even number of points, find midpoint of the middle two
+ // Odd number of points, use position of very middle point
+ if (mid % 1 === 0) {
+ loc = pointAverage([points[mid - 1], points[mid]]);
+ } else {
+ mid = points[Math.floor(mid)];
+ loc = [mid.lon, mid.lat];
+ }
+
+ // One-ways can land on same segment in opposite direction
+ loc = preventCoincident(loc, false);
+
+ var d = new iOsmError({
+ loc: loc,
+ comments: null,
+ error_subtype: '',
+ error_type: k,
+ icon: 'fas-long-arrow-alt-right',
+ identifier: { // this is used to post changes to the error
+ wayId: feature.wayId,
+ fromNodeId: feature.fromNodeId,
+ toNodeId: feature.toNodeId
+ },
+ object_id: feature.wayId,
+ object_type: 'way',
+ status: feature.status
+ });
+
+ // Variables used in the description
+ d.replacements = {
+ percentage: feature.percentOfTrips,
+ num_trips: feature.numberOfTrips,
+ highway: linkErrorObject(t('QA.keepRight.error_parts.highway')),
+ from_node: linkEntity('n' + feature.fromNodeId),
+ to_node: linkEntity('n' + feature.toNodeId)
+ };
+
+ _erCache.data[d.id] = d;
+ _erCache.rtree.insert(encodeErrorRtree(d));
+ });
+ }
+
+ // Tiles at high zoom == missing roads
+ if (data.tiles) {
+ data.tiles.forEach(function(feature) {
+ // Average of recorded points should land on the missing geometry
+ var loc = pointAverage(feature.points);
+
+ // Missing geometry could happen to land on another error
+ loc = preventCoincident(loc, false);
+
+
+ var geoType = feature.type.toLowerCase();
+ var geoIcons = {
+ road: 'maki-car',
+ parking: 'maki-parking',
+ both: 'maki-car',
+ path: 'maki-shoe'
+ };
+
+ var d = new iOsmError({
+ loc: loc,
+ comments: null,
+ error_subtype: geoType,
+ error_type: k,
+ icon: geoIcons[geoType],
+ identifier: { x: feature.x, y: feature.y },
+ status: feature.status
+ });
+
+ d.replacements = {
+ num_trips: feature.numberOfTrips,
+ geometry_type: t('QA.improveOSM.geometry_types.' + geoType)
+ };
+
+ _erCache.data[d.id] = d;
+ _erCache.rtree.insert(encodeErrorRtree(d));
+ });
+ }
+
+ // Entities at high zoom == turn restrictions
+ if (data.entities) {
+ data.entities.forEach(function(feature) {
+ var loc = feature.point;
+
+ // Turn restrictions could be missing at same junction
+ // We also want to bump the error up so node is accessible
+ loc = preventCoincident([loc.lon, loc.lat], true);
+
+ // Elements are presented in a strange way
+ var ids = feature.id.split(',');
+ var from_way = ids[0];
+ var via_node = ids[3];
+ var to_way = ids[2].split(':')[1];
+
+ // Travel direction along from_way clarifies the turn restriction
+ var p1 = feature.segments[0].points[0];
+ var p2 = feature.segments[0].points[1];
+
+ var dir_of_travel = cardinalDirection(relativeBearing(p1, p2));
+
+ var d = new iOsmError({
+ loc: loc,
+ comments: null,
+ error_subtype: '',
+ error_type: k,
+ icon: 'temaki-junction',
+ identifier: feature.id,
+ object_id: via_node,
+ object_type: 'node',
+ status: feature.status
+ });
+
+ // Variables used in the description
+ d.replacements = {
+ num_passed: feature.numberOfPasses,
+ num_trips: feature.segments[0].numberOfTrips,
+ turn_restriction: feature.turnType.toLowerCase(),
+ from_way: linkEntity('w' + from_way),
+ to_way: linkEntity('w' + to_way),
+ travel_direction: dir_of_travel,
+ junction: linkErrorObject(t('QA.keepRight.error_parts.this_node'))
+ };
+
+ _erCache.data[d.id] = d;
+ _erCache.rtree.insert(encodeErrorRtree(d));
+ });
+ }
+ }
+ );
+ });
+
+ _erCache.inflightTile[tile.id] = requests;
+ dispatch.call('loaded');
+ });
+ },
+
+ postUpdate: function(d, callback) {
+ if (!services.osm.authenticated()) { // Username required in payload
+ return callback({ message: 'Not Authenticated', status: -3}, d);
+ }
+ if (_erCache.inflightPost[d.id]) {
+ return callback({ message: 'Error update already inflight', status: -2 }, d);
+ }
+
+ var that = this;
+
+ // Payload can only be sent once username is established
+ services.osm.userDetails(sendPayload);
+
+ function sendPayload(err, user) {
+ if (err) { return callback(err, d); }
+
+ var type = d.error_type;
+ var url = _impOsmUrls[type] + '/comment';
+ var payload = {
+ username: user.display_name
+ };
+
+ // Each error type has different data for identification
+ if (type === 'ow') {
+ payload.roadSegments = [ d.identifier ];
+ } else if (type === 'mr') {
+ payload.tiles = [ d.identifier ];
+ } else if (type === 'tr') {
+ payload.targetIds = [ d.identifier ];
+ }
+
+ // Comments don't currently work, if they ever do in future
+ // it looks as though they require a separate post
+ // if (d.newComment !== undefined) {
+ // payload.text = d.newComment;
+ // }
+
+ if (d.newStatus !== d.status) {
+ payload.status = d.newStatus;
+ payload.text = 'status changed';
+ }
+
+ _erCache.inflightPost[d.id] = d3_request(url)
+ .header('Content-Type', 'application/json')
+ .post(JSON.stringify(payload), function(err) {
+ delete _erCache.inflightPost[d.id];
+
+ // Unsuccessful response status, keep issue open
+ if (err.status !== 200) { return callback(err, d); }
+
+ that.removeError(d);
+
+ // No pretty identifier, so we just use coordinates
+ if (d.newStatus === 'SOLVED') {
+ var closedID = d.loc[1].toFixed(5) + '/' + d.loc[0].toFixed(5);
+ _erCache.closed[d.error_type + ':' + closedID] = true;
+ }
+
+ return callback(err, d);
+ });
+ }
+ },
+
+ // get all cached errors covering the viewport
+ getErrors: function(projection) {
+ var viewport = projection.clipExtent();
+ var min = [viewport[0][0], viewport[1][1]];
+ var max = [viewport[1][0], viewport[0][1]];
+ var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();
+
+ return _erCache.rtree.search(bbox).map(function(d) {
+ return d.data;
+ });
+ },
+
+ // get a single error from the cache
+ getError: function(id) {
+ return _erCache.data[id];
+ },
+
+ // replace a single error in the cache
+ replaceError: function(error) {
+ if (!(error instanceof iOsmError) || !error.id) return;
+
+ _erCache.data[error.id] = error;
+ updateRtree(encodeErrorRtree(error), true); // true = replace
+ return error;
+ },
+
+ // remove a single error from the cache
+ removeError: function(error) {
+ if (!(error instanceof iOsmError) || !error.id) return;
+
+ delete _erCache.data[error.id];
+ updateRtree(encodeErrorRtree(error), false); // false = remove
+ },
+
+ // Used to populate `closed:improveosm` changeset tag
+ getClosedIDs: function() {
+ return Object.keys(_erCache.closed).sort();
+ }
+};
diff --git a/modules/services/index.js b/modules/services/index.js
index 8b75d7418..838ed435f 100644
--- a/modules/services/index.js
+++ b/modules/services/index.js
@@ -1,4 +1,5 @@
import serviceKeepRight from './keepRight';
+import serviceImproveOSM from './improveOSM';
import serviceMapillary from './mapillary';
import serviceMapRules from './maprules';
import serviceNominatim from './nominatim';
@@ -15,6 +16,7 @@ import serviceWikipedia from './wikipedia';
export var services = {
geocoder: serviceNominatim,
keepRight: serviceKeepRight,
+ improveOSM: serviceImproveOSM,
mapillary: serviceMapillary,
openstreetcam: serviceOpenstreetcam,
osm: serviceOsm,
@@ -29,6 +31,7 @@ export var services = {
export {
serviceKeepRight,
+ serviceImproveOSM,
serviceMapillary,
serviceMapRules,
serviceNominatim,
diff --git a/modules/svg/improveOSM.js b/modules/svg/improveOSM.js
new file mode 100644
index 000000000..b9881ff29
--- /dev/null
+++ b/modules/svg/improveOSM.js
@@ -0,0 +1,262 @@
+import _throttle from 'lodash-es/throttle';
+import { select as d3_select } from 'd3-selection';
+
+import { modeBrowse } from '../modes';
+import { svgPointTransform } from './index';
+import { services } from '../services';
+
+var _improveOsmEnabled = false;
+var _errorService;
+
+
+export function svgImproveOSM(projection, context, dispatch) {
+ var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
+ var minZoom = 12;
+ var touchLayer = d3_select(null);
+ var drawLayer = d3_select(null);
+ var _improveOsmVisible = false;
+
+ function markerPath(selection, klass) {
+ selection
+ .attr('class', klass)
+ .attr('transform', 'translate(-10, -28)')
+ .attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6');
+ }
+
+
+ // Loosely-coupled improveOSM service for fetching errors.
+ function getService() {
+ if (services.improveOSM && !_errorService) {
+ _errorService = services.improveOSM;
+ _errorService.on('loaded', throttledRedraw);
+ } else if (!services.improveOSM && _errorService) {
+ _errorService = null;
+ }
+
+ return _errorService;
+ }
+
+
+ // Show the errors
+ function editOn() {
+ if (!_improveOsmVisible) {
+ _improveOsmVisible = true;
+ drawLayer
+ .style('display', 'block');
+ }
+ }
+
+
+ // Immediately remove the errors and their touch targets
+ function editOff() {
+ if (_improveOsmVisible) {
+ _improveOsmVisible = false;
+ drawLayer
+ .style('display', 'none');
+ drawLayer.selectAll('.qa_error.iOSM')
+ .remove();
+ touchLayer.selectAll('.qa_error.iOSM')
+ .remove();
+ }
+ }
+
+
+ // Enable the layer. This shows the errors and transitions them to visible.
+ function layerOn() {
+ editOn();
+
+ drawLayer
+ .style('opacity', 0)
+ .transition()
+ .duration(250)
+ .style('opacity', 1)
+ .on('end interrupt', function () {
+ dispatch.call('change');
+ });
+ }
+
+
+ // Disable the layer. This transitions the layer invisible and then hides the errors.
+ function layerOff() {
+ throttledRedraw.cancel();
+ drawLayer.interrupt();
+ touchLayer.selectAll('.qa_error.iOSM')
+ .remove();
+
+ drawLayer
+ .transition()
+ .duration(250)
+ .style('opacity', 0)
+ .on('end interrupt', function () {
+ editOff();
+ dispatch.call('change');
+ });
+ }
+
+
+ // Update the error markers
+ function updateMarkers() {
+ if (!_improveOsmVisible || !_improveOsmEnabled) return;
+
+ var service = getService();
+ var selectedID = context.selectedErrorID();
+ var data = (service ? service.getErrors(projection) : []);
+ var getTransform = svgPointTransform(projection);
+
+ // Draw markers..
+ var markers = drawLayer.selectAll('.qa_error.iOSM')
+ .data(data, function(d) { return d.id; });
+
+ // exit
+ markers.exit()
+ .remove();
+
+ // enter
+ var markersEnter = markers.enter()
+ .append('g')
+ .attr('class', function(d) {
+ return [
+ 'qa_error',
+ d.source,
+ 'error_id-' + d.id,
+ 'error_type-' + d.error_type + '-' + d.error_subtype
+ ].join(' ');
+ });
+
+ markersEnter
+ .append('polygon')
+ .call(markerPath, 'shadow');
+
+ markersEnter
+ .append('ellipse')
+ .attr('cx', 0)
+ .attr('cy', 0)
+ .attr('rx', 4.5)
+ .attr('ry', 2)
+ .attr('class', 'stroke');
+
+ markersEnter
+ .append('polygon')
+ .attr('fill', 'currentColor')
+ .call(markerPath, 'qa_error-fill');
+
+ markersEnter
+ .append('use')
+ .attr('transform', 'translate(-5.5, -21)')
+ .attr('class', 'icon')
+ .attr('width', '11px')
+ .attr('height', '11px')
+ .attr('xlink:href', function(d) {
+ var picon = d.icon;
+
+ if (!picon) {
+ return '';
+ } else {
+ var isMaki = /^maki-/.test(picon);
+ return '#' + picon + (isMaki ? '-11' : '');
+ }
+ });
+
+ // update
+ markers
+ .merge(markersEnter)
+ .sort(sortY)
+ .classed('selected', function(d) { return d.id === selectedID; })
+ .attr('transform', getTransform);
+
+
+ // Draw targets..
+ if (touchLayer.empty()) return;
+ var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
+
+ var targets = touchLayer.selectAll('.qa_error.iOSM')
+ .data(data, function(d) { return d.id; });
+
+ // exit
+ targets.exit()
+ .remove();
+
+ // enter/update
+ targets.enter()
+ .append('rect')
+ .attr('width', '20px')
+ .attr('height', '30px')
+ .attr('x', '-10px')
+ .attr('y', '-28px')
+ .merge(targets)
+ .sort(sortY)
+ .attr('class', function(d) {
+ return 'qa_error ' + d.source + ' target error_id-' + d.id + ' ' + fillClass;
+ })
+ .attr('transform', getTransform);
+
+
+ function sortY(a, b) {
+ return (a.id === selectedID) ? 1
+ : (b.id === selectedID) ? -1
+ : (a.severity === 'error' && b.severity !== 'error') ? 1
+ : (b.severity === 'error' && a.severity !== 'error') ? -1
+ : b.loc[1] - a.loc[1];
+ }
+ }
+
+
+ // Draw the ImproveOSM layer and schedule loading errors and updating markers.
+ function drawImproveOSM(selection) {
+ var service = getService();
+
+ var surface = context.surface();
+ if (surface && !surface.empty()) {
+ touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');
+ }
+
+ drawLayer = selection.selectAll('.layer-improveOSM')
+ .data(service ? [0] : []);
+
+ drawLayer.exit()
+ .remove();
+
+ drawLayer = drawLayer.enter()
+ .append('g')
+ .attr('class', 'layer-improveOSM')
+ .style('display', _improveOsmEnabled ? 'block' : 'none')
+ .merge(drawLayer);
+
+ if (_improveOsmEnabled) {
+ if (service && ~~context.map().zoom() >= minZoom) {
+ editOn();
+ service.loadErrors(projection);
+ updateMarkers();
+ } else {
+ editOff();
+ }
+ }
+ }
+
+
+ // Toggles the layer on and off
+ drawImproveOSM.enabled = function(val) {
+ if (!arguments.length) return _improveOsmEnabled;
+
+ _improveOsmEnabled = val;
+ if (_improveOsmEnabled) {
+ layerOn();
+ } else {
+ layerOff();
+ if (context.selectedErrorID()) {
+ context.enter(modeBrowse(context));
+ }
+ }
+
+ dispatch.call('change');
+ return this;
+ };
+
+
+ drawImproveOSM.supported = function() {
+ return !!getService();
+ };
+
+
+ return drawImproveOSM;
+}
diff --git a/modules/svg/keepRight.js b/modules/svg/keepRight.js
index c4b8807ab..126ffd320 100644
--- a/modules/svg/keepRight.js
+++ b/modules/svg/keepRight.js
@@ -54,9 +54,9 @@ export function svgKeepRight(projection, context, dispatch) {
_keepRightVisible = false;
drawLayer
.style('display', 'none');
- drawLayer.selectAll('.kr_error')
+ drawLayer.selectAll('.qa_error.kr')
.remove();
- touchLayer.selectAll('.kr_error')
+ touchLayer.selectAll('.qa_error.kr')
.remove();
}
}
@@ -81,7 +81,7 @@ export function svgKeepRight(projection, context, dispatch) {
function layerOff() {
throttledRedraw.cancel();
drawLayer.interrupt();
- touchLayer.selectAll('.kr_error')
+ touchLayer.selectAll('.qa_error.kr')
.remove();
drawLayer
@@ -105,7 +105,7 @@ export function svgKeepRight(projection, context, dispatch) {
var getTransform = svgPointTransform(projection);
// Draw markers..
- var markers = drawLayer.selectAll('.kr_error')
+ var markers = drawLayer.selectAll('.qa_error.kr')
.data(data, function(d) { return d.id; });
// exit
@@ -116,8 +116,13 @@ export function svgKeepRight(projection, context, dispatch) {
var markersEnter = markers.enter()
.append('g')
.attr('class', function(d) {
- return 'kr_error kr_error-' + d.id + ' kr_error_type_' + d.parent_error_type; }
- );
+ return [
+ 'qa_error',
+ d.source,
+ 'error_id-' + d.id,
+ 'error_type-' + d.parent_error_type
+ ].join(' ');
+ });
markersEnter
.append('ellipse')
@@ -133,7 +138,7 @@ export function svgKeepRight(projection, context, dispatch) {
markersEnter
.append('use')
- .attr('class', 'kr_error-fill')
+ .attr('class', 'qa_error-fill')
.attr('width', '20px')
.attr('height', '20px')
.attr('x', '-8px')
@@ -152,7 +157,7 @@ export function svgKeepRight(projection, context, dispatch) {
if (touchLayer.empty()) return;
var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
- var targets = touchLayer.selectAll('.kr_error')
+ var targets = touchLayer.selectAll('.qa_error.kr')
.data(data, function(d) { return d.id; });
// exit
@@ -169,7 +174,7 @@ export function svgKeepRight(projection, context, dispatch) {
.merge(targets)
.sort(sortY)
.attr('class', function(d) {
- return 'kr_error target kr_error-' + d.id + ' ' + fillClass;
+ return 'qa_error ' + d.source + ' target error_id-' + d.id + ' ' + fillClass;
})
.attr('transform', getTransform);
diff --git a/modules/svg/layers.js b/modules/svg/layers.js
index 59c9ab22c..ffe380c89 100644
--- a/modules/svg/layers.js
+++ b/modules/svg/layers.js
@@ -11,6 +11,7 @@ import { svgData } from './data';
import { svgDebug } from './debug';
import { svgGeolocate } from './geolocate';
import { svgKeepRight } from './keepRight';
+import { svgImproveOSM } from './improveOSM';
import { svgStreetside } from './streetside';
import { svgMapillaryImages } from './mapillary_images';
import { svgMapillarySigns } from './mapillary_signs';
@@ -30,6 +31,7 @@ export function svgLayers(projection, context) {
{ id: 'notes', layer: svgNotes(projection, context, dispatch) },
{ id: 'data', layer: svgData(projection, context, dispatch) },
{ id: 'keepRight', layer: svgKeepRight(projection, context, dispatch) },
+ { id: 'improveOSM', layer: svgImproveOSM(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/tag_classes.js b/modules/svg/tag_classes.js
index 588fad521..0e0a08479 100644
--- a/modules/svg/tag_classes.js
+++ b/modules/svg/tag_classes.js
@@ -4,8 +4,8 @@ import { osmPavedTags } from '../osm/tags';
export function svgTagClasses() {
var primaries = [
- 'building', 'highway', 'railway', 'waterway', 'aeroway',
- 'motorway', 'boundary', 'power', 'amenity', 'natural', 'landuse',
+ 'building', 'highway', 'railway', 'waterway', 'aeroway', 'aerialway',
+ 'piste:type', 'boundary', 'power', 'amenity', 'natural', 'landuse',
'leisure', 'military', 'place', 'man_made', 'route', 'attraction'
];
var statuses = [
@@ -30,27 +30,39 @@ export function svgTagClasses() {
}
var t = _tags(entity);
- var isMultiPolygon = (t.type === 'multipolygon');
- var shouldRenderLineAsArea = isMultiPolygon && !entity.hasInterestingTags();
-
var i, k, v;
+ // in some situations we want to render perimeter strokes a certain way
+ var overrideGeometry;
+ if (/\bstroke\b/.test(value)) {
+ if (!!t.barrier && t.barrier !== 'no') {
+ overrideGeometry = 'line';
+ } else if (t.type === 'multipolygon' && !entity.hasInterestingTags()) {
+ overrideGeometry = 'area';
+ }
+ }
+
// preserve base classes (nothing with `tag-`)
var classes = value.trim().split(/\s+/)
.filter(function(klass) {
return klass.length && !/^tag-/.test(klass);
})
- .map(function(klass) { // style multipolygon inner/outers as areas not lines
- return (klass === 'line' && shouldRenderLineAsArea) ? 'area' : klass;
+ .map(function(klass) { // special overrides for some perimeter strokes
+ return (klass === 'line' || klass === 'area') ? (overrideGeometry || klass) : klass;
});
+
// pick at most one primary classification tag..
for (i = 0; i < primaries.length; i++) {
k = primaries[i];
v = t[k];
if (!v || v === 'no') continue;
+ if (k === 'piste:type') { // avoid a ':' in the class name
+ k = 'piste';
+ }
+
primary = k;
if (statuses.indexOf(v) !== -1) { // e.g. `railway=abandoned`
status = v;
diff --git a/modules/ui/commit.js b/modules/ui/commit.js
index 01ece7291..084bc6462 100644
--- a/modules/ui/commit.js
+++ b/modules/ui/commit.js
@@ -110,6 +110,12 @@ export function uiCommit(context) {
tags['closed:keepright'] = krClosed.join(';').substr(0, 255);
}
}
+ if (services.improveOSM) {
+ var iOsmClosed = services.improveOSM.getClosedIDs();
+ if (iOsmClosed.length) {
+ tags['closed:improveosm'] = iOsmClosed.join(';').substr(0, 255);
+ }
+ }
_changeset = _changeset.update({ tags: tags });
@@ -487,4 +493,4 @@ export function uiCommit(context) {
return utilRebind(commit, dispatch, 'on');
-}
+}
\ No newline at end of file
diff --git a/modules/ui/improveOSM_details.js b/modules/ui/improveOSM_details.js
new file mode 100644
index 000000000..be513ccb0
--- /dev/null
+++ b/modules/ui/improveOSM_details.js
@@ -0,0 +1,127 @@
+import {
+ event as d3_event,
+ select as d3_select
+} from 'd3-selection';
+
+import { dataEn } from '../../data';
+import { modeSelect } from '../modes';
+import { t } from '../util/locale';
+import { utilDisplayName, utilEntityOrMemberSelector, utilEntityRoot } from '../util';
+
+
+export function uiImproveOsmDetails(context) {
+ var _error;
+
+
+ function errorDetail(d) {
+ var unknown = t('inspector.unknown');
+
+ if (!d) return unknown;
+ var errorType = d.error_type;
+ var et = dataEn.QA.improveOSM.error_types[errorType];
+
+ var detail;
+ if (et && et.description) {
+ detail = t('QA.improveOSM.error_types.' + errorType + '.description', d.replacements);
+ } else {
+ detail = unknown;
+ }
+
+ return detail;
+ }
+
+
+ function improveOsmDetails(selection) {
+ var details = selection.selectAll('.kr_error-details')
+ .data(
+ (_error ? [_error] : []),
+ function(d) { return d.id + '-' + (d.status || 0); }
+ );
+
+ details.exit()
+ .remove();
+
+ var detailsEnter = details.enter()
+ .append('div')
+ .attr('class', 'kr_error-details kr_error-details-container');
+
+
+ // description
+ var descriptionEnter = detailsEnter
+ .append('div')
+ .attr('class', 'kr_error-details-description');
+
+ descriptionEnter
+ .append('h4')
+ .text(function() { return t('QA.keepRight.detail_description'); });
+
+ descriptionEnter
+ .append('div')
+ .attr('class', 'kr_error-details-description-text')
+ .html(errorDetail);
+
+ // If there are entity links in the error message..
+ descriptionEnter.selectAll('.kr_error_entity_link, .kr_error_object_link')
+ .each(function() {
+ var link = d3_select(this);
+ var isObjectLink = link.classed('kr_error_object_link');
+ var entityID = isObjectLink ?
+ (utilEntityRoot(_error.object_type) + _error.object_id)
+ : this.textContent;
+ var entity = context.hasEntity(entityID);
+
+ // Add click handler
+ link
+ .on('mouseover', function() {
+ context.surface().selectAll(utilEntityOrMemberSelector([entityID], context.graph()))
+ .classed('hover', true);
+ })
+ .on('mouseout', function() {
+ context.surface().selectAll('.hover')
+ .classed('hover', false);
+ })
+ .on('click', function() {
+ d3_event.preventDefault();
+ var osmlayer = context.layers().layer('osm');
+ if (!osmlayer.enabled()) {
+ osmlayer.enabled(true);
+ }
+
+ context.map().centerZoom(_error.loc, 20);
+
+ if (entity) {
+ context.enter(modeSelect(context, [entityID]));
+ } else {
+ context.loadEntity(entityID, function() {
+ context.enter(modeSelect(context, [entityID]));
+ });
+ }
+ });
+
+ // Replace with friendly name if possible
+ // (The entity may not yet be loaded into the graph)
+ if (entity) {
+ var name = utilDisplayName(entity); // try to use common name
+
+ if (!name && !isObjectLink) {
+ var preset = context.presets().match(entity, context.graph());
+ name = preset && !preset.isFallback() && preset.name(); // fallback to preset name
+ }
+
+ if (name) {
+ this.innerText = name;
+ }
+ }
+ });
+ }
+
+
+ improveOsmDetails.error = function(val) {
+ if (!arguments.length) return _error;
+ _error = val;
+ return improveOsmDetails;
+ };
+
+
+ return improveOsmDetails;
+}
diff --git a/modules/ui/improveOSM_editor.js b/modules/ui/improveOSM_editor.js
new file mode 100644
index 000000000..c1c29c1b5
--- /dev/null
+++ b/modules/ui/improveOSM_editor.js
@@ -0,0 +1,195 @@
+import { dispatch as d3_dispatch } from 'd3-dispatch';
+
+import { t } from '../util/locale';
+import { services } from '../services';
+import { modeBrowse } from '../modes';
+import { svgIcon } from '../svg';
+
+import {
+ uiImproveOsmDetails,
+ uiImproveOsmHeader,
+ uiQuickLinks,
+ uiTooltipHtml
+} from './index';
+
+import { utilRebind } from '../util';
+
+
+export function uiImproveOsmEditor(context) {
+ var dispatch = d3_dispatch('change');
+ var errorDetails = uiImproveOsmDetails(context);
+ var errorHeader = uiImproveOsmHeader(context);
+ var quickLinks = uiQuickLinks();
+
+ var _error;
+
+
+ function improveOsmEditor(selection) {
+ // quick links
+ var choices = [{
+ id: 'zoom_to',
+ label: 'inspector.zoom_to.title',
+ tooltip: function() {
+ return uiTooltipHtml(t('inspector.zoom_to.tooltip_issue'), t('inspector.zoom_to.key'));
+ },
+ click: function zoomTo() {
+ context.mode().zoomToSelected();
+ }
+ }];
+
+
+ var header = selection.selectAll('.header')
+ .data([0]);
+
+ var headerEnter = header.enter()
+ .append('div')
+ .attr('class', 'header fillL');
+
+ headerEnter
+ .append('button')
+ .attr('class', 'fr keepRight-editor-close')
+ .on('click', function() {
+ context.enter(modeBrowse(context));
+ })
+ .call(svgIcon('#iD-icon-close'));
+
+ headerEnter
+ .append('h3')
+ .text(t('QA.improveOSM.title'));
+
+
+ var body = selection.selectAll('.body')
+ .data([0]);
+
+ body = body.enter()
+ .append('div')
+ .attr('class', 'body')
+ .merge(body);
+
+ var editor = body.selectAll('.keepRight-editor')
+ .data([0]);
+
+ editor.enter()
+ .append('div')
+ .attr('class', 'modal-section keepRight-editor')
+ .merge(editor)
+ .call(errorHeader.error(_error))
+ .call(quickLinks.choices(choices))
+ .call(errorDetails.error(_error))
+ .call(improveOsmSaveSection);
+ }
+
+ function improveOsmSaveSection(selection) {
+ var isSelected = (_error && _error.id === context.selectedErrorID());
+ var isShown = (_error && (isSelected || _error.newComment || _error.comment));
+ var saveSection = selection.selectAll('.error-save')
+ .data(
+ (isShown ? [_error] : []),
+ function(d) { return d.id + '-' + (d.status || 0); }
+ );
+
+ // exit
+ saveSection.exit()
+ .remove();
+
+ // enter
+ var saveSectionEnter = saveSection.enter()
+ .append('div')
+ .attr('class', 'keepRight-save save-section cf');
+
+ // update
+ saveSection = saveSectionEnter
+ .merge(saveSection)
+ .call(errorSaveButtons);
+ }
+
+ function errorSaveButtons(selection) {
+ var isSelected = (_error && _error.id === context.selectedErrorID());
+ var buttonSection = selection.selectAll('.buttons')
+ .data((isSelected ? [_error] : []), function(d) { return d.status + d.id; });
+
+ // exit
+ buttonSection.exit()
+ .remove();
+
+ // enter
+ var buttonEnter = buttonSection.enter()
+ .append('div')
+ .attr('class', 'buttons');
+
+ // Comments don't currently work
+ // buttonEnter
+ // .append('button')
+ // .attr('class', 'button comment-button action')
+ // .text(t('QA.keepRight.save_comment'));
+
+ buttonEnter
+ .append('button')
+ .attr('class', 'button close-button action');
+
+ buttonEnter
+ .append('button')
+ .attr('class', 'button ignore-button action');
+
+
+ // update
+ buttonSection = buttonSection
+ .merge(buttonEnter);
+
+ // Comments don't currently work
+ // buttonSection.select('.comment-button')
+ // .attr('disabled', function(d) {
+ // return d.newComment === undefined ? true : null;
+ // })
+ // .on('click.comment', function(d) {
+ // this.blur(); // avoid keeping focus on the button - #4641
+ // var errorService = services.improveOSM;
+ // if (errorService) {
+ // errorService.postUpdate(d, function(err, error) {
+ // dispatch.call('change', error);
+ // });
+ // }
+ // });
+
+ buttonSection.select('.close-button')
+ .text(function(d) {
+ var andComment = (d.newComment !== undefined ? '_comment' : '');
+ return t('QA.keepRight.close' + andComment);
+ })
+ .on('click.close', function(d) {
+ this.blur(); // avoid keeping focus on the button - #4641
+ var errorService = services.improveOSM;
+ if (errorService) {
+ d.newStatus = 'SOLVED';
+ errorService.postUpdate(d, function(err, error) {
+ dispatch.call('change', error);
+ });
+ }
+ });
+
+ buttonSection.select('.ignore-button')
+ .text(function(d) {
+ var andComment = (d.newComment !== undefined ? '_comment' : '');
+ return t('QA.keepRight.ignore' + andComment);
+ })
+ .on('click.ignore', function(d) {
+ this.blur(); // avoid keeping focus on the button - #4641
+ var errorService = services.improveOSM;
+ if (errorService) {
+ d.newStatus = 'INVALID';
+ errorService.postUpdate(d, function(err, error) {
+ dispatch.call('change', error);
+ });
+ }
+ });
+ }
+
+ improveOsmEditor.error = function(val) {
+ if (!arguments.length) return _error;
+ _error = val;
+ return improveOsmEditor;
+ };
+
+
+ return utilRebind(improveOsmEditor, dispatch, 'on');
+}
diff --git a/modules/ui/improveOSM_header.js b/modules/ui/improveOSM_header.js
new file mode 100644
index 000000000..aa393b22b
--- /dev/null
+++ b/modules/ui/improveOSM_header.js
@@ -0,0 +1,89 @@
+import { dataEn } from '../../data';
+import { t } from '../util/locale';
+
+
+export function uiImproveOsmHeader() {
+ var _error;
+
+
+ function errorTitle(d) {
+ var unknown = t('inspector.unknown');
+
+ if (!d) return unknown;
+ var errorType = d.error_type;
+ var et = dataEn.QA.improveOSM.error_types[errorType];
+
+ if (et && et.title) {
+ return t('QA.improveOSM.error_types.' + errorType + '.title');
+ } else {
+ return unknown;
+ }
+ }
+
+
+ function improveOsmHeader(selection) {
+ var header = selection.selectAll('.kr_error-header')
+ .data(
+ (_error ? [_error] : []),
+ function(d) { return d.id + '-' + (d.status || 0); }
+ );
+
+ header.exit()
+ .remove();
+
+ var headerEnter = header.enter()
+ .append('div')
+ .attr('class', 'kr_error-header');
+
+ var iconEnter = headerEnter
+ .append('div')
+ .attr('class', 'kr_error-header-icon')
+ .classed('new', function(d) { return d.id < 0; });
+
+ var svgEnter = iconEnter
+ .append('svg')
+ .attr('width', '20px')
+ .attr('height', '30px')
+ .attr('viewbox', '0 0 20 30')
+ .attr('class', function(d) {
+ return 'preset-icon-28 qa_error ' + d.source + ' error_id-' + d.id + ' error_type-' + d.error_type + '-' + d.error_subtype;
+ });
+
+ svgEnter
+ .append('polygon')
+ .attr('fill', 'currentColor')
+ .attr('class', 'qa_error-fill')
+ .attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6');
+
+ svgEnter
+ .append('use')
+ .attr('width', '11px')
+ .attr('height', '11px')
+ .attr('transform', 'translate(4.5, 7)')
+ .attr('xlink:href', function(d) {
+ var picon = d.icon;
+
+ if (!picon) {
+ return '';
+ } else {
+ var isMaki = /^maki-/.test(picon);
+ return '#' + picon + (isMaki ? '-11' : '');
+ }
+ });
+
+ headerEnter
+ .append('div')
+ .attr('class', 'kr_error-header-label')
+ .text(errorTitle);
+ }
+
+
+ improveOsmHeader.error = function(val) {
+ if (!arguments.length) return _error;
+ _error = val;
+ return improveOsmHeader;
+ };
+
+
+ return improveOsmHeader;
+}
diff --git a/modules/ui/index.js b/modules/ui/index.js
index de4ff07c7..c1361064e 100644
--- a/modules/ui/index.js
+++ b/modules/ui/index.js
@@ -28,6 +28,9 @@ export { uiFormFields } from './form_fields';
export { uiFullScreen } from './full_screen';
export { uiGeolocate } from './geolocate';
export { uiHelp } from './help';
+export { uiImproveOsmDetails } from './improveOSM_details';
+export { uiImproveOsmEditor } from './improveOSM_editor';
+export { uiImproveOsmHeader } from './improveOSM_header';
export { uiInfo } from './info';
export { uiInspector } from './inspector';
export { uiKeepRightDetails } from './keepRight_details';
diff --git a/modules/ui/keepRight_header.js b/modules/ui/keepRight_header.js
index 1c8d9533c..534ecaf73 100644
--- a/modules/ui/keepRight_header.js
+++ b/modules/ui/keepRight_header.js
@@ -49,9 +49,9 @@ export function uiKeepRightHeader() {
iconEnter
.append('div')
.attr('class', function(d) {
- return 'preset-icon-28 kr_error kr_error-' + d.id + ' kr_error_type_' + d.parent_error_type;
+ return 'preset-icon-28 qa_error ' + d.source + ' error_id-' + d.id + ' error_type-' + d.parent_error_type;
})
- .call(svgIcon('#iD-icon-bolt', 'kr_error-fill'));
+ .call(svgIcon('#iD-icon-bolt', 'qa_error-fill'));
headerEnter
.append('div')
diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js
index 0d4793224..99236e246 100644
--- a/modules/ui/map_data.js
+++ b/modules/ui/map_data.js
@@ -228,7 +228,7 @@ export function uiMapData(context) {
function drawQAItems(selection) {
- var qaKeys = ['keepRight'];
+ var qaKeys = ['keepRight', 'improveOSM'];
var qaLayers = layers.all().filter(function(obj) { return qaKeys.indexOf(obj.id) !== -1; });
var ul = selection
diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js
index 278c466b0..498baa666 100644
--- a/modules/ui/sidebar.js
+++ b/modules/ui/sidebar.js
@@ -9,9 +9,16 @@ import {
selectAll as d3_selectAll
} from 'd3-selection';
-import { osmEntity, osmNote, krError } from '../osm';
+import { osmEntity, osmNote, iOsmError, krError } from '../osm';
import { services } from '../services';
-import { uiDataEditor, uiFeatureList, uiInspector, uiNoteEditor, uiKeepRightEditor } from './index';
+import {
+ uiDataEditor,
+ uiFeatureList,
+ uiInspector,
+ uiNoteEditor,
+ uiImproveOsmEditor,
+ uiKeepRightEditor
+} from './index';
import { textDirection } from '../util/locale';
@@ -19,10 +26,12 @@ export function uiSidebar(context) {
var inspector = uiInspector(context);
var dataEditor = uiDataEditor(context);
var noteEditor = uiNoteEditor(context);
+ var improveOsmEditor = uiImproveOsmEditor(context);
var keepRightEditor = uiKeepRightEditor(context);
var _current;
var _wasData = false;
var _wasNote = false;
+ var _wasIOsmError = false;
var _wasKRError = false;
@@ -131,6 +140,23 @@ export function uiSidebar(context) {
selection.selectAll('.sidebar-component')
.classed('inspector-hover', true);
+ } else if (datum instanceof iOsmError) {
+ _wasIOsmError = true;
+
+ var improveOSM = services.improveOSM;
+ if (improveOSM) {
+ datum = improveOSM.getError(datum.id);
+ }
+
+ d3_selectAll('.iOSM.qa_error')
+ .classed('hover', function(d) { return d.id === datum.id; });
+
+ sidebar
+ .show(improveOsmEditor.error(datum));
+
+ selection.selectAll('.sidebar-component')
+ .classed('inspector-hover', true);
+
} else if (datum instanceof krError) {
_wasKRError = true;
@@ -139,7 +165,7 @@ export function uiSidebar(context) {
datum = keepRight.getError(datum.id); // marker may contain stale data - get latest
}
- d3_selectAll('.kr_error')
+ d3_selectAll('.kr.qa_error')
.classed('hover', function(d) { return d.id === datum.id; });
sidebar
@@ -173,9 +199,10 @@ export function uiSidebar(context) {
inspector
.state('hide');
- } else if (_wasData || _wasNote || _wasKRError) {
+ } else if (_wasData || _wasNote || _wasIOsmError || _wasKRError) {
_wasNote = false;
_wasData = false;
+ _wasIOsmError = false;
_wasKRError = false;
d3_selectAll('.note').classed('hover', false);
d3_selectAll('.kr_error').classed('hover', false);
diff --git a/svg/fontawesome/fas-long-arrow-alt-right.svg b/svg/fontawesome/fas-long-arrow-alt-right.svg
new file mode 100644
index 000000000..a2b44d34b
--- /dev/null
+++ b/svg/fontawesome/fas-long-arrow-alt-right.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/iD-sprite/icons/icon-bolt.svg b/svg/iD-sprite/icons/icon-bolt.svg
index 8078987b2..aaa264a45 100644
--- a/svg/iD-sprite/icons/icon-bolt.svg
+++ b/svg/iD-sprite/icons/icon-bolt.svg
@@ -1,8 +1,7 @@
diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js
index 5a40332d6..9009dcc2d 100644
--- a/test/spec/svg/layers.js
+++ b/test/spec/svg/layers.js
@@ -26,18 +26,19 @@ 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(11);
+ expect(nodes.length).to.eql(12);
expect(d3.select(nodes[0]).classed('osm')).to.be.true;
expect(d3.select(nodes[1]).classed('notes')).to.be.true;
expect(d3.select(nodes[2]).classed('data')).to.be.true;
expect(d3.select(nodes[3]).classed('keepRight')).to.be.true;
- expect(d3.select(nodes[4]).classed('streetside')).to.be.true;
- expect(d3.select(nodes[5]).classed('mapillary-images')).to.be.true;
- expect(d3.select(nodes[6]).classed('mapillary-signs')).to.be.true;
- expect(d3.select(nodes[7]).classed('openstreetcam-images')).to.be.true;
- expect(d3.select(nodes[8]).classed('debug')).to.be.true;
- expect(d3.select(nodes[9]).classed('geolocate')).to.be.true;
- expect(d3.select(nodes[10]).classed('touch')).to.be.true;
+ expect(d3.select(nodes[4]).classed('improveOSM')).to.be.true;
+ expect(d3.select(nodes[5]).classed('streetside')).to.be.true;
+ expect(d3.select(nodes[6]).classed('mapillary-images')).to.be.true;
+ expect(d3.select(nodes[7]).classed('mapillary-signs')).to.be.true;
+ expect(d3.select(nodes[8]).classed('openstreetcam-images')).to.be.true;
+ expect(d3.select(nodes[9]).classed('debug')).to.be.true;
+ expect(d3.select(nodes[10]).classed('geolocate')).to.be.true;
+ expect(d3.select(nodes[11]).classed('touch')).to.be.true;
});
});
diff --git a/test/spec/svg/tag_classes.js b/test/spec/svg/tag_classes.js
index 8baa177dd..2df7caeb1 100644
--- a/test/spec/svg/tag_classes.js
+++ b/test/spec/svg/tag_classes.js
@@ -209,6 +209,25 @@ describe('iD.svgTagClasses', function () {
expect(selection.attr('class')).to.equal('selected');
});
+ it('stroke overrides: renders areas with barriers as lines', function() {
+ selection
+ .attr('class', 'way area stroke')
+ .datum(iD.osmEntity({tags: {landuse: 'residential', barrier: 'hedge'}}))
+ .call(iD.svgTagClasses());
+ expect(selection.classed('area')).to.be.false;
+ expect(selection.classed('line')).to.be.true;
+ });
+
+ it('stroke overrides: renders simple multipolygon lines as areas', function() {
+ var multipolygon = function () { return { type: 'multipolygon' }; };
+ selection
+ .attr('class', 'way line stroke')
+ .datum(iD.osmEntity({tags: {}}))
+ .call(iD.svgTagClasses().tags(multipolygon));
+ expect(selection.classed('area')).to.be.true;
+ expect(selection.classed('line')).to.be.false;
+ });
+
it('works on SVG elements', function() {
selection = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'g'));
selection