diff --git a/css/20_map.css b/css/20_map.css
index 01962274b..230a8100a 100644
--- a/css/20_map.css
+++ b/css/20_map.css
@@ -45,7 +45,7 @@
/* `.target` objects are interactive */
/* They can be picked up, clicked, hovered, or things can connect to them */
-.qa_error.target,
+.qaItem.target,
.note.target,
.node.target,
.turn .target {
@@ -83,7 +83,7 @@
/* points, notes & QA */
/* points, notes, markers */
-g.qa_error .stroke,
+g.qaItem .stroke,
g.note .stroke {
stroke: #222;
stroke-width: 1;
@@ -91,7 +91,7 @@ g.note .stroke {
opacity: 0.6;
}
-g.qa_error.active .stroke,
+g.qaItem.active .stroke,
g.note.active .stroke {
stroke: #222;
stroke-width: 1;
@@ -106,7 +106,7 @@ g.point .stroke {
}
-g.qa_error .shadow,
+g.qaItem .shadow,
g.point .shadow,
g.note .shadow {
fill: none;
@@ -115,14 +115,14 @@ g.note .shadow {
stroke-opacity: 0;
}
-g.qa_error.hover:not(.selected) .shadow,
+g.qaItem.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.qa_error.selected .shadow,
+g.qaItem.selected .shadow,
g.note.selected .shadow,
g.point.selected .shadow {
stroke-opacity: 0.7;
@@ -430,4 +430,4 @@ g.vertex.highlighted .shadow {
.highlight-edited path.line.shadow.geometry-edited,
.highlight-edited path.area.shadow.geometry-edited {
stroke: rgb(255, 126, 46);
-}
+}
\ No newline at end of file
diff --git a/css/55_cursors.css b/css/55_cursors.css
index 0a5adabbe..d28e33e99 100644
--- a/css/55_cursors.css
+++ b/css/55_cursors.css
@@ -98,10 +98,10 @@
}
.mode-browse .note,
-.mode-browse .qa_error,
+.mode-browse .qaItem,
.mode-select .note,
-.mode-select .qa_error,
+.mode-select .qaItem,
.turn rect,
.turn circle {
cursor: pointer;
-}
+}
\ No newline at end of file
diff --git a/css/65_data.css b/css/65_data.css
index 1687909e6..371dcfaea 100644
--- a/css/65_data.css
+++ b/css/65_data.css
@@ -1,10 +1,10 @@
-/* OSM Notes and KeepRight Layers */
+/* OSM Notes and QA Layers */
-.error-header-icon .qa_error-fill,
-.layer-keepRight .qa_error .qa_error-fill,
-.layer-improveOSM .qa_error .qa_error-fill,
-.layer-osmose .qa_error .qa_error-fill {
+.qa-header-icon .qaItem-fill,
+.layer-keepRight .qaItem .qaItem-fill,
+.layer-improveOSM .qaItem .qaItem-fill,
+.layer-osmose .qaItem .qaItem-fill {
stroke: #333;
stroke-width: 1.3px; /* NOTE: likely a better way to scale the icon stroke */
}
@@ -43,113 +43,111 @@
height: 15px;
}
-/* adjustment for error icon */
-
-.error-header-icon .preset-icon-28 {
+/* adjustment to center QA icons */
+.qa-header-icon .preset-icon-28 {
top: auto;
left: auto;
}
-
-.error-header-icon {
+.qa-header-icon {
display: flex;
align-items: center;
justify-content: center;
}
-/* Keep Right Errors
+/* Keep Right Issues
------------------------------------------------------- */
-.keepRight.error_type-20, /* multiple nodes on same spot */
-.keepRight.error_type-40, /* impossible oneways */
-.keepRight.error_type-210, /* self intersecting ways */
-.keepRight.error_type-270, /* unusual motorway connection */
-.keepRight.error_type-310, /* roundabout issues */
-.keepRight.error_type-320, /* improper _link */
-.keepRight.error_type-350 { /* improper bridge tag */
+.keepRight.itemType-20, /* multiple nodes on same spot */
+.keepRight.itemType-40, /* impossible oneways */
+.keepRight.itemType-210, /* self intersecting ways */
+.keepRight.itemType-270, /* unusual motorway connection */
+.keepRight.itemType-310, /* roundabout issues */
+.keepRight.itemType-320, /* improper _link */
+.keepRight.itemType-350 { /* improper bridge tag */
color: #ff9;
}
-.keepRight.error_type-50 { /* almost junctions */
+.keepRight.itemType-50 { /* almost junctions */
color: #88f;
}
-.keepRight.error_type-60, /* deprecated tags */
-.keepRight.error_type-70, /* tagging issues */
-.keepRight.error_type-90, /* motorway without ref */
-.keepRight.error_type-100, /* place of worship without religion */
-.keepRight.error_type-110, /* poi without name */
-.keepRight.error_type-150, /* railway crossing without tag */
-.keepRight.error_type-220, /* misspelled tag */
-.keepRight.error_type-380 { /* non-physical sport tag */
+.keepRight.itemType-60, /* deprecated tags */
+.keepRight.itemType-70, /* tagging issues */
+.keepRight.itemType-90, /* motorway without ref */
+.keepRight.itemType-100, /* place of worship without religion */
+.keepRight.itemType-110, /* poi without name */
+.keepRight.itemType-150, /* railway crossing without tag */
+.keepRight.itemType-220, /* misspelled tag */
+.keepRight.itemType-380 { /* non-physical sport tag */
color: #5d0;
}
-.keepRight.error_type-130 { /* disconnected ways */
+.keepRight.itemType-130 { /* disconnected ways */
color: #fa3;
}
-.keepRight.error_type-170 { /* FIXME tag */
+.keepRight.itemType-170 { /* FIXME tag */
color: #ff0;
}
-.keepRight.error_type-190 { /* intersection without junction */
+.keepRight.itemType-190 { /* intersection without junction */
color: #f33;
}
-.keepRight.error_type-200 { /* overlapping ways */
+.keepRight.itemType-200 { /* overlapping ways */
color: #fdbf6f;
}
-.keepRight.error_type-160, /* railway layer conflict */
-.keepRight.error_type-230 { /* layer conflict */
+.keepRight.itemType-160, /* railway layer conflict */
+.keepRight.itemType-230 { /* layer conflict */
color: #b60;
}
-.keepRight.error_type-280 { /* boundary issues */
+.keepRight.itemType-280 { /* boundary issues */
color: #5f47a0;
}
-.keepRight.error_type-180, /* relation without type */
-.keepRight.error_type-290 { /* turn restriction issues */
+.keepRight.itemType-180, /* relation without type */
+.keepRight.itemType-290 { /* turn restriction issues */
color: #ace;
}
-.keepRight.error_type-300, /* missing maxspeed */
-.keepRight.error_type-390 { /* missing tracktype */
+.keepRight.itemType-300, /* missing maxspeed */
+.keepRight.itemType-390 { /* missing tracktype */
color: #090;
}
-.keepRight.error_type-360, /* language unknown */
-.keepRight.error_type-370, /* doubled places */
-.keepRight.error_type-410 { /* website issues */
+.keepRight.itemType-360, /* language unknown */
+.keepRight.itemType-370, /* doubled places */
+.keepRight.itemType-410 { /* website issues */
color: #f9b;
}
-.keepRight.error_type-120, /* way without nodes */
-.keepRight.error_type-400 { /* geometry / turn angles */
+.keepRight.itemType-120, /* way without nodes */
+.keepRight.itemType-400 { /* geometry / turn angles */
color: #c35;
}
-/* ImproveOSM Errors
+/* ImproveOSM Issues
------------------------------------------------------- */
-.improveOSM.error_type-ow { /* missing one way */
+.improveOSM.itemType-ow { /* missing one way */
color: #1E90FF;
}
-.improveOSM.error_type-mr-road { /* missing road */
+.improveOSM.itemType-mr-road { /* missing road */
color: #B452CD;
}
-.improveOSM.error_type-mr-path { /* missing path */
+.improveOSM.itemType-mr-path { /* missing path */
color: #A0522D;
}
-.improveOSM.error_type-mr-parking { /* missing parking */
+.improveOSM.itemType-mr-parking { /* missing parking */
color: #EEEE00;
}
-.improveOSM.error_type-mr-both { /* missing road+parking */
+.improveOSM.itemType-mr-both { /* missing road+parking */
color: #FFA500;
}
-.improveOSM.error_type-tr { /* missing turn restriction */
+.improveOSM.itemType-tr { /* missing turn restriction */
color: #EC1C24;
}
diff --git a/css/80_app.css b/css/80_app.css
index 67f1b3148..c062a4a3e 100644
--- a/css/80_app.css
+++ b/css/80_app.css
@@ -649,7 +649,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,
-.sidebar-component .header button.error-editor-close,
+.sidebar-component .header button.qa-editor-close,
.entity-editor-pane .header button.preset-close,
.preset-list-pane .header button.preset-choose {
position: absolute;
@@ -659,7 +659,7 @@ button.add-note svg.icon {
[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'] .sidebar-component .header button.error-editor-close,
+[dir='rtl'] .sidebar-component .header button.qa-editor-close,
[dir='rtl'] .entity-editor-pane .header button.preset-close,
[dir='rtl'] .preset-list-pane .header button.preset-choose {
left: 0;
@@ -2607,10 +2607,10 @@ input.key-trap {
}
-/* OSM Note / KeepRight Editors
+/* OSM Note / QA Editors
------------------------------------------------------- */
.note-header,
-.error-header {
+.qa-header {
background-color: #f6f6f6;
border-radius: 5px;
border: 1px solid #ccc;
@@ -2620,7 +2620,7 @@ input.key-trap {
}
.note-header-icon,
-.error-header-icon {
+.qa-header-icon {
background-color: #fff;
padding: 10px;
flex: 0 0 62px;
@@ -2631,14 +2631,14 @@ input.key-trap {
border-radius: 5px 0 0 5px;
}
[dir='rtl'] .note-header-icon,
-[dir='rtl'] .error-header-icon {
+[dir='rtl'] .qa-header-icon {
border-right: unset;
border-left: 1px solid #ccc;
border-radius: 0 5px 5px 0;
}
.note-header-icon .icon-wrap,
-.error-header-icon .icon-wrap {
+.qa-header-icon .icon-wrap {
position: absolute;
top: 0px;
}
@@ -2654,7 +2654,7 @@ input.key-trap {
}
.note-header-label,
-.error-header-label {
+.qa-header-label {
background-color: #f6f6f6;
padding: 0 15px;
flex: 1 1 100%;
@@ -2663,7 +2663,7 @@ input.key-trap {
border-radius: 0 5px 5px 0;
}
[dir='rtl'] .note-header-label,
-[dir='rtl'] .error-header-label {
+[dir='rtl'] .qa-header-label {
border-radius: 5px 0 0 5px;
}
@@ -2730,11 +2730,11 @@ input.key-trap {
}
.note-save,
-.error-save {
+.qa-save {
padding-top: 20px;
}
-.error-details-container {
+.qa-details-container {
background: #ececec;
padding: 10px;
margin-top: 20px;
@@ -2743,22 +2743,22 @@ input.key-trap {
display: flex;
flex-direction: column;
}
-.error-details-description {
+.qa-details-description {
margin-bottom: 10px;
display: flex;
flex-direction: column;
}
-.error-details-description-text::first-letter {
+.qa-details-description-text::first-letter {
text-transform: capitalize;
}
-[dir='rtl'] .error-details-description-text::first-letter {
+[dir='rtl'] .qa-details-description-text::first-letter {
text-transform: none; /* #5877 */
}
-.error-details-subsection h4 {
+.qa-details-subsection h4 {
padding-top: 10px;
padding-bottom: 0;
}
-.error-details code {
+.qa-details-container code {
padding: .2em .4em;
margin: 0;
font-size: 85%;
@@ -2766,7 +2766,7 @@ input.key-trap {
background-color: rgba(27,31,35,.05);
border-radius: 3px;
}
-.error-details + .translation-link {
+.qa-details-container + .translation-link {
margin-top: 5px;
display: flex;
flex-direction: row;
@@ -2774,7 +2774,7 @@ input.key-trap {
}
.note-save .new-comment-input,
-.error-save .new-comment-input {
+.qa-save .new-comment-input {
width: 100%;
height: 100px;
max-height: 300px;
@@ -2782,7 +2782,7 @@ input.key-trap {
}
.note-save .detail-section,
-.error-save .detail-section {
+.qa-save .detail-section {
margin: 10px 0;
}
diff --git a/data/qa_data.json b/data/qa_data.json
new file mode 100644
index 000000000..ad73cc7dd
--- /dev/null
+++ b/data/qa_data.json
@@ -0,0 +1,91 @@
+{
+ "improveOSM": {
+ "icons": {
+ "ow": "fas-long-arrow-alt-right",
+ "mr-both": "maki-car",
+ "mr-parking": "maki-parking",
+ "mr-path": "maki-shoe",
+ "mr-road": "maki-car",
+ "tr": "temaki-junction"
+ }
+ },
+ "osmose": {
+ "icons": {
+ "0-1": "maki-home",
+ "0-2": "maki-home",
+ "1040-1": "maki-square-stroked",
+ "1050-1": "maki-circle-stroked",
+ "1050-1050": "maki-circle-stroked",
+ "1070-1": "maki-home",
+ "1070-4": "maki-dam",
+ "1070-5": "maki-dam",
+ "1070-8": "maki-cross",
+ "1070-10": "maki-cross",
+ "1150-1": "far-clone",
+ "1150-2": "far-clone",
+ "1150-3": "far-clone",
+ "1190-10": "fas-share-alt",
+ "1190-20": "fas-share-alt",
+ "1190-30": "fas-share-alt",
+ "1280-1": "maki-attraction",
+ "2110-21101": "temaki-plaque",
+ "2110-21102": "fas-shapes",
+ "3040-3040": "far-times-circle",
+ "3090-3090": "fas-calendar-alt",
+ "3161-1": "maki-parking",
+ "3161-2": "maki-parking",
+ "3200-32001": "fas-vector-square",
+ "3200-32002": "fas-vector-square",
+ "3200-32003": "fas-vector-square",
+ "3220-32200": "maki-roadblock",
+ "3220-32201": "maki-roadblock",
+ "3250-32501": "maki-watch",
+ "4010-4010": "maki-waste-basket",
+ "4010-40102": "maki-waste-basket",
+ "4030-900": "fas-yin-yang",
+ "4080-1": "far-dot-circle",
+ "4080-2": "far-dot-circle",
+ "4080-3": "far-dot-circle",
+ "5010-803": "fas-sort-alpha-up",
+ "5010-903": "fas-rocket",
+ "5070-50703": "fas-tint-slash",
+ "5070-50704": "fas-code",
+ "5070-50705": "fas-question",
+ "7040-1": "temaki-power_tower",
+ "7040-2": "temaki-power",
+ "7040-4": "maki-marker",
+ "7040-6": "temaki-power",
+ "7090-1": "maki-rail",
+ "7090-3": "maki-circle",
+ "8300-1": "fas-tachometer-alt",
+ "8300-2": "fas-tachometer-alt",
+ "8300-3": "fas-tachometer-alt",
+ "8300-4": "fas-tachometer-alt",
+ "8300-5": "fas-tachometer-alt",
+ "8300-6": "fas-tachometer-alt",
+ "8300-7": "fas-tachometer-alt",
+ "8300-8": "fas-tachometer-alt",
+ "8300-9": "fas-tachometer-alt",
+ "8300-10": "fas-tachometer-alt",
+ "8300-11": "fas-tachometer-alt",
+ "8300-12": "fas-tachometer-alt",
+ "8300-13": "fas-tachometer-alt",
+ "8300-14": "fas-tachometer-alt",
+ "8300-15": "fas-tachometer-alt",
+ "8300-16": "fas-tachometer-alt",
+ "8300-17": "fas-tachometer-alt",
+ "8300-20": "temaki-height_restrictor",
+ "8300-21": "fas-weight-hanging",
+ "8300-32": "maki-circle-stroked",
+ "8300-34": "temaki-diamond",
+ "8300-39": "temaki-pedestrian",
+ "8360-1": "temaki-bench",
+ "8360-2": "maki-bicycle",
+ "8360-3": "temaki-security_camera",
+ "8360-4": "temaki-fire_hydrant",
+ "8360-5": "temaki-traffic_signals",
+ "9010-9010001": "fas-tags",
+ "9010-9010003": "temaki-plaque"
+ }
+ }
+}
\ No newline at end of file
diff --git a/data/qa_errors.json b/data/qa_errors.json
deleted file mode 100644
index 0d6ed3d0f..000000000
--- a/data/qa_errors.json
+++ /dev/null
@@ -1,93 +0,0 @@
-{
- "services": {
- "improveOSM": {
- "errorIcons": {
- "ow": "fas-long-arrow-alt-right",
- "mr-both": "maki-car",
- "mr-parking": "maki-parking",
- "mr-path": "maki-shoe",
- "mr-road": "maki-car",
- "tr": "temaki-junction"
- }
- },
- "osmose": {
- "errorIcons": {
- "0-1": "maki-home",
- "0-2": "maki-home",
- "1040-1": "maki-square-stroked",
- "1050-1": "maki-circle-stroked",
- "1050-1050": "maki-circle-stroked",
- "1070-1": "maki-home",
- "1070-4": "maki-dam",
- "1070-5": "maki-dam",
- "1070-8": "maki-cross",
- "1070-10": "maki-cross",
- "1150-1": "far-clone",
- "1150-2": "far-clone",
- "1150-3": "far-clone",
- "1190-10": "fas-share-alt",
- "1190-20": "fas-share-alt",
- "1190-30": "fas-share-alt",
- "1280-1": "maki-attraction",
- "2110-21101": "temaki-plaque",
- "2110-21102": "fas-shapes",
- "3040-3040": "far-times-circle",
- "3090-3090": "fas-calendar-alt",
- "3161-1": "maki-parking",
- "3161-2": "maki-parking",
- "3200-32001": "fas-vector-square",
- "3200-32002": "fas-vector-square",
- "3200-32003": "fas-vector-square",
- "3220-32200": "maki-roadblock",
- "3220-32201": "maki-roadblock",
- "3250-32501": "maki-watch",
- "4010-4010": "maki-waste-basket",
- "4010-40102": "maki-waste-basket",
- "4030-900": "fas-yin-yang",
- "4080-1": "far-dot-circle",
- "4080-2": "far-dot-circle",
- "4080-3": "far-dot-circle",
- "5010-803": "fas-sort-alpha-up",
- "5010-903": "fas-rocket",
- "5070-50703": "fas-tint-slash",
- "5070-50704": "fas-code",
- "5070-50705": "fas-question",
- "7040-1": "temaki-power_tower",
- "7040-2": "temaki-power",
- "7040-4": "maki-marker",
- "7040-6": "temaki-power",
- "7090-1": "maki-rail",
- "7090-3": "maki-circle",
- "8300-1": "fas-tachometer-alt",
- "8300-2": "fas-tachometer-alt",
- "8300-3": "fas-tachometer-alt",
- "8300-4": "fas-tachometer-alt",
- "8300-5": "fas-tachometer-alt",
- "8300-6": "fas-tachometer-alt",
- "8300-7": "fas-tachometer-alt",
- "8300-8": "fas-tachometer-alt",
- "8300-9": "fas-tachometer-alt",
- "8300-10": "fas-tachometer-alt",
- "8300-11": "fas-tachometer-alt",
- "8300-12": "fas-tachometer-alt",
- "8300-13": "fas-tachometer-alt",
- "8300-14": "fas-tachometer-alt",
- "8300-15": "fas-tachometer-alt",
- "8300-16": "fas-tachometer-alt",
- "8300-17": "fas-tachometer-alt",
- "8300-20": "temaki-height_restrictor",
- "8300-21": "fas-weight-hanging",
- "8300-32": "maki-circle-stroked",
- "8300-34": "temaki-diamond",
- "8300-39": "temaki-pedestrian",
- "8360-1": "temaki-bench",
- "8360-2": "maki-bicycle",
- "8360-3": "temaki-security_camera",
- "8360-4": "temaki-fire_hydrant",
- "8360-5": "temaki-traffic_signals",
- "9010-9010001": "fas-tags",
- "9010-9010003": "temaki-plaque"
- }
- }
- }
-}
\ No newline at end of file
diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js
index 6cc286b93..df007dc9b 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, qaError } from '../osm';
+import { osmEntity, osmNote, QAItem } from '../osm';
import { utilKeybinding, utilRebind } from '../util';
/*
@@ -131,9 +131,9 @@ export function behaviorHover(context) {
entity = datum;
selector = '.data' + datum.__featurehash__;
- } else if (datum instanceof qaError) {
+ } else if (datum instanceof QAItem) {
entity = datum;
- selector = '.' + datum.service + '.error_id-' + datum.id;
+ selector = '.' + datum.service + '.itemId-' + datum.id;
} else if (datum instanceof osmNote) {
entity = datum;
diff --git a/modules/behavior/select.js b/modules/behavior/select.js
index 311db7da0..511c1b827 100644
--- a/modules/behavior/select.js
+++ b/modules/behavior/select.js
@@ -6,7 +6,7 @@ import { modeSelect } from '../modes/select';
import { modeSelectData } from '../modes/select_data';
import { modeSelectNote } from '../modes/select_note';
import { modeSelectError } from '../modes/select_error';
-import { osmEntity, osmNote, qaError } from '../osm';
+import { osmEntity, osmNote, QAItem } from '../osm';
export function behaviorSelect(context) {
@@ -173,7 +173,7 @@ export function behaviorSelect(context) {
.selectedNoteID(datum.id)
.enter(modeSelectNote(context, datum.id));
- } else if (datum instanceof qaError & !isMultiselect) { // clicked an external QA error
+ } else if (datum instanceof QAItem & !isMultiselect) { // clicked an external QA issue
context
.selectedErrorID(datum.id)
.enter(modeSelectError(context, datum.id, datum.service));
diff --git a/modules/core/context.js b/modules/core/context.js
index 6520fdb75..589e89ac9 100644
--- a/modules/core/context.js
+++ b/modules/core/context.js
@@ -292,6 +292,7 @@ export function coreContext() {
return context;
};
+ // NOTE: Don't change the name of this until UI v3 is merged
let _selectedErrorID;
context.selectedErrorID = function(errorID) {
if (!arguments.length) return _selectedErrorID;
diff --git a/modules/modes/select_error.js b/modules/modes/select_error.js
index 0af8b3acb..751d972b2 100644
--- a/modules/modes/select_error.js
+++ b/modules/modes/select_error.js
@@ -18,7 +18,7 @@ import { uiKeepRightEditor } from '../ui/keepRight_editor';
import { uiOsmoseEditor } from '../ui/osmose_editor';
import { utilKeybinding } from '../util';
-
+// NOTE: Don't change name of this until UI v3 is merged
export function modeSelectError(context, selectedErrorID, selectedErrorService) {
var mode = {
id: 'select-error',
@@ -118,7 +118,7 @@ export function modeSelectError(context, selectedErrorID, selectedErrorService)
if (!checkSelectedID()) return;
var selection = context.surface()
- .selectAll('.error_id-' + selectedErrorID + '.' + selectedErrorService);
+ .selectAll('.itemId-' + selectedErrorID + '.' + selectedErrorService);
if (selection.empty()) {
// Return to browse mode if selected DOM elements have
@@ -150,7 +150,7 @@ export function modeSelectError(context, selectedErrorID, selectedErrorService)
.call(keybinding.unbind);
context.surface()
- .selectAll('.qa_error.selected')
+ .selectAll('.qaItem.selected')
.classed('selected hover', false);
context.map()
@@ -165,4 +165,4 @@ export function modeSelectError(context, selectedErrorID, selectedErrorService)
return mode;
-}
\ No newline at end of file
+}
diff --git a/modules/osm/index.js b/modules/osm/index.js
index 87b34bdd4..8b437967f 100644
--- a/modules/osm/index.js
+++ b/modules/osm/index.js
@@ -4,7 +4,7 @@ export { osmNode } from './node';
export { osmNote } from './note';
export { osmRelation } from './relation';
export { osmWay } from './way';
-export { qaError } from './qa_error';
+export { QAItem } from './qa_item';
export {
osmIntersection,
diff --git a/modules/osm/qa_error.js b/modules/osm/qa_error.js
deleted file mode 100644
index d76893d01..000000000
--- a/modules/osm/qa_error.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { services } from '../../data/qa_errors.json';
-
-
-export function qaError() {
- if (!(this instanceof qaError)) {
- return (new qaError()).initialize(arguments);
- } else if (arguments.length) {
- this.initialize(arguments);
- }
-}
-
-// Generic handling for services without nice IDs
-qaError.id = function() {
- return qaError.id.next--;
-};
-
-qaError.id.next = -1;
-
-Object.assign(qaError.prototype, {
- type: 'qaError',
-
- // All errors need a position
- loc: [0, 0],
-
- // These should be passed in, used to retrieve from qa_errors.json
- service: '',
- error_type: '',
-
- 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];
- }
- }
- }
- }
-
- // Extract common error information from data
- if (this.service && this.error_type) {
- var serviceInfo = services[this.service];
-
- if (serviceInfo && serviceInfo.errorIcons) {
- this.icon = serviceInfo.errorIcons[this.error_type];
- }
- }
-
- // All errors must have an ID for selection
- if (!this.id) {
- this.id = qaError.id() + ''; // as string
- }
-
- return this;
- },
-
- update: function(attrs) {
- return qaError(this, attrs); // {v: 1 + (this.v || 0)}
- }
-});
\ No newline at end of file
diff --git a/modules/osm/qa_item.js b/modules/osm/qa_item.js
new file mode 100644
index 000000000..7f9c48859
--- /dev/null
+++ b/modules/osm/qa_item.js
@@ -0,0 +1,46 @@
+import * as qaServices from '../../data/qa_data.json';
+
+export class QAItem {
+ constructor(loc, service, itemType, id, props) {
+ // Store required properties
+ this.loc = loc;
+ this.service = service;
+ this.itemType = itemType;
+
+ // All issues must have an ID for selection, use generic if none specified
+ this.id = id ? id : `${QAItem.id()}`;
+
+ this.update(props);
+
+ // Some QA services have marker icons to differentiate issues
+ if (service && itemType) {
+ const serviceInfo = qaServices[service];
+
+ if (serviceInfo && serviceInfo.icons) {
+ this.icon = serviceInfo.icons[itemType];
+ }
+ }
+
+ return this;
+ }
+
+ update(props) {
+ // You can't override this inital information
+ const { loc, service, itemType, id } = this;
+
+ Object.keys(props).forEach(prop => this[prop] = props[prop]);
+
+ this.loc = loc;
+ this.service = service;
+ this.itemType = itemType;
+ this.id = id;
+
+ return this;
+ }
+
+ // Generic handling for services without nice IDs
+ static id() {
+ return this.nextId--;
+ }
+}
+QAItem.nextId = -1;
diff --git a/modules/services/improveOSM.js b/modules/services/improveOSM.js
index 281052a08..6620f1e8e 100644
--- a/modules/services/improveOSM.js
+++ b/modules/services/improveOSM.js
@@ -4,491 +4,469 @@ import { dispatch as d3_dispatch } from 'd3-dispatch';
import { json as d3_json } from 'd3-fetch';
import { geoExtent, geoVecAdd, geoVecScale } from '../geo';
-import { qaError } from '../osm';
+import { QAItem } from '../osm';
import { serviceOsm } 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: 'https://grab.community.improve-osm.org/directionOfFlowService',
- mr: 'https://grab.community.improve-osm.org/missingGeoService',
- tr: 'https://grab.community.improve-osm.org/turnRestrictionService'
+const tiler = utilTiler();
+const dispatch = d3_dispatch('loaded');
+const _tileZoom = 14;
+const _impOsmUrls = {
+ ow: 'https://grab.community.improve-osm.org/directionOfFlowService',
+ mr: 'https://grab.community.improve-osm.org/missingGeoService',
+ tr: 'https://grab.community.improve-osm.org/turnRestrictionService'
};
+// This gets reassigned if reset
+let _cache;
+
function abortRequest(i) {
- Object.values(i).forEach(function(controller) {
- if (controller) {
- controller.abort();
- }
- });
+ Object.values(i).forEach(controller => {
+ if (controller) {
+ controller.abort();
+ }
+ });
}
function abortUnwantedRequests(cache, tiles) {
- Object.keys(cache.inflightTile).forEach(function(k) {
- var wanted = tiles.find(function(tile) { return k === tile.id; });
- if (!wanted) {
- abortRequest(cache.inflightTile[k]);
- delete cache.inflightTile[k];
- }
- });
-}
-
-
-function encodeErrorRtree(d) {
- return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d };
-}
-
-
-// replace or remove error from rtree
-function updateRtree(item, replace) {
- _erCache.rtree.remove(item, function isEql(a, b) {
- return a.data.id === b.data.id;
- });
-
- if (replace) {
- _erCache.rtree.insert(item);
+ Object.keys(cache.inflightTile).forEach(k => {
+ const wanted = tiles.find(tile => k === tile.id);
+ if (!wanted) {
+ abortRequest(cache.inflightTile[k]);
+ delete cache.inflightTile[k];
}
+ });
+}
+
+function encodeIssueRtree(d) {
+ return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d };
+}
+
+// Replace or remove QAItem from rtree
+function updateRtree(item, replace) {
+ _cache.rtree.remove(item, (a, b) => a.data.id === b.data.id);
+
+ if (replace) {
+ _cache.rtree.insert(item);
+ }
}
function linkErrorObject(d) {
- return '' + d + '';
+ return `${d}`;
}
function linkEntity(d) {
- return '' + d + '';
+ return `${d}`;
}
function pointAverage(points) {
- if (points.length) {
- var sum = points.reduce(function(acc, point) {
- return geoVecAdd(acc, [point.lon, point.lat]);
- }, [0,0]);
- return geoVecScale(sum, 1 / points.length);
- } else {
- return [0,0];
- }
+ if (points.length) {
+ const sum = points.reduce(
+ (acc, point) => geoVecAdd(acc, [point.lon, point.lat]),
+ [0,0]
+ );
+ return geoVecScale(sum, 1 / points.length);
+ } else {
+ return [0,0];
+ }
}
function relativeBearing(p1, p2) {
- var angle = Math.atan2(p2.lon - p1.lon, p2.lat - p1.lat);
- if (angle < 0) {
- angle += 2 * Math.PI;
- }
+ let 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;
+ // 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'
- };
+ const dir = 45 * Math.round(bearing / 45);
+ const 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]);
+ 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);
+ let coincident = false;
+ do {
+ // first time, move marker up. after that, move marker right.
+ let delta = coincident ? [0.00001, 0] : (bumpUp ? [0, 0.00001] : [0, 0]);
+ loc = geoVecAdd(loc, delta);
+ let bbox = geoExtent(loc).bbox();
+ coincident = _cache.rtree.search(bbox).length;
+ } while (coincident);
- return loc;
+ return loc;
}
export default {
- init: function() {
- if (!_erCache) {
- this.reset();
- }
-
- this.event = utilRebind(this, dispatch, 'on');
- },
-
- reset: function() {
- if (_erCache) {
- Object.values(_erCache.inflightTile).forEach(abortRequest);
- }
- _erCache = {
- data: {},
- loadedTile: {},
- inflightTile: {},
- inflightPost: {},
- closed: {},
- rtree: new RBush()
- };
- },
-
- loadErrors: function(projection) {
- var options = {
- 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 = Object.assign({}, options, { east: rect[0], south: rect[3], west: rect[2], north: rect[1] });
-
- // 3 separate requests to store for each tile
- var requests = {};
-
- Object.keys(_impOsmUrls).forEach(function(k) {
- var v = _impOsmUrls[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 = Object.assign({},
- params,
- (k === 'mr') ? { type: 'PARKING,ROAD,BOTH,PATH' } : { confidenceLevel: 'C1' }
- );
- var url = v + '/search?' + utilQsString(kParams);
-
- var controller = new AbortController();
- requests[k] = controller;
-
- d3_json(url, { signal: controller.signal })
- .then(function(data) {
- delete _erCache.inflightTile[tile.id][k];
- if (!Object.keys(_erCache.inflightTile[tile.id]).length) {
- delete _erCache.inflightTile[tile.id];
- _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 qaError({
- // Info required for every error
- loc: loc,
- service: 'improveOSM',
- error_type: k,
- // Extra details needed for this service
- error_key: k,
- 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) {
- var geoType = feature.type.toLowerCase();
-
- // Average of recorded points should land on the missing geometry
- // Missing geometry could happen to land on another error
- var loc = pointAverage(feature.points);
- loc = preventCoincident(loc, false);
-
- var d = new qaError({
- // Info required for every error
- loc: loc,
- service: 'improveOSM',
- error_type: k + '-' + geoType,
- // Extra details needed for this service
- error_key: k,
- identifier: { x: feature.x, y: feature.y },
- status: feature.status
- });
-
- d.replacements = {
- num_trips: feature.numberOfTrips,
- geometry_type: t('QA.improveOSM.geometry_types.' + geoType)
- };
-
- // -1 trips indicates data came from a 3rd party
- if (feature.numberOfTrips === -1) {
- d.desc = t('QA.improveOSM.error_types.mr.description_alt', d.replacements);
- }
-
- _erCache.data[d.id] = d;
- _erCache.rtree.insert(encodeErrorRtree(d));
- });
- }
-
- // Entities at high zoom == turn restrictions
- if (data.entities) {
- data.entities.forEach(function(feature) {
- // Turn restrictions could be missing at same junction
- // We also want to bump the error up so node is accessible
- var loc = feature.point;
- 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];
-
- var d = new qaError({
- // Info required for every error
- loc: loc,
- service: 'improveOSM',
- error_type: k,
- // Extra details needed for this service
- error_key: k,
- identifier: feature.id,
- object_id: via_node,
- object_type: 'node',
- status: feature.status
- });
-
- // 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));
-
- // 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));
- dispatch.call('loaded');
- });
- }
- })
- .catch(function() {
- delete _erCache.inflightTile[tile.id][k];
- if (!Object.keys(_erCache.inflightTile[tile.id]).length) {
- delete _erCache.inflightTile[tile.id];
- _erCache.loadedTile[tile.id] = true;
- }
- });
- });
-
- _erCache.inflightTile[tile.id] = requests;
- });
- },
-
- getComments: function(d, callback) {
- // If comments already retrieved no need to do so again
- if (d.comments !== undefined) {
- if (callback) callback({}, d);
- return;
- }
-
- var key = d.error_key;
- var qParams = {};
-
- if (key === 'ow') {
- qParams = d.identifier;
- } else if (key === 'mr') {
- qParams.tileX = d.identifier.x;
- qParams.tileY = d.identifier.y;
- } else if (key === 'tr') {
- qParams.targetId = d.identifier;
- }
-
- var url = _impOsmUrls[key] + '/retrieveComments?' + utilQsString(qParams);
-
- var that = this;
- d3_json(url)
- .then(function(data) {
- // Assign directly for immediate use in the callback
- // comments are served newest to oldest
- d.comments = data.comments ? data.comments.reverse() : [];
- that.replaceError(d);
- if (callback) callback(null, d);
- })
- .catch(function(err) {
- if (callback) callback(err.message);
- });
- },
-
- postUpdate: function(d, callback) {
- if (!serviceOsm.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
- serviceOsm.userDetails(sendPayload);
-
- function sendPayload(err, user) {
- if (err) { return callback(err, d); }
-
- var key = d.error_key;
- var url = _impOsmUrls[key] + '/comment';
- var payload = {
- username: user.display_name,
- targetIds: [ d.identifier ]
- };
-
- if (d.newStatus !== undefined) {
- payload.status = d.newStatus;
- payload.text = 'status changed';
- }
-
- // Comment take place of default text
- if (d.newComment !== undefined) {
- payload.text = d.newComment;
- }
-
- var controller = new AbortController();
- _erCache.inflightPost[d.id] = controller;
-
- var options = {
- method: 'POST',
- signal: controller.signal,
- body: JSON.stringify(payload)
- };
-
- d3_json(url, options)
- .then(function() {
- delete _erCache.inflightPost[d.id];
-
- // Just a comment, update error in cache
- if (d.newStatus === undefined) {
- var now = new Date();
- var comments = d.comments ? d.comments : [];
-
- comments.push({
- username: payload.username,
- text: payload.text,
- timestamp: now.getTime() / 1000
- });
-
- that.replaceError(d.update({
- comments: comments,
- newComment: undefined
- }));
- } else {
- that.removeError(d);
- if (d.newStatus === 'SOLVED') {
- // No error identifier, so we give a count of each category
- if (!(d.error_key in _erCache.closed)) {
- _erCache.closed[d.error_key] = 0;
- }
- _erCache.closed[d.error_key] += 1;
- }
- }
- if (callback) callback(null, d);
- })
- .catch(function(err) {
- delete _erCache.inflightPost[d.id];
- if (callback) callback(err.message);
- });
- }
- },
-
-
- // get all cached errors covering the viewport
- getErrors: function(projection) {
- var viewport = projection.clipExtent();
- var min = [viewport[0][0], viewport[1][1]];
- var max = [viewport[1][0], viewport[0][1]];
- var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();
-
- return _erCache.rtree.search(bbox).map(function(d) {
- return d.data;
- });
- },
-
- // get a single error from the cache
- getError: function(id) {
- return _erCache.data[id];
- },
-
- // replace a single error in the cache
- replaceError: function(error) {
- if (!(error instanceof qaError) || !error.id) return;
-
- _erCache.data[error.id] = error;
- updateRtree(encodeErrorRtree(error), true); // true = replace
- return error;
- },
-
- // remove a single error from the cache
- removeError: function(error) {
- if (!(error instanceof qaError) || !error.id) return;
-
- delete _erCache.data[error.id];
- updateRtree(encodeErrorRtree(error), false); // false = remove
- },
-
- // Used to populate `closed:improveosm` changeset tag
- getClosedCounts: function() {
- return _erCache.closed;
+ init() {
+ if (!_cache) {
+ this.reset();
}
-};
\ No newline at end of file
+
+ this.event = utilRebind(this, dispatch, 'on');
+ },
+
+ reset() {
+ if (_cache) {
+ Object.values(_cache.inflightTile).forEach(abortRequest);
+ }
+ _cache = {
+ data: {},
+ loadedTile: {},
+ inflightTile: {},
+ inflightPost: {},
+ closed: {},
+ rtree: new RBush()
+ };
+ },
+
+ loadIssues(projection) {
+ const 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
+ const tiles = tiler
+ .zoomExtent([_tileZoom, _tileZoom])
+ .getTiles(projection);
+
+ // abort inflight requests that are no longer needed
+ abortUnwantedRequests(_cache, tiles);
+
+ // issue new requests..
+ tiles.forEach(tile => {
+ if (_cache.loadedTile[tile.id] || _cache.inflightTile[tile.id]) return;
+
+ const [ east, north, west, south ] = tile.extent.rectangle();
+ const params = Object.assign({}, options, { east, south, west, north });
+
+ // 3 separate requests to store for each tile
+ const requests = {};
+
+ Object.keys(_impOsmUrls).forEach(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
+ const kParams = Object.assign({},
+ params,
+ (k === 'mr') ? { type: 'PARKING,ROAD,BOTH,PATH' } : { confidenceLevel: 'C1' }
+ );
+ const url = `${_impOsmUrls[k]}/search?` + utilQsString(kParams);
+ const controller = new AbortController();
+
+ requests[k] = controller;
+
+ d3_json(url, { signal: controller.signal })
+ .then(data => {
+ delete _cache.inflightTile[tile.id][k];
+ if (!Object.keys(_cache.inflightTile[tile.id]).length) {
+ delete _cache.inflightTile[tile.id];
+ _cache.loadedTile[tile.id] = true;
+ }
+
+ // Road segments at high zoom == oneways
+ if (data.roadSegments) {
+ data.roadSegments.forEach(feature => {
+ // Position error at the approximate middle of the segment
+ const { points, wayId, fromNodeId, toNodeId } = feature;
+ const itemId = `${wayId}${fromNodeId}${toNodeId}`;
+ let mid = points.length / 2;
+ let 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);
+
+ let d = new QAItem(loc, 'improveOSM', k, itemId, {
+ issueKey: k, // used as a category
+ identifier: { // used to post changes
+ wayId,
+ fromNodeId,
+ toNodeId
+ },
+ objectId: wayId,
+ objectType: 'way'
+ });
+
+ // 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)
+ };
+
+ _cache.data[d.id] = d;
+ _cache.rtree.insert(encodeIssueRtree(d));
+ });
+ }
+
+ // Tiles at high zoom == missing roads
+ if (data.tiles) {
+ data.tiles.forEach(feature => {
+ const { type, x, y, numberOfTrips } = feature;
+ const geoType = type.toLowerCase();
+ const itemId = `${geoType}${x}${y}${numberOfTrips}`;
+
+ // Average of recorded points should land on the missing geometry
+ // Missing geometry could happen to land on another error
+ let loc = pointAverage(feature.points);
+ loc = preventCoincident(loc, false);
+
+ let d = new QAItem(loc, 'improveOSM', `${k}-${geoType}`, itemId, {
+ issueKey: k,
+ identifier: { x, y }
+ });
+
+ d.replacements = {
+ num_trips: numberOfTrips,
+ geometry_type: t(`QA.improveOSM.geometry_types.${geoType}`)
+ };
+
+ // -1 trips indicates data came from a 3rd party
+ if (numberOfTrips === -1) {
+ d.desc = t('QA.improveOSM.error_types.mr.description_alt', d.replacements);
+ }
+
+ _cache.data[d.id] = d;
+ _cache.rtree.insert(encodeIssueRtree(d));
+ });
+ }
+
+ // Entities at high zoom == turn restrictions
+ if (data.entities) {
+ data.entities.forEach(feature => {
+ const { point, id, segments, numberOfPasses, turnType } = feature;
+ const itemId = `${id.replace(/[,:+#]/g, "_")}`;
+
+ // Turn restrictions could be missing at same junction
+ // We also want to bump the error up so node is accessible
+ const loc = preventCoincident([point.lon, point.lat], true);
+
+ // Elements are presented in a strange way
+ const ids = id.split(',');
+ const from_way = ids[0];
+ const via_node = ids[3];
+ const to_way = ids[2].split(':')[1];
+
+ let d = new QAItem(loc, 'improveOSM', k, itemId, {
+ issueKey: k,
+ identifier: id,
+ objectId: via_node,
+ objectType: 'node'
+ });
+
+ // Travel direction along from_way clarifies the turn restriction
+ const [ p1, p2 ] = segments[0].points;
+ const dir_of_travel = cardinalDirection(relativeBearing(p1, p2));
+
+ // Variables used in the description
+ d.replacements = {
+ num_passed: numberOfPasses,
+ num_trips: segments[0].numberOfTrips,
+ turn_restriction: 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'))
+ };
+
+ _cache.data[d.id] = d;
+ _cache.rtree.insert(encodeIssueRtree(d));
+ dispatch.call('loaded');
+ });
+ }
+ })
+ .catch(() => {
+ delete _cache.inflightTile[tile.id][k];
+ if (!Object.keys(_cache.inflightTile[tile.id]).length) {
+ delete _cache.inflightTile[tile.id];
+ _cache.loadedTile[tile.id] = true;
+ }
+ });
+ });
+
+ _cache.inflightTile[tile.id] = requests;
+ });
+ },
+
+ getComments(d, callback) {
+ // If comments already retrieved no need to do so again
+ if (d.comments) {
+ if (callback) callback({}, d);
+ return;
+ }
+
+ const key = d.issueKey;
+ let qParams = {};
+
+ if (key === 'ow') {
+ qParams = d.identifier;
+ } else if (key === 'mr') {
+ qParams.tileX = d.identifier.x;
+ qParams.tileY = d.identifier.y;
+ } else if (key === 'tr') {
+ qParams.targetId = d.identifier;
+ }
+
+ const url = `${_impOsmUrls[key]}/retrieveComments?` + utilQsString(qParams);
+ const after = data => {
+ // Assign directly for immediate use in the callback
+ // comments are served newest to oldest
+ d.comments = data.comments ? data.comments.reverse() : [];
+ this.replaceItem(d);
+ if (callback) callback(null, d);
+ };
+
+ d3_json(url)
+ .then(after)
+ .catch(err => {
+ if (callback) callback(err.message);
+ });
+ },
+
+ postUpdate(d, callback) {
+ if (!serviceOsm.authenticated()) { // Username required in payload
+ return callback({ message: 'Not Authenticated', status: -3}, d);
+ }
+ if (_cache.inflightPost[d.id]) {
+ return callback({ message: 'Error update already inflight', status: -2 }, d);
+ }
+
+ // Payload can only be sent once username is established
+ serviceOsm.userDetails(sendPayload.bind(this));
+
+ function sendPayload(err, user) {
+ if (err) { return callback(err, d); }
+
+ const key = d.issueKey;
+ const url = `${_impOsmUrls[key]}/comment`;
+ const payload = {
+ username: user.display_name,
+ targetIds: [ d.identifier ]
+ };
+
+ if (d.newStatus) {
+ payload.status = d.newStatus;
+ payload.text = 'status changed';
+ }
+
+ // Comment take place of default text
+ if (d.newComment) {
+ payload.text = d.newComment;
+ }
+
+ const controller = new AbortController();
+ _cache.inflightPost[d.id] = controller;
+
+ const options = {
+ method: 'POST',
+ signal: controller.signal,
+ body: JSON.stringify(payload)
+ };
+
+ d3_json(url, options)
+ .then(() => {
+ delete _cache.inflightPost[d.id];
+
+ // Just a comment, update error in cache
+ if (!d.newStatus) {
+ const now = new Date();
+ let comments = d.comments ? d.comments : [];
+
+ comments.push({
+ username: payload.username,
+ text: payload.text,
+ timestamp: now.getTime() / 1000
+ });
+
+ this.replaceItem(d.update({
+ comments: comments,
+ newComment: undefined
+ }));
+ } else {
+ this.removeItem(d);
+ if (d.newStatus === 'SOLVED') {
+ // Keep track of the number of issues closed per type to tag the changeset
+ if (!(d.issueKey in _cache.closed)) {
+ _cache.closed[d.issueKey] = 0;
+ }
+ _cache.closed[d.issueKey] += 1;
+ }
+ }
+ if (callback) callback(null, d);
+ })
+ .catch(err => {
+ delete _cache.inflightPost[d.id];
+ if (callback) callback(err.message);
+ });
+ }
+ },
+
+
+ // Get all cached QAItems covering the viewport
+ getItems(projection) {
+ const viewport = projection.clipExtent();
+ const min = [viewport[0][0], viewport[1][1]];
+ const max = [viewport[1][0], viewport[0][1]];
+ const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();
+
+ return _cache.rtree.search(bbox).map(d => d.data);
+ },
+
+ // Get a QAItem from cache
+ // NOTE: Don't change method name until UI v3 is merged
+ getError(id) {
+ return _cache.data[id];
+ },
+
+ // Replace a single QAItem in the cache
+ replaceItem(issue) {
+ if (!(issue instanceof QAItem) || !issue.id) return;
+
+ _cache.data[issue.id] = issue;
+ updateRtree(encodeIssueRtree(issue), true); // true = replace
+ return issue;
+ },
+
+ // Remove a single QAItem from the cache
+ removeItem(issue) {
+ if (!(issue instanceof QAItem) || !issue.id) return;
+
+ delete _cache.data[issue.id];
+ updateRtree(encodeIssueRtree(issue), false); // false = remove
+ },
+
+ // Used to populate `closed:improveosm:*` changeset tags
+ getClosedCounts() {
+ return _cache.closed;
+ }
+};
diff --git a/modules/services/keepRight.js b/modules/services/keepRight.js
index 48e21da0a..66bfd74b5 100644
--- a/modules/services/keepRight.js
+++ b/modules/services/keepRight.js
@@ -4,509 +4,496 @@ import { dispatch as d3_dispatch } from 'd3-dispatch';
import { json as d3_json } from 'd3-fetch';
import { geoExtent, geoVecAdd } from '../geo';
-import { qaError } from '../osm';
+import { QAItem } from '../osm';
import { t } from '../util/locale';
import { utilRebind, utilTiler, utilQsString } from '../util';
import { errorTypes, localizeStrings } from '../../data/keepRight.json';
+const tiler = utilTiler();
+const dispatch = d3_dispatch('loaded');
+const _tileZoom = 14;
+const _krUrlRoot = 'https://www.keepright.at';
-var tiler = utilTiler();
-var dispatch = d3_dispatch('loaded');
+// This gets reassigned if reset
+let _cache;
-var _krCache;
-var _krZoom = 14;
-var _krUrlRoot = 'https://www.keepright.at/';
-
-var _krRuleset = [
- // no 20 - multiple node on same spot - these are mostly boundaries overlapping roads
- 30, 40, 50, 60, 70, 90, 100, 110, 120, 130, 150, 160, 170, 180,
- 190, 191, 192, 193, 194, 195, 196, 197, 198,
- 200, 201, 202, 203, 204, 205, 206, 207, 208, 210, 220,
- 230, 231, 232, 270, 280, 281, 282, 283, 284, 285,
- 290, 291, 292, 293, 294, 295, 296, 297, 298, 300, 310, 311, 312, 313,
- 320, 350, 360, 370, 380, 390, 400, 401, 402, 410, 411, 412, 413
+const _krRuleset = [
+ // no 20 - multiple node on same spot - these are mostly boundaries overlapping roads
+ 30, 40, 50, 60, 70, 90, 100, 110, 120, 130, 150, 160, 170, 180,
+ 190, 191, 192, 193, 194, 195, 196, 197, 198,
+ 200, 201, 202, 203, 204, 205, 206, 207, 208, 210, 220,
+ 230, 231, 232, 270, 280, 281, 282, 283, 284, 285,
+ 290, 291, 292, 293, 294, 295, 296, 297, 298, 300, 310, 311, 312, 313,
+ 320, 350, 360, 370, 380, 390, 400, 401, 402, 410, 411, 412, 413
];
function abortRequest(controller) {
- if (controller) {
- controller.abort();
- }
+ if (controller) {
+ controller.abort();
+ }
}
function abortUnwantedRequests(cache, tiles) {
- Object.keys(cache.inflightTile).forEach(function(k) {
- var wanted = tiles.find(function(tile) { return k === tile.id; });
- if (!wanted) {
- abortRequest(cache.inflightTile[k]);
- delete cache.inflightTile[k];
- }
- });
-}
-
-
-function encodeErrorRtree(d) {
- return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d };
-}
-
-
-// replace or remove error from rtree
-function updateRtree(item, replace) {
- _krCache.rtree.remove(item, function isEql(a, b) {
- return a.data.id === b.data.id;
- });
-
- if (replace) {
- _krCache.rtree.insert(item);
+ Object.keys(cache.inflightTile).forEach(k => {
+ const wanted = tiles.find(tile => k === tile.id);
+ if (!wanted) {
+ abortRequest(cache.inflightTile[k]);
+ delete cache.inflightTile[k];
}
+ });
+}
+
+
+function encodeIssueRtree(d) {
+ return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d };
+}
+
+
+// Replace or remove QAItem from rtree
+function updateRtree(item, replace) {
+ _cache.rtree.remove(item, (a, b) => a.data.id === b.data.id);
+
+ if (replace) {
+ _cache.rtree.insert(item);
+ }
}
function tokenReplacements(d) {
- if (!(d instanceof qaError)) return;
+ if (!(d instanceof QAItem)) return;
- var htmlRegex = new RegExp(/<\/[a-z][\s\S]*>/);
- var replacements = {};
+ const htmlRegex = new RegExp(/<\/[a-z][\s\S]*>/);
+ const replacements = {};
- var errorTemplate = errorTypes[d.which_type];
- if (!errorTemplate) {
- /* eslint-disable no-console */
- console.log('No Template: ', d.which_type);
- console.log(' ', d.description);
- /* eslint-enable no-console */
- return;
+ const issueTemplate = errorTypes[d.whichType];
+ if (!issueTemplate) {
+ /* eslint-disable no-console */
+ console.log('No Template: ', d.whichType);
+ console.log(' ', d.description);
+ /* eslint-enable no-console */
+ return;
+ }
+
+ // some descriptions are just fixed text
+ if (!issueTemplate.regex) return;
+
+ // regex pattern should match description with variable details captured
+ const errorRegex = new RegExp(issueTemplate.regex, 'i');
+ const errorMatch = errorRegex.exec(d.description);
+ if (!errorMatch) {
+ /* eslint-disable no-console */
+ console.log('Unmatched: ', d.whichType);
+ console.log(' ', d.description);
+ console.log(' ', errorRegex);
+ /* eslint-enable no-console */
+ return;
+ }
+
+ for (let i = 1; i < errorMatch.length; i++) { // skip first
+ let capture = errorMatch[i];
+ let idType;
+
+ idType = 'IDs' in issueTemplate ? issueTemplate.IDs[i-1] : '';
+ if (idType && capture) { // link IDs if present in the capture
+ capture = parseError(capture, idType);
+ } else if (htmlRegex.test(capture)) { // escape any html in non-IDs
+ capture = '\\' + capture + '\\';
+ } else {
+ const compare = capture.toLowerCase();
+ if (localizeStrings[compare]) { // some replacement strings can be localized
+ capture = t('QA.keepRight.error_parts.' + localizeStrings[compare]);
+ }
}
- // some descriptions are just fixed text
- if (!errorTemplate.regex) return;
+ replacements['var' + i] = capture;
+ }
- // regex pattern should match description with variable details captured
- var errorRegex = new RegExp(errorTemplate.regex, 'i');
- var errorMatch = errorRegex.exec(d.description);
- if (!errorMatch) {
- /* eslint-disable no-console */
- console.log('Unmatched: ', d.which_type);
- console.log(' ', d.description);
- console.log(' ', errorRegex);
- /* eslint-enable no-console */
- return;
- }
-
- for (var i = 1; i < errorMatch.length; i++) { // skip first
- var capture = errorMatch[i];
- var idType;
-
- idType = 'IDs' in errorTemplate ? errorTemplate.IDs[i-1] : '';
- if (idType && capture) { // link IDs if present in the capture
- capture = parseError(capture, idType);
- } else if (htmlRegex.test(capture)) { // escape any html in non-IDs
- capture = '\\' + capture + '\\';
- } else {
- var compare = capture.toLowerCase();
- if (localizeStrings[compare]) { // some replacement strings can be localized
- capture = t('QA.keepRight.error_parts.' + localizeStrings[compare]);
- }
- }
-
- replacements['var' + i] = capture;
- }
-
- return replacements;
+ return replacements;
}
function parseError(capture, idType) {
- var compare = capture.toLowerCase();
- if (localizeStrings[compare]) { // some replacement strings can be localized
- capture = t('QA.keepRight.error_parts.' + localizeStrings[compare]);
+ const compare = capture.toLowerCase();
+ if (localizeStrings[compare]) { // some replacement strings can be localized
+ capture = t('QA.keepRight.error_parts.' + localizeStrings[compare]);
+ }
+
+ switch (idType) {
+ // link a string like "this node"
+ case 'this':
+ capture = linkErrorObject(capture);
+ break;
+
+ case 'url':
+ capture = linkURL(capture);
+ break;
+
+ // link an entity ID
+ case 'n':
+ case 'w':
+ case 'r':
+ capture = linkEntity(idType + capture);
+ break;
+
+ // some errors have more complex ID lists/variance
+ case '20':
+ capture = parse20(capture);
+ break;
+ case '211':
+ capture = parse211(capture);
+ break;
+ case '231':
+ capture = parse231(capture);
+ break;
+ case '294':
+ capture = parse294(capture);
+ break;
+ case '370':
+ capture = parse370(capture);
+ break;
+ }
+
+ return capture;
+
+
+ function linkErrorObject(d) {
+ return `${d}`;
+ }
+
+ function linkEntity(d) {
+ return `${d}`;
+ }
+
+ function linkURL(d) {
+ return `${d}`;
+ }
+
+ // arbitrary node list of form: #ID, #ID, #ID...
+ function parse211(capture) {
+ let newList = [];
+ const items = capture.split(', ');
+
+ items.forEach(item => {
+ // ID has # at the front
+ let id = linkEntity('n' + item.slice(1));
+ newList.push(id);
+ });
+
+ return newList.join(', ');
+ }
+
+ // arbitrary way list of form: #ID(layer),#ID(layer),#ID(layer)...
+ function parse231(capture) {
+ let newList = [];
+ // unfortunately 'layer' can itself contain commas, so we split on '),'
+ const items = capture.split('),');
+
+ items.forEach(item => {
+ const match = item.match(/\#(\d+)\((.+)\)?/);
+ if (match !== null && match.length > 2) {
+ newList.push(linkEntity('w' + match[1]) + ' ' +
+ t('QA.keepRight.errorTypes.231.layer', { layer: match[2] })
+ );
+ }
+ });
+
+ return newList.join(', ');
+ }
+
+ // arbitrary node/relation list of form: from node #ID,to relation #ID,to node #ID...
+ function parse294(capture) {
+ let newList = [];
+ const items = capture.split(',');
+
+ items.forEach(item => {
+ // item of form "from/to node/relation #ID"
+ item = item.split(' ');
+
+ // to/from role is more clear in quotes
+ const role = `"${item[0]}"`;
+
+ // first letter of node/relation provides the type
+ const idType = item[1].slice(0,1);
+
+ // ID has # at the front
+ let id = item[2].slice(1);
+ id = linkEntity(idType + id);
+
+ newList.push(`${role} ${item[1]} ${id}`);
+ });
+
+ return newList.join(', ');
+ }
+
+ // may or may not include the string "(including the name 'name')"
+ function parse370(capture) {
+ if (!capture) return '';
+
+ const match = capture.match(/\(including the name (\'.+\')\)/);
+ if (match && match.length) {
+ return t('QA.keepRight.errorTypes.370.including_the_name', { name: match[1] });
}
+ return '';
+ }
- switch (idType) {
- // link a string like "this node"
- case 'this':
- capture = linkErrorObject(capture);
- break;
+ // arbitrary node list of form: #ID,#ID,#ID...
+ function parse20(capture) {
+ let newList = [];
+ const items = capture.split(',');
- case 'url':
- capture = linkURL(capture);
- break;
+ items.forEach(item => {
+ // ID has # at the front
+ const id = linkEntity('n' + item.slice(1));
+ newList.push(id);
+ });
- // link an entity ID
- case 'n':
- case 'w':
- case 'r':
- capture = linkEntity(idType + capture);
- break;
-
- // some errors have more complex ID lists/variance
- case '20':
- capture = parse20(capture);
- break;
- case '211':
- capture = parse211(capture);
- break;
- case '231':
- capture = parse231(capture);
- break;
- case '294':
- capture = parse294(capture);
- break;
- case '370':
- capture = parse370(capture);
- break;
- }
-
- return capture;
-
-
- function linkErrorObject(d) {
- return '' + d + '';
- }
-
- function linkEntity(d) {
- return '' + d + '';
- }
-
- function linkURL(d) {
- return '' + d + '';
- }
-
- // arbitrary node list of form: #ID, #ID, #ID...
- function parse211(capture) {
- var newList = [];
- var items = capture.split(', ');
-
- items.forEach(function(item) {
- // ID has # at the front
- var id = linkEntity('n' + item.slice(1));
- newList.push(id);
- });
-
- return newList.join(', ');
- }
-
- // arbitrary way list of form: #ID(layer),#ID(layer),#ID(layer)...
- function parse231(capture) {
- var newList = [];
- // unfortunately 'layer' can itself contain commas, so we split on '),'
- var items = capture.split('),');
-
- items.forEach(function(item) {
- var match = item.match(/\#(\d+)\((.+)\)?/);
- if (match !== null && match.length > 2) {
- newList.push(linkEntity('w' + match[1]) + ' ' +
- t('QA.keepRight.errorTypes.231.layer', { layer: match[2] })
- );
- }
- });
-
- return newList.join(', ');
- }
-
- // arbitrary node/relation list of form: from node #ID,to relation #ID,to node #ID...
- function parse294(capture) {
- var newList = [];
- var items = capture.split(',');
-
- items.forEach(function(item) {
- var role;
- var idType;
- var id;
-
- // item of form "from/to node/relation #ID"
- item = item.split(' ');
-
- // to/from role is more clear in quotes
- role = '"' + item[0] + '"';
-
- // first letter of node/relation provides the type
- idType = item[1].slice(0,1);
-
- // ID has # at the front
- id = item[2].slice(1);
- id = linkEntity(idType + id);
-
- item = [role, item[1], id].join(' ');
- newList.push(item);
- });
-
- return newList.join(', ');
- }
-
- // may or may not include the string "(including the name 'name')"
- function parse370(capture) {
- if (!capture) return '';
-
- var match = capture.match(/\(including the name (\'.+\')\)/);
- if (match !== null && match.length) {
- return t('QA.keepRight.errorTypes.370.including_the_name', { name: match[1] });
- }
- return '';
- }
-
- // arbitrary node list of form: #ID,#ID,#ID...
- function parse20(capture) {
- var newList = [];
- var items = capture.split(',');
-
- items.forEach(function(item) {
- // ID has # at the front
- var id = linkEntity('n' + item.slice(1));
- newList.push(id);
- });
-
- return newList.join(', ');
- }
+ return newList.join(', ');
+ }
}
export default {
- init: function() {
- if (!_krCache) {
- this.reset();
- }
-
- this.event = utilRebind(this, dispatch, 'on');
- },
-
- reset: function() {
- if (_krCache) {
- Object.values(_krCache.inflightTile).forEach(abortRequest);
- }
-
- _krCache = {
- data: {},
- loadedTile: {},
- inflightTile: {},
- inflightPost: {},
- closed: {},
- rtree: new RBush()
- };
- },
-
-
- // KeepRight API: http://osm.mueschelsoft.de/keepright/interfacing.php
- loadErrors: function(projection) {
- var options = { format: 'geojson' };
- var rules = _krRuleset.join();
-
- // determine the needed tiles to cover the view
- var tiles = tiler
- .zoomExtent([_krZoom, _krZoom])
- .getTiles(projection);
-
- // abort inflight requests that are no longer needed
- abortUnwantedRequests(_krCache, tiles);
-
- // issue new requests..
- tiles.forEach(function(tile) {
- if (_krCache.loadedTile[tile.id] || _krCache.inflightTile[tile.id]) return;
-
- var rect = tile.extent.rectangle();
- var params = Object.assign({}, options, { left: rect[0], bottom: rect[3], right: rect[2], top: rect[1] });
- var url = _krUrlRoot + 'export.php?' + utilQsString(params) + '&ch=' + rules;
-
- var controller = new AbortController();
- _krCache.inflightTile[tile.id] = controller;
-
- d3_json(url, { signal: controller.signal })
- .then(function(data) {
- delete _krCache.inflightTile[tile.id];
- _krCache.loadedTile[tile.id] = true;
- if (!data || !data.features || !data.features.length) {
- throw new Error('No Data');
- }
-
- data.features.forEach(function(feature) {
- var loc = feature.geometry.coordinates;
- var props = feature.properties;
-
- // if there is a parent, save its error type e.g.:
- // Error 191 = "highway-highway"
- // Error 190 = "intersections without junctions" (parent)
- var errorType = props.error_type;
- var errorTemplate = errorTypes[errorType];
- var parentErrorType = (Math.floor(errorType / 10) * 10).toString();
-
- // try to handle error type directly, fallback to parent error type.
- var whichType = errorTemplate ? errorType : parentErrorType;
- var whichTemplate = errorTypes[whichType];
-
- // Rewrite a few of the errors at this point..
- // This is done to make them easier to linkify and translate.
- switch (whichType) {
- case '170':
- props.description = 'This feature has a FIXME tag: ' + props.description;
- break;
- case '292':
- case '293':
- props.description = props.description.replace('A turn-', 'This turn-');
- break;
- case '294':
- case '295':
- case '296':
- case '297':
- case '298':
- props.description = 'This turn-restriction~' + props.description;
- break;
- case '300':
- props.description = 'This highway is missing a maxspeed tag';
- break;
- case '411':
- case '412':
- case '413':
- props.description = 'This feature~' + props.description;
- break;
- }
-
- // - move markers slightly so it doesn't obscure the geometry,
- // - then move markers away from other coincident markers
- var coincident = false;
- do {
- // first time, move marker up. after that, move marker right.
- var delta = coincident ? [0.00001, 0] : [0, 0.00001];
- loc = geoVecAdd(loc, delta);
- var bbox = geoExtent(loc).bbox();
- coincident = _krCache.rtree.search(bbox).length;
- } while (coincident);
-
- var d = new qaError({
- // Required values
- loc: loc,
- service: 'keepRight',
- error_type: errorType,
- // Extra values for this service
- id: props.error_id,
- comment: props.comment || null,
- description: props.description || '',
- error_id: props.error_id,
- which_type: whichType,
- parent_error_type: parentErrorType,
- severity: whichTemplate.severity || 'error',
- object_id: props.object_id,
- object_type: props.object_type,
- schema: props.schema,
- title: props.title
- });
-
- d.replacements = tokenReplacements(d);
-
- _krCache.data[d.id] = d;
- _krCache.rtree.insert(encodeErrorRtree(d));
- });
-
- dispatch.call('loaded');
- })
- .catch(function() {
- delete _krCache.inflightTile[tile.id];
- _krCache.loadedTile[tile.id] = true;
- });
-
- });
- },
-
-
- postKeepRightUpdate: function(d, callback) {
- if (_krCache.inflightPost[d.id]) {
- return callback({ message: 'Error update already inflight', status: -2 }, d);
- }
-
- var that = this;
- var params = { schema: d.schema, id: d.error_id };
-
- if (d.state) {
- params.st = d.state;
- }
- if (d.newComment !== undefined) {
- params.co = d.newComment;
- }
-
- // NOTE: This throws a CORS err, but it seems successful.
- // We don't care too much about the response, so this is fine.
- var url = _krUrlRoot + 'comment.php?' + utilQsString(params);
-
- var controller = new AbortController();
- _krCache.inflightPost[d.id] = controller;
-
- fetch(url, { method: 'POST', signal: controller.signal })
- .then(function(response) {
- delete _krCache.inflightPost[d.id];
- if (!response.ok) {
- throw new Error(response.status + ' ' + response.statusText);
- }
-
- if (d.state === 'ignore') { // ignore permanently (false positive)
- that.removeError(d);
-
- } else if (d.state === 'ignore_t') { // ignore temporarily (error fixed)
- that.removeError(d);
- _krCache.closed[d.schema + ':' + d.error_id] = true;
-
- } else {
- d = that.replaceError(d.update({
- comment: d.newComment,
- newComment: undefined,
- state: undefined
- }));
- }
-
- if (callback) callback(null, d);
- })
- .catch(function(err) {
- delete _krCache.inflightPost[d.id];
- if (callback) callback(err.message);
- });
- },
-
-
- // get all cached errors covering the viewport
- getErrors: function(projection) {
- var viewport = projection.clipExtent();
- var min = [viewport[0][0], viewport[1][1]];
- var max = [viewport[1][0], viewport[0][1]];
- var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();
-
- return _krCache.rtree.search(bbox).map(function(d) {
- return d.data;
- });
- },
-
-
- // get a single error from the cache
- getError: function(id) {
- return _krCache.data[id];
- },
-
-
- // replace a single error in the cache
- replaceError: function(error) {
- if (!(error instanceof qaError) || !error.id) return;
-
- _krCache.data[error.id] = error;
- updateRtree(encodeErrorRtree(error), true); // true = replace
- return error;
- },
-
-
- // remove a single error from the cache
- removeError: function(error) {
- if (!(error instanceof qaError) || !error.id) return;
-
- delete _krCache.data[error.id];
- updateRtree(encodeErrorRtree(error), false); // false = remove
- },
-
-
- errorURL: function(error) {
- return _krUrlRoot + 'report_map.php?schema=' + error.schema + '&error=' + error.id;
- },
-
-
- // Get an array of errors closed during this session.
- // Used to populate `closed:keepright` changeset tag
- getClosedIDs: function() {
- return Object.keys(_krCache.closed).sort();
+ init() {
+ if (!_cache) {
+ this.reset();
}
+ this.event = utilRebind(this, dispatch, 'on');
+ },
+
+ reset() {
+ if (_cache) {
+ Object.values(_cache.inflightTile).forEach(abortRequest);
+ }
+
+ _cache = {
+ data: {},
+ loadedTile: {},
+ inflightTile: {},
+ inflightPost: {},
+ closed: {},
+ rtree: new RBush()
+ };
+ },
+
+
+ // KeepRight API: http://osm.mueschelsoft.de/keepright/interfacing.php
+ loadIssues(projection) {
+ const options = {
+ format: 'geojson',
+ ch: _krRuleset
+ };
+
+ // determine the needed tiles to cover the view
+ const tiles = tiler
+ .zoomExtent([_tileZoom, _tileZoom])
+ .getTiles(projection);
+
+ // abort inflight requests that are no longer needed
+ abortUnwantedRequests(_cache, tiles);
+
+ // issue new requests..
+ tiles.forEach(tile => {
+ if (_cache.loadedTile[tile.id] || _cache.inflightTile[tile.id]) return;
+
+ const [ left, top, right, bottom ] = tile.extent.rectangle();
+ const params = Object.assign({}, options, { left, bottom, right, top });
+ const url = `${_krUrlRoot}/export.php?` + utilQsString(params);
+ const controller = new AbortController();
+
+ _cache.inflightTile[tile.id] = controller;
+
+ d3_json(url, { signal: controller.signal })
+ .then(data => {
+ delete _cache.inflightTile[tile.id];
+ _cache.loadedTile[tile.id] = true;
+ if (!data || !data.features || !data.features.length) {
+ throw new Error('No Data');
+ }
+
+ data.features.forEach(feature => {
+ const {
+ properties: {
+ error_type: itemType,
+ error_id: id,
+ comment = null,
+ object_id: objectId,
+ object_type: objectType,
+ schema,
+ title
+ }
+ } = feature;
+ let {
+ geometry: { coordinates: loc },
+ properties: { description = '' }
+ } = feature;
+
+ // if there is a parent, save its error type e.g.:
+ // Error 191 = "highway-highway"
+ // Error 190 = "intersections without junctions" (parent)
+ const issueTemplate = errorTypes[itemType];
+ const parentIssueType = (Math.floor(itemType / 10) * 10).toString();
+
+ // try to handle error type directly, fallback to parent error type.
+ const whichType = issueTemplate ? itemType : parentIssueType;
+ const whichTemplate = errorTypes[whichType];
+
+ // Rewrite a few of the errors at this point..
+ // This is done to make them easier to linkify and translate.
+ switch (whichType) {
+ case '170':
+ description = `This feature has a FIXME tag: ${description}`;
+ break;
+ case '292':
+ case '293':
+ description = description.replace('A turn-', 'This turn-');
+ break;
+ case '294':
+ case '295':
+ case '296':
+ case '297':
+ case '298':
+ description = `This turn-restriction~${description}`;
+ break;
+ case '300':
+ description = 'This highway is missing a maxspeed tag';
+ break;
+ case '411':
+ case '412':
+ case '413':
+ description = `This feature~${description}`;
+ break;
+ }
+
+ // move markers slightly so it doesn't obscure the geometry,
+ // then move markers away from other coincident markers
+ let coincident = false;
+ do {
+ // first time, move marker up. after that, move marker right.
+ let delta = coincident ? [0.00001, 0] : [0, 0.00001];
+ loc = geoVecAdd(loc, delta);
+ let bbox = geoExtent(loc).bbox();
+ coincident = _cache.rtree.search(bbox).length;
+ } while (coincident);
+
+ let d = new QAItem(loc, 'keepRight', itemType, id, {
+ comment,
+ description,
+ whichType,
+ parentIssueType,
+ severity: whichTemplate.severity || 'error',
+ objectId,
+ objectType,
+ schema,
+ title
+ });
+
+ d.replacements = tokenReplacements(d);
+
+ _cache.data[id] = d;
+ _cache.rtree.insert(encodeIssueRtree(d));
+ });
+
+ dispatch.call('loaded');
+ })
+ .catch(() => {
+ delete _cache.inflightTile[tile.id];
+ _cache.loadedTile[tile.id] = true;
+ });
+
+ });
+ },
+
+
+ postUpdate(d, callback) {
+ if (_cache.inflightPost[d.id]) {
+ return callback({ message: 'Error update already inflight', status: -2 }, d);
+ }
+
+ const params = { schema: d.schema, id: d.id };
+
+ if (d.newStatus) {
+ params.st = d.newStatus;
+ }
+ if (d.newComment !== undefined) {
+ params.co = d.newComment;
+ }
+
+ // NOTE: This throws a CORS err, but it seems successful.
+ // We don't care too much about the response, so this is fine.
+ const url = `${_krUrlRoot}/comment.php?` + utilQsString(params);
+ const controller = new AbortController();
+
+ _cache.inflightPost[d.id] = controller;
+
+ // Since this is expected to throw an error just continue as if it worked
+ // (worst case scenario the request truly fails and issue will show up if iD restarts)
+ d3_json(url, { signal: controller.signal })
+ .finally(() => {
+ delete _cache.inflightPost[d.id];
+
+ if (d.newStatus === 'ignore') {
+ // ignore permanently (false positive)
+ this.removeItem(d);
+ } else if (d.newStatus === 'ignore_t') {
+ // ignore temporarily (error fixed)
+ this.removeItem(d);
+ _cache.closed[`${d.schema}:${d.id}`] = true;
+ } else {
+ d = this.replaceItem(d.update({
+ comment: d.newComment,
+ newComment: undefined,
+ newState: undefined
+ }));
+ }
+
+ if (callback) callback(null, d);
+ });
+ },
+
+ // Get all cached QAItems covering the viewport
+ getItems(projection) {
+ const viewport = projection.clipExtent();
+ const min = [viewport[0][0], viewport[1][1]];
+ const max = [viewport[1][0], viewport[0][1]];
+ const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();
+
+ return _cache.rtree.search(bbox).map(d => d.data);
+ },
+
+ // Get a QAItem from cache
+ // NOTE: Don't change method name until UI v3 is merged
+ getError(id) {
+ return _cache.data[id];
+ },
+
+ // Replace a single QAItem in the cache
+ replaceItem(item) {
+ if (!(item instanceof QAItem) || !item.id) return;
+
+ _cache.data[item.id] = item;
+ updateRtree(encodeIssueRtree(item), true); // true = replace
+ return item;
+ },
+
+ // Remove a single QAItem from the cache
+ removeItem(item) {
+ if (!(item instanceof QAItem) || !item.id) return;
+
+ delete _cache.data[item.id];
+ updateRtree(encodeIssueRtree(item), false); // false = remove
+ },
+
+ issueURL(item) {
+ return `${_krUrlRoot}/report_map.php?schema=${item.schema}&error=${item.id}`;
+ },
+
+ // Get an array of issues closed during this session.
+ // Used to populate `closed:keepright` changeset tag
+ getClosedIDs() {
+ return Object.keys(_cache.closed).sort();
+ }
+
};
diff --git a/modules/services/osmose.js b/modules/services/osmose.js
index e34260e0e..d593f8eac 100644
--- a/modules/services/osmose.js
+++ b/modules/services/osmose.js
@@ -5,23 +5,21 @@ import { json as d3_json } from 'd3-fetch';
import { currentLocale } from '../util/locale';
import { geoExtent, geoVecAdd } from '../geo';
-import { qaError } from '../osm';
+import { QAItem } from '../osm';
import { utilRebind, utilTiler, utilQsString } from '../util';
-import { services as qaServices } from '../../data/qa_errors.json';
+import * as qaServices from '../../data/qa_data.json';
const tiler = utilTiler();
const dispatch = d3_dispatch('loaded');
+const _tileZoom = 14;
const _osmoseUrlRoot = 'https://osmose.openstreetmap.fr/en/api/0.3beta';
const _osmoseItems =
- Object.keys(qaServices.osmose.errorIcons)
+ Object.keys(qaServices.osmose.icons)
.map(s => s.split('-')[0])
.reduce((unique, item) => unique.indexOf(item) !== -1 ? unique : [...unique, item], []);
-const _erZoom = 14;
-const _stringCache = {};
-const _colorCache = {};
// This gets reassigned if reset
-let _erCache;
+let _cache;
function abortRequest(controller) {
if (controller) {
@@ -39,20 +37,20 @@ function abortUnwantedRequests(cache, tiles) {
});
}
-function encodeErrorRtree(d) {
+function encodeIssueRtree(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
+// Replace or remove QAItem from rtree
function updateRtree(item, replace) {
- _erCache.rtree.remove(item, (a, b) => a.data.id === b.data.id);
+ _cache.rtree.remove(item, (a, b) => a.data.id === b.data.id);
if (replace) {
- _erCache.rtree.insert(item);
+ _cache.rtree.insert(item);
}
}
-// Errors shouldn't obscure eachother
+// Issues shouldn't obscure eachother
function preventCoincident(loc) {
let coincident = false;
do {
@@ -60,7 +58,7 @@ function preventCoincident(loc) {
let delta = coincident ? [0.00001, 0] : [0, 0.00001];
loc = geoVecAdd(loc, delta);
let bbox = geoExtent(loc).bbox();
- coincident = _erCache.rtree.search(bbox).length;
+ coincident = _cache.rtree.search(bbox).length;
} while (coincident);
return loc;
@@ -68,7 +66,7 @@ function preventCoincident(loc) {
export default {
init() {
- if (!_erCache) {
+ if (!_cache) {
this.reset();
}
@@ -76,77 +74,72 @@ export default {
},
reset() {
- if (_erCache) {
- Object.values(_erCache.inflightTile).forEach(abortRequest);
+ if (_cache) {
+ Object.values(_cache.inflightTile).forEach(abortRequest);
}
- _erCache = {
+ _cache = {
data: {},
loadedTile: {},
inflightTile: {},
inflightPost: {},
closed: {},
- rtree: new RBush()
+ rtree: new RBush(),
+ strings: {},
+ colors: {}
};
},
- loadErrors(projection) {
+ loadIssues(projection) {
let params = {
- // Tiles return a maximum # of errors
+ // Tiles return a maximum # of issues
// So we want to filter our request for only types iD supports
item: _osmoseItems
};
// determine the needed tiles to cover the view
let tiles = tiler
- .zoomExtent([_erZoom, _erZoom])
+ .zoomExtent([_tileZoom, _tileZoom])
.getTiles(projection);
// abort inflight requests that are no longer needed
- abortUnwantedRequests(_erCache, tiles);
+ abortUnwantedRequests(_cache, tiles);
// issue new requests..
tiles.forEach(tile => {
- if (_erCache.loadedTile[tile.id] || _erCache.inflightTile[tile.id]) return;
+ if (_cache.loadedTile[tile.id] || _cache.inflightTile[tile.id]) return;
let [ x, y, z ] = tile.xyz;
let url = `${_osmoseUrlRoot}/issues/${z}/${x}/${y}.json?` + utilQsString(params);
let controller = new AbortController();
- _erCache.inflightTile[tile.id] = controller;
+ _cache.inflightTile[tile.id] = controller;
d3_json(url, { signal: controller.signal })
.then(data => {
- delete _erCache.inflightTile[tile.id];
- _erCache.loadedTile[tile.id] = true;
+ delete _cache.inflightTile[tile.id];
+ _cache.loadedTile[tile.id] = true;
if (data.features) {
data.features.forEach(issue => {
- const { item, class: error_class, uuid: identifier } = issue.properties;
- // Item is the type of error, w/ class tells us the sub-type
- const error_type = `${item}-${error_class}`;
+ const { item, class: cl, uuid: id } = issue.properties;
+ /* Osmose issues are uniquely identified by a unique
+ `item` and `class` combination (both integer values) */
+ const itemType = `${item}-${cl}`;
- // Filter out unsupported error types (some are too specific or advanced)
- if (error_type in qaServices.osmose.errorIcons) {
+ // Filter out unsupported issue types (some are too specific or advanced)
+ if (itemType in qaServices.osmose.icons) {
let loc = issue.geometry.coordinates; // lon, lat
loc = preventCoincident(loc);
- let d = new qaError({
- // Info required for every error
- loc,
- service: 'osmose',
- error_type,
- // Extra details needed for this service
- identifier, // needed to query and update the error
- item // category of the issue for styling
- });
+ let d = new QAItem(loc, 'osmose', itemType, id, { item });
- // Setting elems here prevents UI error detail requests
- if (d.item === 8300 || d.item === 8360) {
+ // Setting elems here prevents UI detail requests
+ if (item === 8300 || item === 8360) {
d.elems = [];
}
- _erCache.data[d.id] = d;
- _erCache.rtree.insert(encodeErrorRtree(d));
+ _cache.data[d.id] = d;
+ _cache.rtree.insert(encodeIssueRtree(d));
}
});
}
@@ -154,48 +147,47 @@ export default {
dispatch.call('loaded');
})
.catch(() => {
- delete _erCache.inflightTile[tile.id];
- _erCache.loadedTile[tile.id] = true;
+ delete _cache.inflightTile[tile.id];
+ _cache.loadedTile[tile.id] = true;
});
});
},
- loadErrorDetail(d) {
- // Error details only need to be fetched once
- if (d.elems !== undefined) {
- return Promise.resolve(d);
+ loadIssueDetail(issue) {
+ // Issue details only need to be fetched once
+ if (issue.elems !== undefined) {
+ return Promise.resolve(issue);
}
- const url = `${_osmoseUrlRoot}/issue/${d.identifier}?langs=${currentLocale}`;
+ const url = `${_osmoseUrlRoot}/issue/${issue.id}?langs=${currentLocale}`;
const cacheDetails = data => {
// Associated elements used for highlighting
// Assign directly for immediate use in the callback
- d.elems = data.elems.map(e => e.type.substring(0,1) + e.id);
+ issue.elems = data.elems.map(e => e.type.substring(0,1) + e.id);
// Some issues have instance specific detail in a subtitle
- d.detail = data.subtitle;
+ issue.detail = data.subtitle;
- this.replaceError(d);
+ this.replaceItem(issue);
};
- return jsonPromise(url, cacheDetails)
- .then(() => d);
+ return jsonPromise(url, cacheDetails).then(() => issue);
},
loadStrings(callback, locale=currentLocale) {
- const issueTypes = Object.keys(qaServices.osmose.errorIcons);
+ const items = Object.keys(qaServices.osmose.icons);
if (
- locale in _stringCache
- && Object.keys(_stringCache[locale]).length === issueTypes.length
+ locale in _cache.strings
+ && Object.keys(_cache.strings[locale]).length === items.length
) {
- if (callback) callback(null, _stringCache[locale]);
+ if (callback) callback(null, _cache.strings[locale]);
return;
}
// May be partially populated already if some requests were successful
- if (!(locale in _stringCache)) {
- _stringCache[locale] = {};
+ if (!(locale in _cache.strings)) {
+ _cache.strings[locale] = {};
}
const format = string => {
@@ -206,9 +198,9 @@ export default {
// Only need to cache strings for supported issue types
// Using multiple individual item + class requests to reduce fetched data size
- const allRequests = issueTypes.map(issueType => {
+ const allRequests = items.map(itemType => {
// No need to request data we already have
- if (issueType in _stringCache[locale]) return;
+ if (itemType in _cache.strings[locale]) return;
const cacheData = data => {
// Bunch of nested single value arrays of objects
@@ -219,7 +211,7 @@ export default {
// If null default value is reached, data wasn't as expected (or was empty)
if (!cl) {
/* eslint-disable no-console */
- console.log(`Osmose strings request (${issueType}) had unexpected data`);
+ console.log(`Osmose strings request (${itemType}) had unexpected data`);
/* eslint-enable no-console */
return;
}
@@ -227,7 +219,7 @@ export default {
// Cache served item colors to automatically style issue markers later
const { item: itemInt, color } = item;
if (/^#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/.test(color)) {
- _colorCache[itemInt] = color;
+ _cache.colors[itemInt] = color;
}
// Value of root key will be null if no string exists
@@ -240,10 +232,10 @@ export default {
if (trap) issueStrings.trap = format(trap.auto);
if (fix) issueStrings.fix = format(fix.auto);
- _stringCache[locale][issueType] = issueStrings;
+ _cache.strings[locale][itemType] = issueStrings;
};
- const [ item, cl ] = issueType.split('-');
+ const [ item, cl ] = itemType.split('-');
// Osmose API falls back to English strings where untranslated or if locale doesn't exist
const url = `${_osmoseUrlRoot}/items/${item}/class/${cl}?langs=${locale}`;
@@ -252,88 +244,87 @@ export default {
});
Promise.all(allRequests)
- .then(() => { if (callback) callback(null, _stringCache[locale]); })
+ .then(() => { if (callback) callback(null, _cache.strings[locale]); })
.catch(err => { if (callback) callback(err); });
},
- getStrings(issueType, locale=currentLocale) {
+ getStrings(itemType, locale=currentLocale) {
// No need to fallback to English, Osmose API handles this for us
- return (locale in _stringCache) ? _stringCache[locale][issueType] : {};
+ return (locale in _cache.strings) ? _cache.strings[locale][itemType] : {};
},
getColor(itemType) {
- return (itemType in _colorCache) ? _colorCache[itemType] : '#FFFFFF';
+ return (itemType in _cache.colors) ? _cache.colors[itemType] : '#FFFFFF';
},
- postUpdate(d, callback) {
- if (_erCache.inflightPost[d.id]) {
- return callback({ message: 'Error update already inflight', status: -2 }, d);
+ postUpdate(issue, callback) {
+ if (_cache.inflightPost[issue.id]) {
+ return callback({ message: 'Issue update already inflight', status: -2 }, issue);
}
// UI sets the status to either 'done' or 'false'
- let url = `${_osmoseUrlRoot}/issue/${d.identifier}/${d.newStatus}`;
+ const url = `${_osmoseUrlRoot}/issue/${issue.id}/${issue.newStatus}`;
+ const controller = new AbortController();
+ const after = () => {
+ delete _cache.inflightPost[issue.id];
- let controller = new AbortController();
- _erCache.inflightPost[d.id] = controller;
+ this.removeItem(issue);
+ if (issue.newStatus === 'done') {
+ // Keep track of the number of issues closed per `item` to tag the changeset
+ if (!(issue.item in _cache.closed)) {
+ _cache.closed[issue.item] = 0;
+ }
+ _cache.closed[issue.item] += 1;
+ }
+ if (callback) callback(null, issue);
+ };
+
+ _cache.inflightPost[issue.id] = controller;
fetch(url, { signal: controller.signal })
- .then(() => {
- delete _erCache.inflightPost[d.id];
-
- this.removeError(d);
- if (d.newStatus === 'done') {
- // No error identifier, so we give a count of each category
- if (!(d.item in _erCache.closed)) {
- _erCache.closed[d.item] = 0;
- }
- _erCache.closed[d.item] += 1;
- }
- if (callback) callback(null, d);
- })
+ .then(after)
.catch(err => {
- delete _erCache.inflightPost[d.id];
+ delete _cache.inflightPost[issue.id];
if (callback) callback(err.message);
});
},
+ // Get all cached QAItems covering the viewport
+ getItems(projection) {
+ const viewport = projection.clipExtent();
+ const min = [viewport[0][0], viewport[1][1]];
+ const max = [viewport[1][0], viewport[0][1]];
+ const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();
- // get all cached errors covering the viewport
- getErrors(projection) {
- let viewport = projection.clipExtent();
- let min = [viewport[0][0], viewport[1][1]];
- let max = [viewport[1][0], viewport[0][1]];
- let bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();
-
- return _erCache.rtree.search(bbox).map(d => {
- return d.data;
- });
+ return _cache.rtree.search(bbox).map(d => d.data);
},
- // get a single error from the cache
+ // Get a QAItem from cache
+ // NOTE: Don't change method name until UI v3 is merged
getError(id) {
- return _erCache.data[id];
+ return _cache.data[id];
},
- // replace a single error in the cache
- replaceError(error) {
- if (!(error instanceof qaError) || !error.id) return;
+ // Replace a single QAItem in the cache
+ replaceItem(item) {
+ if (!(item instanceof QAItem) || !item.id) return;
- _erCache.data[error.id] = error;
- updateRtree(encodeErrorRtree(error), true); // true = replace
- return error;
+ _cache.data[item.id] = item;
+ updateRtree(encodeIssueRtree(item), true); // true = replace
+ return item;
},
- // remove a single error from the cache
- removeError(error) {
- if (!(error instanceof qaError) || !error.id) return;
+ // Remove a single QAItem from the cache
+ removeItem(item) {
+ if (!(item instanceof QAItem) || !item.id) return;
- delete _erCache.data[error.id];
- updateRtree(encodeErrorRtree(error), false); // false = remove
+ delete _cache.data[item.id];
+ updateRtree(encodeIssueRtree(item), false); // false = remove
},
// Used to populate `closed:osmose:*` changeset tags
getClosedCounts() {
- return _erCache.closed;
+ return _cache.closed;
}
};
diff --git a/modules/svg/improveOSM.js b/modules/svg/improveOSM.js
index 9dee08553..867732116 100644
--- a/modules/svg/improveOSM.js
+++ b/modules/svg/improveOSM.js
@@ -5,256 +5,232 @@ import { modeBrowse } from '../modes/browse';
import { svgPointTransform } from './helpers';
import { services } from '../services';
-var _improveOsmEnabled = false;
-var _errorService;
-
+let _layerEnabled = false;
+let _qaService;
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;
+ const throttledRedraw = _throttle(() => dispatch.call('change'), 1000);
+ const minZoom = 12;
- 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');
+ let touchLayer = d3_select(null);
+ let drawLayer = d3_select(null);
+ let layerVisible = 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 issues
+ function getService() {
+ if (services.improveOSM && !_qaService) {
+ _qaService = services.improveOSM;
+ _qaService.on('loaded', throttledRedraw);
+ } else if (!services.improveOSM && _qaService) {
+ _qaService = null;
}
+ return _qaService;
+ }
- // 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 markers
+ function editOn() {
+ if (!layerVisible) {
+ layerVisible = true;
+ drawLayer
+ .style('display', 'block');
}
+ }
-
- // Show the errors
- function editOn() {
- if (!_improveOsmVisible) {
- _improveOsmVisible = true;
- drawLayer
- .style('display', 'block');
- }
+ // Immediately remove the markers and their touch targets
+ function editOff() {
+ if (layerVisible) {
+ layerVisible = false;
+ drawLayer
+ .style('display', 'none');
+ drawLayer.selectAll('.qaItem.improveOSM')
+ .remove();
+ touchLayer.selectAll('.qaItem.improveOSM')
+ .remove();
}
+ }
+ // Enable the layer. This shows the markers and transitions them to visible.
+ function layerOn() {
+ editOn();
- // Immediately remove the errors and their touch targets
- function editOff() {
- if (_improveOsmVisible) {
- _improveOsmVisible = false;
- drawLayer
- .style('display', 'none');
- drawLayer.selectAll('.qa_error.improveOSM')
- .remove();
- touchLayer.selectAll('.qa_error.improveOSM')
- .remove();
- }
- }
+ drawLayer
+ .style('opacity', 0)
+ .transition()
+ .duration(250)
+ .style('opacity', 1)
+ .on('end interrupt', () => dispatch.call('change'));
+ }
+ // Disable the layer. This transitions the layer invisible and then hides the markers.
+ function layerOff() {
+ throttledRedraw.cancel();
+ drawLayer.interrupt();
+ touchLayer.selectAll('.qaItem.improveOSM')
+ .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.improveOSM')
- .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.improveOSM')
- .data(data, function(d) { return d.id; });
-
- // exit
- markers.exit()
- .remove();
-
- // enter
- var markersEnter = markers.enter()
- .append('g')
- .attr('class', function(d) {
- return [
- 'qa_error',
- d.service,
- 'error_id-' + d.id,
- 'error_type-' + d.error_type
- ].join(' ');
- });
-
- markersEnter
- .append('polygon')
- .call(markerPath, 'shadow');
-
- markersEnter
- .append('ellipse')
- .attr('cx', 0)
- .attr('cy', 0)
- .attr('rx', 4.5)
- .attr('ry', 2)
- .attr('class', 'stroke');
-
- markersEnter
- .append('polygon')
- .attr('fill', 'currentColor')
- .call(markerPath, 'qa_error-fill');
-
- markersEnter
- .append('use')
- .attr('transform', 'translate(-5.5, -21)')
- .attr('class', 'icon-annotation')
- .attr('width', '11px')
- .attr('height', '11px')
- .attr('xlink:href', function(d) {
- var picon = d.icon;
-
- if (!picon) {
- return '';
- } else {
- var isMaki = /^maki-/.test(picon);
- return '#' + picon + (isMaki ? '-11' : '');
- }
- });
-
- // update
- markers
- .merge(markersEnter)
- .sort(sortY)
- .classed('selected', function(d) { return d.id === selectedID; })
- .attr('transform', getTransform);
-
-
- // Draw targets..
- if (touchLayer.empty()) return;
- var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
-
- var targets = touchLayer.selectAll('.qa_error.improveOSM')
- .data(data, function(d) { return d.id; });
-
- // exit
- targets.exit()
- .remove();
-
- // enter/update
- targets.enter()
- .append('rect')
- .attr('width', '20px')
- .attr('height', '30px')
- .attr('x', '-10px')
- .attr('y', '-28px')
- .merge(targets)
- .sort(sortY)
- .attr('class', function(d) {
- return 'qa_error ' + d.service + ' target error_id-' + d.id + ' ' + fillClass;
- })
- .attr('transform', getTransform);
-
-
- function sortY(a, b) {
- return (a.id === selectedID) ? 1
- : (b.id === selectedID) ? -1
- : b.loc[1] - a.loc[1];
- }
- }
-
-
- // Draw the 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));
- }
- }
-
+ drawLayer
+ .transition()
+ .duration(250)
+ .style('opacity', 0)
+ .on('end interrupt', () => {
+ editOff();
dispatch.call('change');
- return this;
- };
+ });
+ }
+
+ // Update the issue markers
+ function updateMarkers() {
+ if (!layerVisible || !_layerEnabled) return;
+
+ const service = getService();
+ const selectedID = context.selectedErrorID();
+ const data = (service ? service.getItems(projection) : []);
+ const getTransform = svgPointTransform(projection);
+
+ // Draw markers..
+ const markers = drawLayer.selectAll('.qaItem.improveOSM')
+ .data(data, d => d.id);
+
+ // exit
+ markers.exit()
+ .remove();
+
+ // enter
+ const markersEnter = markers.enter()
+ .append('g')
+ .attr('class', d => `qaItem ${d.service} itemId-${d.id} itemType-${d.itemType}`);
+
+ 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, 'qaItem-fill');
+
+ markersEnter
+ .append('use')
+ .attr('transform', 'translate(-5.5, -21)')
+ .attr('class', 'icon-annotation')
+ .attr('width', '11px')
+ .attr('height', '11px')
+ .attr('xlink:href', d => {
+ const picon = d.icon;
+
+ if (!picon) {
+ return '';
+ } else {
+ const isMaki = /^maki-/.test(picon);
+ return `#${picon}${isMaki ? '-11' : ''}`;
+ }
+ });
+
+ // update
+ markers
+ .merge(markersEnter)
+ .sort(sortY)
+ .classed('selected', d => d.id === selectedID)
+ .attr('transform', getTransform);
- drawImproveOSM.supported = function() {
- return !!getService();
- };
+ // Draw targets..
+ if (touchLayer.empty()) return;
+ const fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
+ const targets = touchLayer.selectAll('.qaItem.improveOSM')
+ .data(data, d => d.id);
- return drawImproveOSM;
-}
\ No newline at end of file
+ // 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', d => `qaItem ${d.service} target ${fillClass} itemId-${d.id}`)
+ .attr('transform', getTransform);
+
+ function sortY(a, b) {
+ return (a.id === selectedID) ? 1
+ : (b.id === selectedID) ? -1
+ : b.loc[1] - a.loc[1];
+ }
+ }
+
+ // Draw the ImproveOSM layer and schedule loading issues and updating markers.
+ function drawImproveOSM(selection) {
+ const service = getService();
+
+ const 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', _layerEnabled ? 'block' : 'none')
+ .merge(drawLayer);
+
+ if (_layerEnabled) {
+ if (service && ~~context.map().zoom() >= minZoom) {
+ editOn();
+ service.loadIssues(projection);
+ updateMarkers();
+ } else {
+ editOff();
+ }
+ }
+ }
+
+ // Toggles the layer on and off
+ drawImproveOSM.enabled = function(val) {
+ if (!arguments.length) return _layerEnabled;
+
+ _layerEnabled = val;
+ if (_layerEnabled) {
+ layerOn();
+ } else {
+ layerOff();
+ if (context.selectedErrorID()) {
+ context.enter(modeBrowse(context));
+ }
+ }
+
+ dispatch.call('change');
+ return this;
+ };
+
+ drawImproveOSM.supported = () => !!getService();
+
+ return drawImproveOSM;
+}
diff --git a/modules/svg/keepRight.js b/modules/svg/keepRight.js
index 8e19bab42..08c4f578c 100644
--- a/modules/svg/keepRight.js
+++ b/modules/svg/keepRight.js
@@ -5,246 +5,222 @@ import { modeBrowse } from '../modes/browse';
import { svgPointTransform } from './helpers';
import { services } from '../services';
-var _keepRightEnabled = false;
-var _keepRightService;
-
+let _layerEnabled = false;
+let _qaService;
export function svgKeepRight(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 _keepRightVisible = false;
+ const throttledRedraw = _throttle(() => dispatch.call('change'), 1000);
+ const minZoom = 12;
+ let touchLayer = d3_select(null);
+ let drawLayer = d3_select(null);
+ let layerVisible = false;
- function markerPath(selection, klass) {
- selection
- .attr('class', klass)
- .attr('transform', 'translate(-4, -24)')
- .attr('d', 'M11.6,6.2H7.1l1.4-5.1C8.6,0.6,8.1,0,7.5,0H2.2C1.7,0,1.3,0.3,1.3,0.8L0,10.2c-0.1,0.6,0.4,1.1,0.9,1.1h4.6l-1.8,7.6C3.6,19.4,4.1,20,4.7,20c0.3,0,0.6-0.2,0.8-0.5l6.9-11.9C12.7,7,12.3,6.2,11.6,6.2z');
+ function markerPath(selection, klass) {
+ selection
+ .attr('class', klass)
+ .attr('transform', 'translate(-4, -24)')
+ .attr('d', 'M11.6,6.2H7.1l1.4-5.1C8.6,0.6,8.1,0,7.5,0H2.2C1.7,0,1.3,0.3,1.3,0.8L0,10.2c-0.1,0.6,0.4,1.1,0.9,1.1h4.6l-1.8,7.6C3.6,19.4,4.1,20,4.7,20c0.3,0,0.6-0.2,0.8-0.5l6.9-11.9C12.7,7,12.3,6.2,11.6,6.2z');
+ }
+
+ // Loosely-coupled keepRight service for fetching issues.
+ function getService() {
+ if (services.keepRight && !_qaService) {
+ _qaService = services.keepRight;
+ _qaService.on('loaded', throttledRedraw);
+ } else if (!services.keepRight && _qaService) {
+ _qaService = null;
}
+ return _qaService;
+ }
- // Loosely-coupled keepRight service for fetching errors.
- function getService() {
- if (services.keepRight && !_keepRightService) {
- _keepRightService = services.keepRight;
- _keepRightService.on('loaded', throttledRedraw);
- } else if (!services.keepRight && _keepRightService) {
- _keepRightService = null;
- }
-
- return _keepRightService;
+ // Show the markers
+ function editOn() {
+ if (!layerVisible) {
+ layerVisible = true;
+ drawLayer
+ .style('display', 'block');
}
+ }
-
- // Show the errors
- function editOn() {
- if (!_keepRightVisible) {
- _keepRightVisible = true;
- drawLayer
- .style('display', 'block');
- }
+ // Immediately remove the markers and their touch targets
+ function editOff() {
+ if (layerVisible) {
+ layerVisible = false;
+ drawLayer
+ .style('display', 'none');
+ drawLayer.selectAll('.qaItem.keepRight')
+ .remove();
+ touchLayer.selectAll('.qaItem.keepRight')
+ .remove();
}
+ }
+ // Enable the layer. This shows the markers and transitions them to visible.
+ function layerOn() {
+ editOn();
- // Immediately remove the errors and their touch targets
- function editOff() {
- if (_keepRightVisible) {
- _keepRightVisible = false;
- drawLayer
- .style('display', 'none');
- drawLayer.selectAll('.qa_error.keepRight')
- .remove();
- touchLayer.selectAll('.qa_error.keepRight')
- .remove();
- }
- }
+ drawLayer
+ .style('opacity', 0)
+ .transition()
+ .duration(250)
+ .style('opacity', 1)
+ .on('end interrupt', () => dispatch.call('change'));
+ }
+ // Disable the layer. This transitions the layer invisible and then hides the markers.
+ function layerOff() {
+ throttledRedraw.cancel();
+ drawLayer.interrupt();
+ touchLayer.selectAll('.qaItem.keepRight')
+ .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.keepRight')
- .remove();
-
- drawLayer
- .transition()
- .duration(250)
- .style('opacity', 0)
- .on('end interrupt', function () {
- editOff();
- dispatch.call('change');
- });
- }
-
-
- // Update the error markers
- function updateMarkers() {
- if (!_keepRightVisible || !_keepRightEnabled) 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.keepRight')
- .data(data, function(d) { return d.id; });
-
- // exit
- markers.exit()
- .remove();
-
- // enter
- var markersEnter = markers.enter()
- .append('g')
- .attr('class', function(d) {
- return [
- 'qa_error',
- d.service,
- 'error_id-' + d.id,
- 'error_type-' + d.parent_error_type
- ].join(' ');
- });
-
- markersEnter
- .append('ellipse')
- .attr('cx', 0.5)
- .attr('cy', 1)
- .attr('rx', 6.5)
- .attr('ry', 3)
- .attr('class', 'stroke');
-
- markersEnter
- .append('path')
- .call(markerPath, 'shadow');
-
- markersEnter
- .append('use')
- .attr('class', 'qa_error-fill')
- .attr('width', '20px')
- .attr('height', '20px')
- .attr('x', '-8px')
- .attr('y', '-22px')
- .attr('xlink:href', '#iD-icon-bolt');
-
- // 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.keepRight')
- .data(data, function(d) { return d.id; });
-
- // exit
- targets.exit()
- .remove();
-
- // enter/update
- targets.enter()
- .append('rect')
- .attr('width', '20px')
- .attr('height', '20px')
- .attr('x', '-8px')
- .attr('y', '-22px')
- .merge(targets)
- .sort(sortY)
- .attr('class', function(d) {
- return 'qa_error ' + d.service + ' target error_id-' + d.id + ' ' + fillClass;
- })
- .attr('transform', getTransform);
-
-
- function sortY(a, b) {
- return (a.id === selectedID) ? 1
- : (b.id === selectedID) ? -1
- : (a.severity === 'error' && b.severity !== 'error') ? 1
- : (b.severity === 'error' && a.severity !== 'error') ? -1
- : b.loc[1] - a.loc[1];
- }
- }
-
-
- // Draw the keepRight layer and schedule loading errors and updating markers.
- function drawKeepRight(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-keepRight')
- .data(service ? [0] : []);
-
- drawLayer.exit()
- .remove();
-
- drawLayer = drawLayer.enter()
- .append('g')
- .attr('class', 'layer-keepRight')
- .style('display', _keepRightEnabled ? 'block' : 'none')
- .merge(drawLayer);
-
- if (_keepRightEnabled) {
- if (service && ~~context.map().zoom() >= minZoom) {
- editOn();
- service.loadErrors(projection);
- updateMarkers();
- } else {
- editOff();
- }
- }
- }
-
-
- // Toggles the layer on and off
- drawKeepRight.enabled = function(val) {
- if (!arguments.length) return _keepRightEnabled;
-
- _keepRightEnabled = val;
- if (_keepRightEnabled) {
- layerOn();
- } else {
- layerOff();
- if (context.selectedErrorID()) {
- context.enter(modeBrowse(context));
- }
- }
-
+ drawLayer
+ .transition()
+ .duration(250)
+ .style('opacity', 0)
+ .on('end interrupt', () => {
+ editOff();
dispatch.call('change');
- return this;
- };
+ });
+ }
+
+ // Update the issue markers
+ function updateMarkers() {
+ if (!layerVisible || !_layerEnabled) return;
+
+ const service = getService();
+ const selectedID = context.selectedErrorID();
+ const data = (service ? service.getItems(projection) : []);
+ const getTransform = svgPointTransform(projection);
+
+ // Draw markers..
+ const markers = drawLayer.selectAll('.qaItem.keepRight')
+ .data(data, d => d.id);
+
+ // exit
+ markers.exit()
+ .remove();
+
+ // enter
+ const markersEnter = markers.enter()
+ .append('g')
+ .attr('class', d => `qaItem ${d.service} itemId-${d.id} itemType-${d.parentIssueType}`);
+
+ markersEnter
+ .append('ellipse')
+ .attr('cx', 0.5)
+ .attr('cy', 1)
+ .attr('rx', 6.5)
+ .attr('ry', 3)
+ .attr('class', 'stroke');
+
+ markersEnter
+ .append('path')
+ .call(markerPath, 'shadow');
+
+ markersEnter
+ .append('use')
+ .attr('class', 'qaItem-fill')
+ .attr('width', '20px')
+ .attr('height', '20px')
+ .attr('x', '-8px')
+ .attr('y', '-22px')
+ .attr('xlink:href', '#iD-icon-bolt');
+
+ // update
+ markers
+ .merge(markersEnter)
+ .sort(sortY)
+ .classed('selected', d => d.id === selectedID)
+ .attr('transform', getTransform);
- drawKeepRight.supported = function() {
- return !!getService();
- };
+ // Draw targets..
+ if (touchLayer.empty()) return;
+ const fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
+
+ const targets = touchLayer.selectAll('.qaItem.keepRight')
+ .data(data, d => d.id);
+
+ // exit
+ targets.exit()
+ .remove();
+
+ // enter/update
+ targets.enter()
+ .append('rect')
+ .attr('width', '20px')
+ .attr('height', '20px')
+ .attr('x', '-8px')
+ .attr('y', '-22px')
+ .merge(targets)
+ .sort(sortY)
+ .attr('class', d => `qaItem ${d.service} target ${fillClass} itemId-${d.id}`)
+ .attr('transform', getTransform);
- return drawKeepRight;
+ 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 keepRight layer and schedule loading issues and updating markers.
+ function drawKeepRight(selection) {
+ const service = getService();
+
+ const surface = context.surface();
+ if (surface && !surface.empty()) {
+ touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');
+ }
+
+ drawLayer = selection.selectAll('.layer-keepRight')
+ .data(service ? [0] : []);
+
+ drawLayer.exit()
+ .remove();
+
+ drawLayer = drawLayer.enter()
+ .append('g')
+ .attr('class', 'layer-keepRight')
+ .style('display', _layerEnabled ? 'block' : 'none')
+ .merge(drawLayer);
+
+ if (_layerEnabled) {
+ if (service && ~~context.map().zoom() >= minZoom) {
+ editOn();
+ service.loadIssues(projection);
+ updateMarkers();
+ } else {
+ editOff();
+ }
+ }
+ }
+
+ // Toggles the layer on and off
+ drawKeepRight.enabled = function(val) {
+ if (!arguments.length) return _layerEnabled;
+
+ _layerEnabled = val;
+ if (_layerEnabled) {
+ layerOn();
+ } else {
+ layerOff();
+ if (context.selectedErrorID()) {
+ context.enter(modeBrowse(context));
+ }
+ }
+
+ dispatch.call('change');
+ return this;
+ };
+
+ drawKeepRight.supported = () => !!getService();
+
+ return drawKeepRight;
}
diff --git a/modules/svg/osmose.js b/modules/svg/osmose.js
index 1fb565080..e3e7d6617 100644
--- a/modules/svg/osmose.js
+++ b/modules/svg/osmose.js
@@ -5,261 +5,237 @@ import { modeBrowse } from '../modes/browse';
import { svgPointTransform } from './helpers';
import { services } from '../services';
-var _osmoseEnabled = false;
-var _errorService;
-
+let _layerEnabled = false;
+let _qaService;
export function svgOsmose(projection, context, dispatch) {
- var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
- var minZoom = 12;
- var touchLayer = d3_select(null);
- var drawLayer = d3_select(null);
- var _osmoseVisible = false;
+ const throttledRedraw = _throttle(() => dispatch.call('change'), 1000);
+ const minZoom = 12;
- 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');
+ let touchLayer = d3_select(null);
+ let drawLayer = d3_select(null);
+ let layerVisible = false;
+
+ function markerPath(selection, klass) {
+ selection
+ .attr('class', klass)
+ .attr('transform', 'translate(-10, -28)')
+ .attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6');
+ }
+
+ // Loosely-coupled osmose service for fetching issues
+ function getService() {
+ if (services.osmose && !_qaService) {
+ _qaService = services.osmose;
+ _qaService.on('loaded', throttledRedraw);
+ } else if (!services.osmose && _qaService) {
+ _qaService = null;
}
+ return _qaService;
+ }
- // Loosely-coupled osmose service for fetching errors.
- function getService() {
- if (services.osmose && !_errorService) {
- _errorService = services.osmose;
- _errorService.on('loaded', throttledRedraw);
- } else if (!services.osmose && _errorService) {
- _errorService = null;
- }
-
- return _errorService;
+ // Show the markers
+ function editOn() {
+ if (!layerVisible) {
+ layerVisible = true;
+ drawLayer
+ .style('display', 'block');
}
+ }
-
- // Show the errors
- function editOn() {
- if (!_osmoseVisible) {
- _osmoseVisible = true;
- drawLayer
- .style('display', 'block');
- }
+ // Immediately remove the markers and their touch targets
+ function editOff() {
+ if (layerVisible) {
+ layerVisible = false;
+ drawLayer
+ .style('display', 'none');
+ drawLayer.selectAll('.qaItem.osmose')
+ .remove();
+ touchLayer.selectAll('.qaItem.osmose')
+ .remove();
}
+ }
+ // Enable the layer. This shows the markers and transitions them to visible.
+ function layerOn() {
+ // Strings supplied by Osmose fetched before showing layer for first time
+ // NOTE: Currently no way to change locale in iD at runtime, would need to re-call this method if that's ever implemented
+ // FIXME: If layer is toggled quickly multiple requests are sent
+ // FIXME: No error handling in place
+ getService().loadStrings(editOn);
- // Immediately remove the errors and their touch targets
- function editOff() {
- if (_osmoseVisible) {
- _osmoseVisible = false;
- drawLayer
- .style('display', 'none');
- drawLayer.selectAll('.qa_error.osmose')
- .remove();
- touchLayer.selectAll('.qa_error.osmose')
- .remove();
- }
- }
-
-
- // Enable the layer. This shows the errors and transitions them to visible.
- function layerOn() {
- // Strings supplied by Osmose fetched before showing layer for first time
- // NOTE: Currently no way to change locale in iD at runtime, would need to re-call this method if that's ever implemented
- // FIXME: If layer is toggled quickly multiple requests are sent
- // FIXME: No error handling in place
- getService().loadStrings(editOn);
-
- drawLayer
- .style('opacity', 0)
- .transition()
- .duration(250)
- .style('opacity', 1)
- .on('end interrupt', function () {
- dispatch.call('change');
- });
- }
-
-
- // Disable the layer. This transitions the layer invisible and then hides the errors.
- function layerOff() {
- throttledRedraw.cancel();
- drawLayer.interrupt();
- touchLayer.selectAll('.qa_error.osmose')
- .remove();
-
- drawLayer
- .transition()
- .duration(250)
- .style('opacity', 0)
- .on('end interrupt', function () {
- editOff();
- dispatch.call('change');
- });
- }
-
-
- // Update the error markers
- function updateMarkers() {
- if (!_osmoseVisible || !_osmoseEnabled) return;
-
- var service = getService();
- var selectedID = context.selectedErrorID();
- var data = (service ? service.getErrors(projection) : []);
- var getTransform = svgPointTransform(projection);
-
- // Draw markers..
- var markers = drawLayer.selectAll('.qa_error.osmose')
- .data(data, function(d) { return d.id; });
-
- // exit
- markers.exit()
- .remove();
-
- // enter
- var markersEnter = markers.enter()
- .append('g')
- .attr('class', function(d) {
- return [
- 'qa_error',
- d.service,
- 'error_id-' + d.id,
- 'error_type-' + d.error_type,
- 'item-' + d.item
- ].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', d => getService().getColor(d.item))
- .call(markerPath, 'qa_error-fill');
-
- markersEnter
- .append('use')
- .attr('transform', 'translate(-5.5, -21)')
- .attr('class', 'icon-annotation')
- .attr('width', '11px')
- .attr('height', '11px')
- .attr('xlink:href', function(d) {
- var picon = d.icon;
-
- if (!picon) {
- return '';
- } else {
- var isMaki = /^maki-/.test(picon);
- return '#' + picon + (isMaki ? '-11' : '');
- }
- });
-
- // update
- markers
- .merge(markersEnter)
- .sort(sortY)
- .classed('selected', function(d) { return d.id === selectedID; })
- .attr('transform', getTransform);
-
-
- // Draw targets..
- if (touchLayer.empty()) return;
- var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
-
- var targets = touchLayer.selectAll('.qa_error.osmose')
- .data(data, function(d) { return d.id; });
-
- // exit
- targets.exit()
- .remove();
-
- // enter/update
- targets.enter()
- .append('rect')
- .attr('width', '20px')
- .attr('height', '30px')
- .attr('x', '-10px')
- .attr('y', '-28px')
- .merge(targets)
- .sort(sortY)
- .attr('class', function(d) {
- return 'qa_error ' + d.service + ' target error_id-' + d.id + ' ' + fillClass;
- })
- .attr('transform', getTransform);
-
-
- function sortY(a, b) {
- return (a.id === selectedID) ? 1
- : (b.id === selectedID) ? -1
- : b.loc[1] - a.loc[1];
- }
- }
-
-
- // Draw the Osmose layer and schedule loading errors and updating markers.
- function drawOsmose(selection) {
- var service = getService();
-
- var surface = context.surface();
- if (surface && !surface.empty()) {
- touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');
- }
-
- drawLayer = selection.selectAll('.layer-osmose')
- .data(service ? [0] : []);
-
- drawLayer.exit()
- .remove();
-
- drawLayer = drawLayer.enter()
- .append('g')
- .attr('class', 'layer-osmose')
- .style('display', _osmoseEnabled ? 'block' : 'none')
- .merge(drawLayer);
-
- if (_osmoseEnabled) {
- if (service && ~~context.map().zoom() >= minZoom) {
- editOn();
- service.loadErrors(projection);
- updateMarkers();
- } else {
- editOff();
- }
- }
- }
-
-
- // Toggles the layer on and off
- drawOsmose.enabled = function(val) {
- if (!arguments.length) return _osmoseEnabled;
-
- _osmoseEnabled = val;
- if (_osmoseEnabled) {
- layerOn();
- } else {
- layerOff();
- if (context.selectedErrorID()) {
- context.enter(modeBrowse(context));
- }
- }
-
+ drawLayer
+ .style('opacity', 0)
+ .transition()
+ .duration(250)
+ .style('opacity', 1)
+ .on('end interrupt', () => {
dispatch.call('change');
- return this;
- };
+ });
+ }
+ // Disable the layer. This transitions the layer invisible and then hides the markers.
+ function layerOff() {
+ throttledRedraw.cancel();
+ drawLayer.interrupt();
+ touchLayer.selectAll('.qaItem.osmose')
+ .remove();
- drawOsmose.supported = function() {
- return !!getService();
- };
+ drawLayer
+ .transition()
+ .duration(250)
+ .style('opacity', 0)
+ .on('end interrupt', () => {
+ editOff();
+ dispatch.call('change');
+ });
+ }
+ // Update the issue markers
+ function updateMarkers() {
+ if (!layerVisible || !_layerEnabled) return;
- return drawOsmose;
+ const service = getService();
+ const selectedID = context.selectedErrorID();
+ const data = (service ? service.getItems(projection) : []);
+ const getTransform = svgPointTransform(projection);
+
+ // Draw markers..
+ const markers = drawLayer.selectAll('.qaItem.osmose')
+ .data(data, d => d.id);
+
+ // exit
+ markers.exit()
+ .remove();
+
+ // enter
+ const markersEnter = markers.enter()
+ .append('g')
+ .attr('class', d => `qaItem ${d.service} itemId-${d.id} itemType-${d.itemType}`);
+
+ 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', d => service.getColor(d.item))
+ .call(markerPath, 'qaItem-fill');
+
+ markersEnter
+ .append('use')
+ .attr('transform', 'translate(-5.5, -21)')
+ .attr('class', 'icon-annotation')
+ .attr('width', '11px')
+ .attr('height', '11px')
+ .attr('xlink:href', d => {
+ const picon = d.icon;
+
+ if (!picon) {
+ return '';
+ } else {
+ const isMaki = /^maki-/.test(picon);
+ return `#${picon}${isMaki ? '-11' : ''}`;
+ }
+ });
+
+ // update
+ markers
+ .merge(markersEnter)
+ .sort(sortY)
+ .classed('selected', d => d.id === selectedID)
+ .attr('transform', getTransform);
+
+ // Draw targets..
+ if (touchLayer.empty()) return;
+ const fillClass = context.getDebug('target') ? 'pink' : 'nocolor';
+
+ const targets = touchLayer.selectAll('.qaItem.osmose')
+ .data(data, d => 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', d => `qaItem ${d.service} target ${fillClass} itemId-${d.id}`)
+ .attr('transform', getTransform);
+
+ function sortY(a, b) {
+ return (a.id === selectedID) ? 1
+ : (b.id === selectedID) ? -1
+ : b.loc[1] - a.loc[1];
+ }
+ }
+
+ // Draw the Osmose layer and schedule loading issues and updating markers.
+ function drawOsmose(selection) {
+ const service = getService();
+
+ const surface = context.surface();
+ if (surface && !surface.empty()) {
+ touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');
+ }
+
+ drawLayer = selection.selectAll('.layer-osmose')
+ .data(service ? [0] : []);
+
+ drawLayer.exit()
+ .remove();
+
+ drawLayer = drawLayer.enter()
+ .append('g')
+ .attr('class', 'layer-osmose')
+ .style('display', _layerEnabled ? 'block' : 'none')
+ .merge(drawLayer);
+
+ if (_layerEnabled) {
+ if (service && ~~context.map().zoom() >= minZoom) {
+ editOn();
+ service.loadIssues(projection);
+ updateMarkers();
+ } else {
+ editOff();
+ }
+ }
+ }
+
+ // Toggles the layer on and off
+ drawOsmose.enabled = function(val) {
+ if (!arguments.length) return _layerEnabled;
+
+ _layerEnabled = val;
+ if (_layerEnabled) {
+ layerOn();
+ } else {
+ layerOff();
+ if (context.selectedErrorID()) {
+ context.enter(modeBrowse(context));
+ }
+ }
+
+ dispatch.call('change');
+ return this;
+ };
+
+ drawOsmose.supported = () => !!getService();
+
+ return drawOsmose;
}
diff --git a/modules/ui/commit.js b/modules/ui/commit.js
index 3adf1cabb..e3955ca9f 100644
--- a/modules/ui/commit.js
+++ b/modules/ui/commit.js
@@ -138,7 +138,7 @@ export function uiCommit(context) {
// assign tags for closed issues and notes
var osmClosed = osm.getClosedIDs();
- var issueType;
+ var itemType;
if (osmClosed.length) {
tags['closed:note'] = osmClosed.join(';').substr(0, tagCharLimit);
}
@@ -150,14 +150,14 @@ export function uiCommit(context) {
}
if (services.improveOSM) {
var iOsmClosed = services.improveOSM.getClosedCounts();
- for (issueType in iOsmClosed) {
- tags['closed:improveosm:' + issueType] = iOsmClosed[issueType].toString().substr(0, tagCharLimit);
+ for (itemType in iOsmClosed) {
+ tags['closed:improveosm:' + itemType] = iOsmClosed[itemType].toString().substr(0, tagCharLimit);
}
}
if (services.osmose) {
var osmoseClosed = services.osmose.getClosedCounts();
- for (issueType in osmoseClosed) {
- tags['closed:osmose:' + issueType] = osmoseClosed[issueType].toString().substr(0, tagCharLimit);
+ for (itemType in osmoseClosed) {
+ tags['closed:osmose:' + itemType] = osmoseClosed[itemType].toString().substr(0, tagCharLimit);
}
}
diff --git a/modules/ui/improveOSM_comments.js b/modules/ui/improveOSM_comments.js
index e1ee629b1..a80d52ddd 100644
--- a/modules/ui/improveOSM_comments.js
+++ b/modules/ui/improveOSM_comments.js
@@ -6,89 +6,86 @@ import { services } from '../services';
import { utilDetect } from '../util/detect';
export function uiImproveOsmComments() {
- var _error;
+ let _qaItem;
+ function issueComments(selection) {
+ // make the div immediately so it appears above the buttons
+ let comments = selection.selectAll('.comments-container')
+ .data([0]);
- function errorComments(selection) {
- // make the div immediately so it appears above the buttons
- var comments = selection.selectAll('.comments-container')
- .data([0]);
+ comments = comments.enter()
+ .append('div')
+ .attr('class', 'comments-container')
+ .merge(comments);
- comments = comments.enter()
- .append('div')
- .attr('class', 'comments-container')
- .merge(comments);
+ // must retrieve comments from API before they can be displayed
+ services.improveOSM.getComments(_qaItem, (err, d) => {
+ if (!d.comments) return; // nothing to do here
- // must retrieve comments from API before they can be displayed
- services.improveOSM.getComments(_error, function(err, d) {
- if (!d.comments) { return; } // nothing to do here
+ const commentEnter = comments.selectAll('.comment')
+ .data(d.comments)
+ .enter()
+ .append('div')
+ .attr('class', 'comment');
- var commentEnter = comments.selectAll('.comment')
- .data(d.comments)
- .enter()
- .append('div')
- .attr('class', 'comment');
+ commentEnter
+ .append('div')
+ .attr('class', 'comment-avatar')
+ .call(svgIcon('#iD-icon-avatar', 'comment-avatar-icon'));
- commentEnter
- .append('div')
- .attr('class', 'comment-avatar')
- .call(svgIcon('#iD-icon-avatar', 'comment-avatar-icon'));
+ const mainEnter = commentEnter
+ .append('div')
+ .attr('class', 'comment-main');
- var mainEnter = commentEnter
- .append('div')
- .attr('class', 'comment-main');
+ const metadataEnter = mainEnter
+ .append('div')
+ .attr('class', 'comment-metadata');
- var metadataEnter = mainEnter
- .append('div')
- .attr('class', 'comment-metadata');
+ metadataEnter
+ .append('div')
+ .attr('class', 'comment-author')
+ .each(function(d) {
+ const osm = services.osm;
+ let selection = d3_select(this);
+ if (osm && d.username) {
+ selection = selection
+ .append('a')
+ .attr('class', 'comment-author-link')
+ .attr('href', osm.userURL(d.username))
+ .attr('tabindex', -1)
+ .attr('target', '_blank');
+ }
+ selection
+ .text(d => d.username);
+ });
- metadataEnter
- .append('div')
- .attr('class', 'comment-author')
- .each(function(d) {
- var selection = d3_select(this);
- var osm = services.osm;
- if (osm && d.username) {
- selection = selection
- .append('a')
- .attr('class', 'comment-author-link')
- .attr('href', osm.userURL(d.username))
- .attr('tabindex', -1)
- .attr('target', '_blank');
- }
- selection
- .text(function(d) { return d.username; });
- });
+ metadataEnter
+ .append('div')
+ .attr('class', 'comment-date')
+ .text(d => t('note.status.commented', { when: localeDateString(d.timestamp) }));
- metadataEnter
- .append('div')
- .attr('class', 'comment-date')
- .text(function(d) {
- return t('note.status.commented', { when: localeDateString(d.timestamp) });
- });
+ mainEnter
+ .append('div')
+ .attr('class', 'comment-text')
+ .append('p')
+ .text(d => d.text);
+ });
+ }
- mainEnter
- .append('div')
- .attr('class', 'comment-text')
- .append('p')
- .text(function(d) { return d.text; });
- });
- }
+ function localeDateString(s) {
+ if (!s) return null;
+ const detected = utilDetect();
+ const options = { day: 'numeric', month: 'short', year: 'numeric' };
+ const d = new Date(s * 1000); // timestamp is served in seconds, date takes ms
+ if (isNaN(d.getTime())) return null;
+ return d.toLocaleDateString(detected.locale, options);
+ }
- function localeDateString(s) {
- if (!s) return null;
- var detected = utilDetect();
- var options = { day: 'numeric', month: 'short', year: 'numeric' };
- var d = new Date(s * 1000); // timestamp is served in seconds, date takes ms
- if (isNaN(d.getTime())) return null;
- return d.toLocaleDateString(detected.locale, options);
- }
+ issueComments.issue = function(val) {
+ if (!arguments.length) return _qaItem;
+ _qaItem = val;
+ return issueComments;
+ };
- errorComments.error = function(val) {
- if (!arguments.length) return _error;
- _error = val;
- return errorComments;
- };
-
- return errorComments;
+ return issueComments;
}
diff --git a/modules/ui/improveOSM_details.js b/modules/ui/improveOSM_details.js
index 435c09c51..d2b786d77 100644
--- a/modules/ui/improveOSM_details.js
+++ b/modules/ui/improveOSM_details.js
@@ -1,6 +1,6 @@
import {
- event as d3_event,
- select as d3_select
+ event as d3_event,
+ select as d3_select
} from 'd3-selection';
import { dataEn } from '../../data';
@@ -8,130 +8,125 @@ import { modeSelect } from '../modes/select';
import { t } from '../util/locale';
import { utilDisplayName, utilEntityOrMemberSelector, utilEntityRoot } from '../util';
-
export function uiImproveOsmDetails(context) {
- var _error;
+ let _qaItem;
+
+ function issueDetail(d) {
+ const unknown = t('inspector.unknown');
+
+ if (!d) return unknown;
+
+ if (d.desc) return d.desc;
+
+ const itemType = d.issueKey;
+ const et = dataEn.QA.improveOSM.error_types[itemType];
+
+ let detail;
+ if (et && et.description) {
+ detail = t(`QA.improveOSM.error_types.${itemType}.description`, d.replacements);
+ } else {
+ detail = unknown;
+ }
+
+ return detail;
+ }
+
+ function improveOsmDetails(selection) {
+ const details = selection.selectAll('.error-details')
+ .data(
+ (_qaItem ? [_qaItem] : []),
+ d => `${d.id}-${d.status || 0}`
+ );
+
+ details.exit()
+ .remove();
+
+ const detailsEnter = details.enter()
+ .append('div')
+ .attr('class', 'error-details qa-details-container');
- function errorDetail(d) {
- var unknown = t('inspector.unknown');
+ // description
+ const descriptionEnter = detailsEnter
+ .append('div')
+ .attr('class', 'qa-details-description');
- if (!d) return unknown;
+ descriptionEnter
+ .append('h4')
+ .text(() => t('QA.keepRight.detail_description'));
- if (d.desc) return d.desc;
+ descriptionEnter
+ .append('div')
+ .attr('class', 'qa-details-description-text')
+ .html(issueDetail);
- var errorType = d.error_key;
- var et = dataEn.QA.improveOSM.error_types[errorType];
+ // If there are entity links in the error message..
+ let relatedEntities = [];
+ descriptionEnter.selectAll('.error_entity_link, .error_object_link')
+ .each(function() {
+ const link = d3_select(this);
+ const isObjectLink = link.classed('error_object_link');
+ const entityID = isObjectLink ?
+ (utilEntityRoot(_qaItem.objectType) + _qaItem.objectId)
+ : this.textContent;
+ const entity = context.hasEntity(entityID);
- var detail;
- if (et && et.description) {
- detail = t('QA.improveOSM.error_types.' + errorType + '.description', d.replacements);
- } else {
- detail = unknown;
+ relatedEntities.push(entityID);
+
+ // Add click handler
+ link
+ .on('mouseenter', () => {
+ context.surface().selectAll(utilEntityOrMemberSelector([entityID], context.graph()))
+ .classed('hover', true);
+ })
+ .on('mouseleave', () => {
+ context.surface().selectAll('.hover')
+ .classed('hover', false);
+ })
+ .on('click', () => {
+ d3_event.preventDefault();
+ const osmlayer = context.layers().layer('osm');
+ if (!osmlayer.enabled()) {
+ osmlayer.enabled(true);
+ }
+
+ context.map().centerZoom(_qaItem.loc, 20);
+
+ if (entity) {
+ context.enter(modeSelect(context, [entityID]));
+ } else {
+ context.loadEntity(entityID, () => {
+ context.enter(modeSelect(context, [entityID]));
+ });
+ }
+ });
+
+ // Replace with friendly name if possible
+ // (The entity may not yet be loaded into the graph)
+ if (entity) {
+ let name = utilDisplayName(entity); // try to use common name
+
+ if (!name && !isObjectLink) {
+ const preset = context.presets().match(entity, context.graph());
+ name = preset && !preset.isFallback() && preset.name(); // fallback to preset name
+ }
+
+ if (name) {
+ this.innerText = name;
+ }
}
+ });
- return detail;
- }
-
-
- function improveOsmDetails(selection) {
- var details = selection.selectAll('.error-details')
- .data(
- (_error ? [_error] : []),
- function(d) { return d.id + '-' + (d.status || 0); }
- );
-
- details.exit()
- .remove();
-
- var detailsEnter = details.enter()
- .append('div')
- .attr('class', 'error-details error-details-container');
-
-
- // description
- var descriptionEnter = detailsEnter
- .append('div')
- .attr('class', 'error-details-description');
-
- descriptionEnter
- .append('h4')
- .text(function() { return t('QA.keepRight.detail_description'); });
-
- descriptionEnter
- .append('div')
- .attr('class', 'error-details-description-text')
- .html(errorDetail);
-
- // If there are entity links in the error message..
- var relatedEntities = [];
- descriptionEnter.selectAll('.error_entity_link, .error_object_link')
- .each(function() {
- var link = d3_select(this);
- var isObjectLink = link.classed('error_object_link');
- var entityID = isObjectLink ?
- (utilEntityRoot(_error.object_type) + _error.object_id)
- : this.textContent;
- var entity = context.hasEntity(entityID);
-
- relatedEntities.push(entityID);
-
- // Add click handler
- link
- .on('mouseenter', function() {
- context.surface().selectAll(utilEntityOrMemberSelector([entityID], context.graph()))
- .classed('hover', true);
- })
- .on('mouseleave', 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;
- }
- }
- });
-
- // Don't hide entities related to this error - #5880
- context.features().forceVisible(relatedEntities);
- context.map().pan([0,0]); // trigger a redraw
- }
-
-
- improveOsmDetails.error = function(val) {
- if (!arguments.length) return _error;
- _error = val;
- return improveOsmDetails;
- };
-
+ // Don't hide entities related to this error - #5880
+ context.features().forceVisible(relatedEntities);
+ context.map().pan([0,0]); // trigger a redraw
+ }
+ improveOsmDetails.issue = function(val) {
+ if (!arguments.length) return _qaItem;
+ _qaItem = val;
return improveOsmDetails;
-}
\ No newline at end of file
+ };
+
+ return improveOsmDetails;
+}
diff --git a/modules/ui/improveOSM_editor.js b/modules/ui/improveOSM_editor.js
index dd046627d..a09e86141 100644
--- a/modules/ui/improveOSM_editor.js
+++ b/modules/ui/improveOSM_editor.js
@@ -14,217 +14,197 @@ import { uiTooltipHtml } from './tooltipHtml';
import { utilNoAuto, utilRebind } from '../util';
-
export function uiImproveOsmEditor(context) {
- var dispatch = d3_dispatch('change');
- var errorDetails = uiImproveOsmDetails(context);
- var errorComments = uiImproveOsmComments(context);
- var errorHeader = uiImproveOsmHeader(context);
- var quickLinks = uiQuickLinks();
+ const dispatch = d3_dispatch('change');
+ const qaDetails = uiImproveOsmDetails(context);
+ const qaComments = uiImproveOsmComments(context);
+ const qaHeader = uiImproveOsmHeader(context);
+ const quickLinks = uiQuickLinks();
- var _error;
+ let _qaItem;
+ function improveOsmEditor(selection) {
+ // quick links
+ const choices = [{
+ id: 'zoom_to',
+ label: 'inspector.zoom_to.title',
+ tooltip: () => uiTooltipHtml(t('inspector.zoom_to.tooltip_qaItem'), t('inspector.zoom_to.key')),
+ click: () => context.mode().zoomToSelected()
+ }];
- 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();
- }
- }];
+ const headerEnter = selection.selectAll('.header')
+ .data([0])
+ .enter()
+ .append('div')
+ .attr('class', 'header fillL');
+ headerEnter
+ .append('button')
+ .attr('class', 'fr qa-editor-close')
+ .on('click', () => context.enter(modeBrowse(context)))
+ .call(svgIcon('#iD-icon-close'));
- var header = selection.selectAll('.header')
- .data([0]);
+ headerEnter
+ .append('h3')
+ .text(t('QA.improveOSM.title'));
- var headerEnter = header.enter()
- .append('div')
- .attr('class', 'header fillL');
+ let body = selection.selectAll('.body')
+ .data([0]);
- headerEnter
- .append('button')
- .attr('class', 'fr error-editor-close')
- .on('click', function() {
- context.enter(modeBrowse(context));
- })
- .call(svgIcon('#iD-icon-close'));
+ body = body.enter()
+ .append('div')
+ .attr('class', 'body')
+ .merge(body);
- headerEnter
- .append('h3')
- .text(t('QA.improveOSM.title'));
+ const editor = body.selectAll('.qa-editor')
+ .data([0]);
+ editor.enter()
+ .append('div')
+ .attr('class', 'modal-section qa-editor')
+ .merge(editor)
+ .call(qaHeader.issue(_qaItem))
+ .call(quickLinks.choices(choices))
+ .call(qaDetails.issue(_qaItem))
+ .call(qaComments.issue(_qaItem))
+ .call(improveOsmSaveSection);
+ }
- var body = selection.selectAll('.body')
- .data([0]);
+ function improveOsmSaveSection(selection) {
+ const isSelected = (_qaItem && _qaItem.id === context.selectedErrorID());
+ const isShown = (_qaItem && (isSelected || _qaItem.newComment || _qaItem.comment));
+ let saveSection = selection.selectAll('.qa-save')
+ .data(
+ (isShown ? [_qaItem] : []),
+ d => `${d.id}-${d.status || 0}`
+ );
- body = body.enter()
- .append('div')
- .attr('class', 'body')
- .merge(body);
+ // exit
+ saveSection.exit()
+ .remove();
- var editor = body.selectAll('.error-editor')
- .data([0]);
+ // enter
+ const saveSectionEnter = saveSection.enter()
+ .append('div')
+ .attr('class', 'qa-save save-section cf');
- editor.enter()
- .append('div')
- .attr('class', 'modal-section error-editor')
- .merge(editor)
- .call(errorHeader.error(_error))
- .call(quickLinks.choices(choices))
- .call(errorDetails.error(_error))
- .call(errorComments.error(_error))
- .call(improveOsmSaveSection);
+ saveSectionEnter
+ .append('h4')
+ .attr('class', '.qa-save-header')
+ .text(t('note.newComment'));
+
+ saveSectionEnter
+ .append('textarea')
+ .attr('class', 'new-comment-input')
+ .attr('placeholder', t('QA.keepRight.comment_placeholder'))
+ .attr('maxlength', 1000)
+ .property('value', d => d.newComment)
+ .call(utilNoAuto)
+ .on('input', changeInput)
+ .on('blur', changeInput);
+
+ // update
+ saveSection = saveSectionEnter
+ .merge(saveSection)
+ .call(qaSaveButtons);
+
+ function changeInput() {
+ const input = d3_select(this);
+ let val = input.property('value').trim();
+
+ if (val === '') {
+ val = undefined;
+ }
+
+ // store the unsaved comment with the issue itself
+ _qaItem = _qaItem.update({ newComment: val });
+
+ const qaService = services.improveOSM;
+ if (qaService) {
+ qaService.replaceItem(_qaItem);
+ }
+
+ saveSection
+ .call(qaSaveButtons);
}
+ }
- 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); }
- );
+ function qaSaveButtons(selection) {
+ const isSelected = (_qaItem && _qaItem.id === context.selectedErrorID());
+ let buttonSection = selection.selectAll('.buttons')
+ .data((isSelected ? [_qaItem] : []), d => d.status + d.id);
- // exit
- saveSection.exit()
- .remove();
+ // exit
+ buttonSection.exit()
+ .remove();
- // enter
- var saveSectionEnter = saveSection.enter()
- .append('div')
- .attr('class', 'error-save save-section cf');
+ // enter
+ const buttonEnter = buttonSection.enter()
+ .append('div')
+ .attr('class', 'buttons');
- saveSectionEnter
- .append('h4')
- .attr('class', '.error-save-header')
- .text(t('note.newComment'));
+ buttonEnter
+ .append('button')
+ .attr('class', 'button comment-button action')
+ .text(t('QA.keepRight.save_comment'));
- saveSectionEnter
- .append('textarea')
- .attr('class', 'new-comment-input')
- .attr('placeholder', t('QA.keepRight.comment_placeholder'))
- .attr('maxlength', 1000)
- .property('value', function(d) { return d.newComment; })
- .call(utilNoAuto)
- .on('input', changeInput)
- .on('blur', changeInput);
+ buttonEnter
+ .append('button')
+ .attr('class', 'button close-button action');
- // update
- saveSection = saveSectionEnter
- .merge(saveSection)
- .call(errorSaveButtons);
+ buttonEnter
+ .append('button')
+ .attr('class', 'button ignore-button action');
- function changeInput() {
- var input = d3_select(this);
- var val = input.property('value').trim();
+ // update
+ buttonSection = buttonSection
+ .merge(buttonEnter);
- if (val === '') {
- val = undefined;
- }
-
- // store the unsaved comment with the error itself
- _error = _error.update({ newComment: val });
-
- var errorService = services.improveOSM;
- if (errorService) {
- errorService.replaceError(_error);
- }
-
- saveSection
- .call(errorSaveButtons);
+ buttonSection.select('.comment-button')
+ .attr('disabled', d => d.newComment ? null : true)
+ .on('click.comment', function(d) {
+ this.blur(); // avoid keeping focus on the button - #4641
+ const qaService = services.improveOSM;
+ if (qaService) {
+ qaService.postUpdate(d, (err, item) => dispatch.call('change', item));
}
- }
+ });
- 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; });
+ buttonSection.select('.close-button')
+ .text(d => {
+ const andComment = (d.newComment ? '_comment' : '');
+ return t(`QA.keepRight.close${andComment}`);
+ })
+ .on('click.close', function(d) {
+ this.blur(); // avoid keeping focus on the button - #4641
+ const qaService = services.improveOSM;
+ if (qaService) {
+ d.newStatus = 'SOLVED';
+ qaService.postUpdate(d, (err, item) => dispatch.call('change', item));
+ }
+ });
- // exit
- buttonSection.exit()
- .remove();
+ buttonSection.select('.ignore-button')
+ .text(d => {
+ const andComment = (d.newComment ? '_comment' : '');
+ return t(`QA.keepRight.ignore${andComment}`);
+ })
+ .on('click.ignore', function(d) {
+ this.blur(); // avoid keeping focus on the button - #4641
+ const qaService = services.improveOSM;
+ if (qaService) {
+ d.newStatus = 'INVALID';
+ qaService.postUpdate(d, (err, item) => dispatch.call('change', item));
+ }
+ });
+ }
- // enter
- var buttonEnter = buttonSection.enter()
- .append('div')
- .attr('class', 'buttons');
+ // NOTE: Don't change method name until UI v3 is merged
+ improveOsmEditor.error = function(val) {
+ if (!arguments.length) return _qaItem;
+ _qaItem = val;
+ return improveOsmEditor;
+ };
- 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);
-
- 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');
+ return utilRebind(improveOsmEditor, dispatch, 'on');
}
diff --git a/modules/ui/improveOSM_header.js b/modules/ui/improveOSM_header.js
index bc0fb28b3..5fffbe2af 100644
--- a/modules/ui/improveOSM_header.js
+++ b/modules/ui/improveOSM_header.js
@@ -3,94 +3,80 @@ import { t } from '../util/locale';
export function uiImproveOsmHeader() {
- var _error;
+ let _qaItem;
+ function issueTitle(d) {
+ const unknown = t('inspector.unknown');
- function errorTitle(d) {
- var unknown = t('inspector.unknown');
+ if (!d) return unknown;
+ const { issueKey } = d;
+ const et = dataEn.QA.improveOSM.error_types[issueKey];
- if (!d) return unknown;
- var errorType = d.error_key;
- var et = dataEn.QA.improveOSM.error_types[errorType];
-
- if (et && et.title) {
- return t('QA.improveOSM.error_types.' + errorType + '.title');
- } else {
- return unknown;
- }
+ if (et && et.title) {
+ return t(`QA.improveOSM.error_types.${issueKey}.title`);
+ } else {
+ return unknown;
}
+ }
+ function improveOsmHeader(selection) {
+ const header = selection.selectAll('.qa-header')
+ .data(
+ (_qaItem ? [_qaItem] : []),
+ d => `${d.id}-${d.status || 0}`
+ );
- function improveOsmHeader(selection) {
- var header = selection.selectAll('.error-header')
- .data(
- (_error ? [_error] : []),
- function(d) { return d.id + '-' + (d.status || 0); }
- );
+ header.exit()
+ .remove();
- header.exit()
- .remove();
+ const headerEnter = header.enter()
+ .append('div')
+ .attr('class', 'qa-header');
- var headerEnter = header.enter()
- .append('div')
- .attr('class', 'error-header');
+ const svgEnter = headerEnter
+ .append('div')
+ .attr('class', 'qa-header-icon')
+ .classed('new', d => d.id < 0)
+ .append('svg')
+ .attr('width', '20px')
+ .attr('height', '30px')
+ .attr('viewbox', '0 0 20 30')
+ .attr('class', d => `preset-icon-28 qaItem ${d.service} itemId-${d.id} itemType-${d.itemType}`);
- var iconEnter = headerEnter
- .append('div')
- .attr('class', 'error-header-icon')
- .classed('new', function(d) { return d.id < 0; });
+ svgEnter
+ .append('polygon')
+ .attr('fill', 'currentColor')
+ .attr('class', 'qaItem-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');
- 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.service,
- 'error_id-' + d.id,
- 'error_type-' + d.error_type
- ].join(' ');
- });
+ svgEnter
+ .append('use')
+ .attr('class', 'icon-annotation')
+ .attr('width', '11px')
+ .attr('height', '11px')
+ .attr('transform', 'translate(4.5, 7)')
+ .attr('xlink:href', d => {
+ const picon = d.icon;
- 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('class', 'icon-annotation')
- .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', 'error-header-label')
- .text(errorTitle);
- }
-
-
- improveOsmHeader.error = function(val) {
- if (!arguments.length) return _error;
- _error = val;
- return improveOsmHeader;
- };
+ if (!picon) {
+ return '';
+ } else {
+ const isMaki = /^maki-/.test(picon);
+ return `#${picon}${isMaki ? '-11' : ''}`;
+ }
+ });
+ headerEnter
+ .append('div')
+ .attr('class', 'qa-header-label')
+ .text(issueTitle);
+ }
+ improveOsmHeader.issue = function(val) {
+ if (!arguments.length) return _qaItem;
+ _qaItem = val;
return improveOsmHeader;
-}
\ No newline at end of file
+ };
+
+ return improveOsmHeader;
+}
diff --git a/modules/ui/keepRight_details.js b/modules/ui/keepRight_details.js
index 966e75f0e..624e7cdfe 100644
--- a/modules/ui/keepRight_details.js
+++ b/modules/ui/keepRight_details.js
@@ -1,6 +1,6 @@
import {
- event as d3_event,
- select as d3_select
+ event as d3_event,
+ select as d3_select
} from 'd3-selection';
import { dataEn } from '../../data';
@@ -8,132 +8,124 @@ import { modeSelect } from '../modes/select';
import { t } from '../util/locale';
import { utilDisplayName, utilEntityOrMemberSelector, utilEntityRoot } from '../util';
-
export function uiKeepRightDetails(context) {
- var _error;
+ let _qaItem;
+ function issueDetail(d) {
+ const unknown = t('inspector.unknown');
+ if (!d) return unknown;
- function errorDetail(d) {
- var unknown = t('inspector.unknown');
+ const { itemType, parentIssueType, replacements } = d;
+ const et = dataEn.QA.keepRight.errorTypes[itemType];
+ const pt = dataEn.QA.keepRight.errorTypes[parentIssueType];
- if (!d) return unknown;
- var errorType = d.error_type;
- var parentErrorType = d.parent_error_type;
+ let detail;
+ if (et && et.description) {
+ detail = t(`QA.keepRight.errorTypes.${itemType}.description`, replacements);
+ } else if (pt && pt.description) {
+ detail = t(`QA.keepRight.errorTypes.${parentIssueType}.description`, replacements);
+ } else {
+ detail = unknown;
+ }
- var et = dataEn.QA.keepRight.errorTypes[errorType];
- var pt = dataEn.QA.keepRight.errorTypes[parentErrorType];
+ return detail;
+ }
- var detail;
- if (et && et.description) {
- detail = t('QA.keepRight.errorTypes.' + errorType + '.description', d.replacements);
- } else if (pt && pt.description) {
- detail = t('QA.keepRight.errorTypes.' + parentErrorType + '.description', d.replacements);
- } else {
- detail = unknown;
+ function keepRightDetails(selection) {
+ const details = selection.selectAll('.error-details')
+ .data(
+ (_qaItem ? [_qaItem] : []),
+ d => `${d.id}-${d.status || 0}`
+ );
+
+ details.exit()
+ .remove();
+
+ const detailsEnter = details.enter()
+ .append('div')
+ .attr('class', 'error-details qa-details-container');
+
+ // description
+ const descriptionEnter = detailsEnter
+ .append('div')
+ .attr('class', 'qa-details-description');
+
+ descriptionEnter
+ .append('h4')
+ .text(() => t('QA.keepRight.detail_description'));
+
+ descriptionEnter
+ .append('div')
+ .attr('class', 'qa-details-description-text')
+ .html(issueDetail);
+
+ // If there are entity links in the error message..
+ let relatedEntities = [];
+ descriptionEnter.selectAll('.error_entity_link, .error_object_link')
+ .each(function() {
+ const link = d3_select(this);
+ const isObjectLink = link.classed('error_object_link');
+ const entityID = isObjectLink ?
+ (utilEntityRoot(_qaItem.objectType) + _qaItem.objectId)
+ : this.textContent;
+ const entity = context.hasEntity(entityID);
+
+ relatedEntities.push(entityID);
+
+ // Add click handler
+ link
+ .on('mouseenter', () => {
+ context.surface().selectAll(utilEntityOrMemberSelector([entityID], context.graph()))
+ .classed('hover', true);
+ })
+ .on('mouseleave', () => {
+ context.surface().selectAll('.hover')
+ .classed('hover', false);
+ })
+ .on('click', () => {
+ d3_event.preventDefault();
+ const osmlayer = context.layers().layer('osm');
+ if (!osmlayer.enabled()) {
+ osmlayer.enabled(true);
+ }
+
+ context.map().centerZoomEase(_qaItem.loc, 20);
+
+ if (entity) {
+ context.enter(modeSelect(context, [entityID]));
+ } else {
+ context.loadEntity(entityID, () => {
+ context.enter(modeSelect(context, [entityID]));
+ });
+ }
+ });
+
+ // Replace with friendly name if possible
+ // (The entity may not yet be loaded into the graph)
+ if (entity) {
+ let name = utilDisplayName(entity); // try to use common name
+
+ if (!name && !isObjectLink) {
+ const preset = context.presets().match(entity, context.graph());
+ name = preset && !preset.isFallback() && preset.name(); // fallback to preset name
+ }
+
+ if (name) {
+ this.innerText = name;
+ }
}
+ });
- return detail;
- }
-
-
- function keepRightDetails(selection) {
- var details = selection.selectAll('.error-details')
- .data(
- (_error ? [_error] : []),
- function(d) { return d.id + '-' + (d.status || 0); }
- );
-
- details.exit()
- .remove();
-
- var detailsEnter = details.enter()
- .append('div')
- .attr('class', 'error-details error-details-container');
-
-
- // description
- var descriptionEnter = detailsEnter
- .append('div')
- .attr('class', 'error-details-description');
-
- descriptionEnter
- .append('h4')
- .text(function() { return t('QA.keepRight.detail_description'); });
-
- descriptionEnter
- .append('div')
- .attr('class', 'error-details-description-text')
- .html(errorDetail);
-
- // If there are entity links in the error message..
- var relatedEntities = [];
- descriptionEnter.selectAll('.error_entity_link, .error_object_link')
- .each(function() {
- var link = d3_select(this);
- var isObjectLink = link.classed('error_object_link');
- var entityID = isObjectLink ?
- (utilEntityRoot(_error.object_type) + _error.object_id)
- : this.textContent;
- var entity = context.hasEntity(entityID);
-
- relatedEntities.push(entityID);
-
- // Add click handler
- link
- .on('mouseenter', function() {
- context.surface().selectAll(utilEntityOrMemberSelector([entityID], context.graph()))
- .classed('hover', true);
- })
- .on('mouseleave', 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().centerZoomEase(_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;
- }
- }
- });
-
- // Don't hide entities related to this error - #5880
- context.features().forceVisible(relatedEntities);
- context.map().pan([0,0]); // trigger a redraw
- }
-
-
- keepRightDetails.error = function(val) {
- if (!arguments.length) return _error;
- _error = val;
- return keepRightDetails;
- };
-
+ // Don't hide entities related to this issue - #5880
+ context.features().forceVisible(relatedEntities);
+ context.map().pan([0,0]); // trigger a redraw
+ }
+ keepRightDetails.issue = function(val) {
+ if (!arguments.length) return _qaItem;
+ _qaItem = val;
return keepRightDetails;
-}
\ No newline at end of file
+ };
+
+ return keepRightDetails;
+}
diff --git a/modules/ui/keepRight_editor.js b/modules/ui/keepRight_editor.js
index f866094aa..18fb2306f 100644
--- a/modules/ui/keepRight_editor.js
+++ b/modules/ui/keepRight_editor.js
@@ -14,229 +14,208 @@ import { uiViewOnKeepRight } from './view_on_keepRight';
import { utilNoAuto, utilRebind } from '../util';
-
export function uiKeepRightEditor(context) {
- var dispatch = d3_dispatch('change');
- var keepRightDetails = uiKeepRightDetails(context);
- var keepRightHeader = uiKeepRightHeader(context);
- var quickLinks = uiQuickLinks();
+ const dispatch = d3_dispatch('change');
+ const qaDetails = uiKeepRightDetails(context);
+ const qaHeader = uiKeepRightHeader(context);
+ const quickLinks = uiQuickLinks();
- var _error;
+ let _qaItem;
+
+ function keepRightEditor(selection) {
+ // quick links
+ const choices = [{
+ id: 'zoom_to',
+ label: 'inspector.zoom_to.title',
+ tooltip: () => uiTooltipHtml(t('inspector.zoom_to.tooltip_qaItem'), t('inspector.zoom_to.key')),
+ click: () => context.mode().zoomToSelected()
+ }];
+
+ const headerEnter = selection.selectAll('.header')
+ .data([0])
+ .enter()
+ .append('div')
+ .attr('class', 'header fillL');
+
+ headerEnter
+ .append('button')
+ .attr('class', 'fr qa-editor-close')
+ .on('click', () => context.enter(modeBrowse(context)))
+ .call(svgIcon('#iD-icon-close'));
+
+ headerEnter
+ .append('h3')
+ .text(t('QA.keepRight.title'));
- function keepRightEditor(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();
- }
- }];
+ let body = selection.selectAll('.body')
+ .data([0]);
+
+ body = body.enter()
+ .append('div')
+ .attr('class', 'body')
+ .merge(body);
+
+ const editor = body.selectAll('.qa-editor')
+ .data([0]);
+
+ editor.enter()
+ .append('div')
+ .attr('class', 'modal-section qa-editor')
+ .merge(editor)
+ .call(qaHeader.issue(_qaItem))
+ .call(quickLinks.choices(choices))
+ .call(qaDetails.issue(_qaItem))
+ .call(keepRightSaveSection);
- var header = selection.selectAll('.header')
- .data([0]);
+ const footer = selection.selectAll('.footer')
+ .data([0]);
- var headerEnter = header.enter()
- .append('div')
- .attr('class', 'header fillL');
-
- headerEnter
- .append('button')
- .attr('class', 'fr error-editor-close')
- .on('click', function() {
- context.enter(modeBrowse(context));
- })
- .call(svgIcon('#iD-icon-close'));
-
- headerEnter
- .append('h3')
- .text(t('QA.keepRight.title'));
+ footer.enter()
+ .append('div')
+ .attr('class', 'footer')
+ .merge(footer)
+ .call(uiViewOnKeepRight(context).what(_qaItem));
+ }
- var body = selection.selectAll('.body')
- .data([0]);
+ function keepRightSaveSection(selection) {
+ const isSelected = (_qaItem && _qaItem.id === context.selectedErrorID());
+ const isShown = (_qaItem && (isSelected || _qaItem.newComment || _qaItem.comment));
+ let saveSection = selection.selectAll('.qa-save')
+ .data(
+ (isShown ? [_qaItem] : []),
+ d => `${d.id}-${d.status || 0}`
+ );
- body = body.enter()
- .append('div')
- .attr('class', 'body')
- .merge(body);
+ // exit
+ saveSection.exit()
+ .remove();
- var editor = body.selectAll('.error-editor')
- .data([0]);
+ // enter
+ const saveSectionEnter = saveSection.enter()
+ .append('div')
+ .attr('class', 'qa-save save-section cf');
- editor.enter()
- .append('div')
- .attr('class', 'modal-section error-editor')
- .merge(editor)
- .call(keepRightHeader.error(_error))
- .call(quickLinks.choices(choices))
- .call(keepRightDetails.error(_error))
- .call(keepRightSaveSection);
+ saveSectionEnter
+ .append('h4')
+ .attr('class', '.qa-save-header')
+ .text(t('QA.keepRight.comment'));
+ saveSectionEnter
+ .append('textarea')
+ .attr('class', 'new-comment-input')
+ .attr('placeholder', t('QA.keepRight.comment_placeholder'))
+ .attr('maxlength', 1000)
+ .property('value', d => d.newComment || d.comment)
+ .call(utilNoAuto)
+ .on('input', changeInput)
+ .on('blur', changeInput);
- var footer = selection.selectAll('.footer')
- .data([0]);
+ // update
+ saveSection = saveSectionEnter
+ .merge(saveSection)
+ .call(qaSaveButtons);
- footer.enter()
- .append('div')
- .attr('class', 'footer')
- .merge(footer)
- .call(uiViewOnKeepRight(context).what(_error));
+ function changeInput() {
+ const input = d3_select(this);
+ let val = input.property('value').trim();
+
+ if (val === _qaItem.comment) {
+ val = undefined;
+ }
+
+ // store the unsaved comment with the issue itself
+ _qaItem = _qaItem.update({ newComment: val });
+
+ const qaService = services.keepRight;
+ if (qaService) {
+ qaService.replaceItem(_qaItem); // update keepright cache
+ }
+
+ saveSection
+ .call(qaSaveButtons);
}
+ }
- function keepRightSaveSection(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); }
- );
+ function qaSaveButtons(selection) {
+ const isSelected = (_qaItem && _qaItem.id === context.selectedErrorID());
+ let buttonSection = selection.selectAll('.buttons')
+ .data((isSelected ? [_qaItem] : []), d => d.status + d.id);
- // exit
- saveSection.exit()
- .remove();
+ // exit
+ buttonSection.exit()
+ .remove();
- // enter
- var saveSectionEnter = saveSection.enter()
- .append('div')
- .attr('class', 'error-save save-section cf');
+ // enter
+ const buttonEnter = buttonSection.enter()
+ .append('div')
+ .attr('class', 'buttons');
- saveSectionEnter
- .append('h4')
- .attr('class', '.error-save-header')
- .text(t('QA.keepRight.comment'));
+ buttonEnter
+ .append('button')
+ .attr('class', 'button comment-button action')
+ .text(t('QA.keepRight.save_comment'));
- saveSectionEnter
- .append('textarea')
- .attr('class', 'new-comment-input')
- .attr('placeholder', t('QA.keepRight.comment_placeholder'))
- .attr('maxlength', 1000)
- .property('value', function(d) { return d.newComment || d.comment; })
- .call(utilNoAuto)
- .on('input', changeInput)
- .on('blur', changeInput);
+ buttonEnter
+ .append('button')
+ .attr('class', 'button close-button action');
- // update
- saveSection = saveSectionEnter
- .merge(saveSection)
- .call(keepRightSaveButtons);
+ buttonEnter
+ .append('button')
+ .attr('class', 'button ignore-button action');
+ // update
+ buttonSection = buttonSection
+ .merge(buttonEnter);
- function changeInput() {
- var input = d3_select(this);
- var val = input.property('value').trim();
-
- if (val === _error.comment) {
- val = undefined;
- }
-
- // store the unsaved comment with the error itself
- _error = _error.update({ newComment: val });
-
- var keepRight = services.keepRight;
- if (keepRight) {
- keepRight.replaceError(_error); // update keepright cache
- }
-
- saveSection
- .call(keepRightSaveButtons);
+ buttonSection.select('.comment-button') // select and propagate data
+ .attr('disabled', d => d.newComment ? null : true)
+ .on('click.comment', function(d) {
+ this.blur(); // avoid keeping focus on the button - #4641
+ const qaService = services.keepRight;
+ if (qaService) {
+ qaService.postUpdate(d, (err, item) => dispatch.call('change', item));
}
- }
+ });
+ buttonSection.select('.close-button') // select and propagate data
+ .text(d => {
+ const andComment = (d.newComment ? '_comment' : '');
+ return t(`QA.keepRight.close${andComment}`);
+ })
+ .on('click.close', function(d) {
+ this.blur(); // avoid keeping focus on the button - #4641
+ const qaService = services.keepRight;
+ if (qaService) {
+ d.newStatus = 'ignore_t'; // ignore temporarily (item fixed)
+ qaService.postUpdate(d, (err, item) => dispatch.call('change', item));
+ }
+ });
- function keepRightSaveButtons(selection) {
- var isSelected = (_error && _error.id === context.selectedErrorID());
- var buttonSection = selection.selectAll('.buttons')
- .data((isSelected ? [_error] : []), function(d) { return d.status + d.id; });
+ buttonSection.select('.ignore-button') // select and propagate data
+ .text(d => {
+ const andComment = (d.newComment ? '_comment' : '');
+ return t(`QA.keepRight.ignore${andComment}`);
+ })
+ .on('click.ignore', function(d) {
+ this.blur(); // avoid keeping focus on the button - #4641
+ const qaService = services.keepRight;
+ if (qaService) {
+ d.newStatus = 'ignore'; // ignore permanently (false positive)
+ qaService.postUpdate(d, (err, item) => dispatch.call('change', item));
+ }
+ });
+ }
- // exit
- buttonSection.exit()
- .remove();
+ // NOTE: Don't change method name until UI v3 is merged
+ keepRightEditor.error = function(val) {
+ if (!arguments.length) return _qaItem;
+ _qaItem = val;
+ return keepRightEditor;
+ };
- // enter
- var buttonEnter = buttonSection.enter()
- .append('div')
- .attr('class', 'buttons');
-
- 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);
-
- buttonSection.select('.comment-button') // select and propagate data
- .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 keepRight = services.keepRight;
- if (keepRight) {
- keepRight.postKeepRightUpdate(d, function(err, error) {
- dispatch.call('change', error);
- });
- }
- });
-
- buttonSection.select('.close-button') // select and propagate data
- .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 keepRight = services.keepRight;
- if (keepRight) {
- d.state = 'ignore_t'; // ignore temporarily (error fixed)
- keepRight.postKeepRightUpdate(d, function(err, error) {
- dispatch.call('change', error);
- });
- }
- });
-
- buttonSection.select('.ignore-button') // select and propagate data
- .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 keepRight = services.keepRight;
- if (keepRight) {
- d.state = 'ignore'; // ignore permanently (false positive)
- keepRight.postKeepRightUpdate(d, function(err, error) {
- dispatch.call('change', error);
- });
- }
- });
- }
-
-
- keepRightEditor.error = function(val) {
- if (!arguments.length) return _error;
- _error = val;
- return keepRightEditor;
- };
-
-
- return utilRebind(keepRightEditor, dispatch, 'on');
+ return utilRebind(keepRightEditor, dispatch, 'on');
}
diff --git a/modules/ui/keepRight_header.js b/modules/ui/keepRight_header.js
index b2d4130a1..9fdce6399 100644
--- a/modules/ui/keepRight_header.js
+++ b/modules/ui/keepRight_header.js
@@ -4,68 +4,61 @@ import { t } from '../util/locale';
export function uiKeepRightHeader() {
- var _error;
+ let _qaItem;
+ function issueTitle(d) {
+ const unknown = t('inspector.unknown');
- function errorTitle(d) {
- var unknown = t('inspector.unknown');
+ if (!d) return unknown;
+ const { itemType, parentIssueType } = d;
- if (!d) return unknown;
- var errorType = d.error_type;
- var parentErrorType = d.parent_error_type;
+ const et = dataEn.QA.keepRight.errorTypes[itemType];
+ const pt = dataEn.QA.keepRight.errorTypes[parentIssueType];
- var et = dataEn.QA.keepRight.errorTypes[errorType];
- var pt = dataEn.QA.keepRight.errorTypes[parentErrorType];
-
- if (et && et.title) {
- return t('QA.keepRight.errorTypes.' + errorType + '.title');
- } else if (pt && pt.title) {
- return t('QA.keepRight.errorTypes.' + parentErrorType + '.title');
- } else {
- return unknown;
- }
+ if (et && et.title) {
+ return t(`QA.keepRight.errorTypes.${itemType}.title`);
+ } else if (pt && pt.title) {
+ return t(`QA.keepRight.errorTypes.${parentIssueType}.title`);
+ } else {
+ return unknown;
}
+ }
+ function keepRightHeader(selection) {
+ const header = selection.selectAll('.qa-header')
+ .data(
+ (_qaItem ? [_qaItem] : []),
+ d => `${d.id}-${d.status || 0}`
+ );
- function keepRightHeader(selection) {
- var header = selection.selectAll('.error-header')
- .data(
- (_error ? [_error] : []),
- function(d) { return d.id + '-' + (d.status || 0); }
- );
+ header.exit()
+ .remove();
- header.exit()
- .remove();
+ const headerEnter = header.enter()
+ .append('div')
+ .attr('class', 'qa-header');
- var headerEnter = header.enter()
- .append('div')
- .attr('class', 'error-header');
+ const iconEnter = headerEnter
+ .append('div')
+ .attr('class', 'qa-header-icon')
+ .classed('new', d => d.id < 0);
- var iconEnter = headerEnter
- .append('div')
- .attr('class', 'error-header-icon')
- .classed('new', function(d) { return d.id < 0; });
-
- iconEnter
- .append('div')
- .attr('class', function(d) {
- return 'preset-icon-28 qa_error ' + d.service + ' error_id-' + d.id + ' error_type-' + d.parent_error_type;
- })
- .call(svgIcon('#iD-icon-bolt', 'qa_error-fill'));
-
- headerEnter
- .append('div')
- .attr('class', 'error-header-label')
- .text(errorTitle);
- }
-
-
- keepRightHeader.error = function(val) {
- if (!arguments.length) return _error;
- _error = val;
- return keepRightHeader;
- };
+ iconEnter
+ .append('div')
+ .attr('class', d => `preset-icon-28 qaItem ${d.service} itemId-${d.id} itemType-${d.parentIssueType}`)
+ .call(svgIcon('#iD-icon-bolt', 'qaItem-fill'));
+ headerEnter
+ .append('div')
+ .attr('class', 'qa-header-label')
+ .text(issueTitle);
+ }
+ keepRightHeader.issue = val => {
+ if (!arguments.length) return _qaItem;
+ _qaItem = val;
return keepRightHeader;
+ };
+
+ return keepRightHeader;
}
diff --git a/modules/ui/osmose_details.js b/modules/ui/osmose_details.js
index 174009900..22f43bce5 100644
--- a/modules/ui/osmose_details.js
+++ b/modules/ui/osmose_details.js
@@ -10,13 +10,13 @@ import { utilDisplayName, utilEntityOrMemberSelector } from '../util';
export function uiOsmoseDetails(context) {
- let _error;
+ let _qaItem;
function issueString(d, type) {
if (!d) return '';
// Issue strings are cached from Osmose API
- const s = services.osmose.getStrings(d.error_type);
+ const s = services.osmose.getStrings(d.itemType);
return (type in s) ? s[type] : '';
}
@@ -24,7 +24,7 @@ export function uiOsmoseDetails(context) {
function osmoseDetails(selection) {
const details = selection.selectAll('.error-details')
.data(
- _error ? [_error] : [],
+ _qaItem ? [_qaItem] : [],
d => `${d.id}-${d.status || 0}`
);
@@ -33,14 +33,14 @@ export function uiOsmoseDetails(context) {
const detailsEnter = details.enter()
.append('div')
- .attr('class', 'error-details error-details-container');
+ .attr('class', 'error-details qa-details-container');
// Description
- if (issueString(_error, 'detail')) {
+ if (issueString(_qaItem, 'detail')) {
const div = detailsEnter
.append('div')
- .attr('class', 'error-details-subsection');
+ .attr('class', 'qa-details-subsection');
div
.append('h4')
@@ -48,24 +48,24 @@ export function uiOsmoseDetails(context) {
div
.append('p')
- .attr('class', 'error-details-description-text')
+ .attr('class', 'qa-details-description-text')
.html(d => issueString(d, 'detail'));
}
// Elements (populated later as data is requested)
const detailsDiv = detailsEnter
.append('div')
- .attr('class', 'error-details-subsection');
+ .attr('class', 'qa-details-subsection');
const elemsDiv = detailsEnter
.append('div')
- .attr('class', 'error-details-subsection');
+ .attr('class', 'qa-details-subsection');
// Suggested Fix (musn't exist for every issue type)
- if (issueString(_error, 'fix')) {
+ if (issueString(_qaItem, 'fix')) {
const div = detailsEnter
.append('div')
- .attr('class', 'error-details-subsection');
+ .attr('class', 'qa-details-subsection');
div
.append('h4')
@@ -77,10 +77,10 @@ export function uiOsmoseDetails(context) {
}
// Common Pitfalls (musn't exist for every issue type)
- if (issueString(_error, 'trap')) {
+ if (issueString(_qaItem, 'trap')) {
const div = detailsEnter
.append('div')
- .attr('class', 'error-details-subsection');
+ .attr('class', 'qa-details-subsection');
div
.append('h4')
@@ -105,7 +105,7 @@ export function uiOsmoseDetails(context) {
.append('use')
.attr('href', '#iD-icon-out-link');
- services.osmose.loadErrorDetail(_error)
+ services.osmose.loadIssueDetail(_qaItem)
.then(d => {
// No details to add if there are no associated issue elements
if (!d.elems || d.elems.length === 0) return;
@@ -116,7 +116,6 @@ export function uiOsmoseDetails(context) {
if (d.detail) {
detailsDiv
.append('h4')
- .attr('class', 'error-details-subtitle')
.text(() => t('QA.osmose.detail_title'));
detailsDiv
@@ -127,13 +126,10 @@ export function uiOsmoseDetails(context) {
// Create list of linked issue elements
elemsDiv
.append('h4')
- .attr('class', 'error-details-subtitle')
.text(() => t('QA.osmose.elems_title'));
elemsDiv
- .append('ul')
- .attr('class', 'error-details-elements')
- .selectAll('.error_entity_link')
+ .append('ul').selectAll('li')
.data(d.elems)
.enter()
.append('li')
@@ -189,7 +185,7 @@ export function uiOsmoseDetails(context) {
}
});
- // Don't hide entities related to this error - #5880
+ // Don't hide entities related to this issue - #5880
context.features().forceVisible(d.elems);
context.map().pan([0,0]); // trigger a redraw
})
@@ -197,9 +193,9 @@ export function uiOsmoseDetails(context) {
}
- osmoseDetails.error = val => {
- if (!arguments.length) return _error;
- _error = val;
+ osmoseDetails.issue = function(val) {
+ if (!arguments.length) return _qaItem;
+ _qaItem = val;
return osmoseDetails;
};
diff --git a/modules/ui/osmose_editor.js b/modules/ui/osmose_editor.js
index 4571eeb56..93df17386 100644
--- a/modules/ui/osmose_editor.js
+++ b/modules/ui/osmose_editor.js
@@ -12,159 +12,140 @@ import { uiTooltipHtml } from './tooltipHtml';
import { utilRebind } from '../util';
-
export function uiOsmoseEditor(context) {
- var dispatch = d3_dispatch('change');
- var errorDetails = uiOsmoseDetails(context);
- var errorHeader = uiOsmoseHeader(context);
- var quickLinks = uiQuickLinks();
+ const dispatch = d3_dispatch('change');
+ const qaDetails = uiOsmoseDetails(context);
+ const qaHeader = uiOsmoseHeader(context);
+ const quickLinks = uiQuickLinks();
- var _error;
+ let _qaItem;
+ function osmoseEditor(selection) {
+ // quick links
+ const choices = [{
+ id: 'zoom_to',
+ label: 'inspector.zoom_to.title',
+ tooltip: () => uiTooltipHtml(t('inspector.zoom_to.tooltip_qaItem'), t('inspector.zoom_to.key')),
+ click: () => context.mode().zoomToSelected()
+ }];
- function osmoseEditor(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();
- }
- }];
+ const header = selection.selectAll('.header')
+ .data([0]);
+ const headerEnter = header.enter()
+ .append('div')
+ .attr('class', 'header fillL');
- var header = selection.selectAll('.header')
- .data([0]);
+ headerEnter
+ .append('button')
+ .attr('class', 'fr qa-editor-close')
+ .on('click', () => context.enter(modeBrowse(context)))
+ .call(svgIcon('#iD-icon-close'));
- var headerEnter = header.enter()
- .append('div')
- .attr('class', 'header fillL');
+ headerEnter
+ .append('h3')
+ .text(t('QA.osmose.title'));
- headerEnter
- .append('button')
- .attr('class', 'fr error-editor-close')
- .on('click', function() {
- context.enter(modeBrowse(context));
- })
- .call(svgIcon('#iD-icon-close'));
+ let body = selection.selectAll('.body')
+ .data([0]);
- headerEnter
- .append('h3')
- .text(t('QA.osmose.title'));
+ body = body.enter()
+ .append('div')
+ .attr('class', 'body')
+ .merge(body);
+ let editor = body.selectAll('.qa-editor')
+ .data([0]);
- var body = selection.selectAll('.body')
- .data([0]);
+ editor.enter()
+ .append('div')
+ .attr('class', 'modal-section qa-editor')
+ .merge(editor)
+ .call(qaHeader.issue(_qaItem))
+ .call(quickLinks.choices(choices))
+ .call(qaDetails.issue(_qaItem))
+ .call(osmoseSaveSection);
+ }
- body = body.enter()
- .append('div')
- .attr('class', 'body')
- .merge(body);
+ function osmoseSaveSection(selection) {
+ const isSelected = (_qaItem && _qaItem.id === context.selectedErrorID());
+ const isShown = (_qaItem && isSelected);
+ let saveSection = selection.selectAll('.qa-save')
+ .data(
+ (isShown ? [_qaItem] : []),
+ d => `${d.id}-${d.status || 0}`
+ );
- var editor = body.selectAll('.error-editor')
- .data([0]);
+ // exit
+ saveSection.exit()
+ .remove();
- editor.enter()
- .append('div')
- .attr('class', 'modal-section error-editor')
- .merge(editor)
- .call(errorHeader.error(_error))
- .call(quickLinks.choices(choices))
- .call(errorDetails.error(_error))
- .call(osmoseSaveSection);
- }
+ // enter
+ const saveSectionEnter = saveSection.enter()
+ .append('div')
+ .attr('class', 'qa-save save-section cf');
- function osmoseSaveSection(selection) {
- var isSelected = (_error && _error.id === context.selectedErrorID());
- var isShown = (_error && isSelected);
- var saveSection = selection.selectAll('.error-save')
- .data(
- (isShown ? [_error] : []),
- function(d) { return d.id + '-' + (d.status || 0); }
- );
+ // update
+ saveSection = saveSectionEnter
+ .merge(saveSection)
+ .call(qaSaveButtons);
+ }
- // exit
- saveSection.exit()
- .remove();
+ function qaSaveButtons(selection) {
+ const isSelected = (_qaItem && _qaItem.id === context.selectedErrorID());
+ let buttonSection = selection.selectAll('.buttons')
+ .data((isSelected ? [_qaItem] : []), d => d.status + d.id);
- // enter
- var saveSectionEnter = saveSection.enter()
- .append('div')
- .attr('class', 'error-save save-section cf');
+ // exit
+ buttonSection.exit()
+ .remove();
- // update
- saveSection = saveSectionEnter
- .merge(saveSection)
- .call(errorSaveButtons);
- }
+ // enter
+ const buttonEnter = buttonSection.enter()
+ .append('div')
+ .attr('class', 'buttons');
- 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; });
+ buttonEnter
+ .append('button')
+ .attr('class', 'button close-button action');
- // exit
- buttonSection.exit()
- .remove();
+ buttonEnter
+ .append('button')
+ .attr('class', 'button ignore-button action');
- // enter
- var buttonEnter = buttonSection.enter()
- .append('div')
- .attr('class', 'buttons');
+ // update
+ buttonSection = buttonSection
+ .merge(buttonEnter);
- buttonEnter
- .append('button')
- .attr('class', 'button close-button action');
+ buttonSection.select('.close-button')
+ .text(() => t('QA.keepRight.close'))
+ .on('click.close', function(d) {
+ this.blur(); // avoid keeping focus on the button - #4641
+ const qaService = services.osmose;
+ if (qaService) {
+ d.newStatus = 'done';
+ qaService.postUpdate(d, (err, item) => dispatch.call('change', item));
+ }
+ });
- buttonEnter
- .append('button')
- .attr('class', 'button ignore-button action');
+ buttonSection.select('.ignore-button')
+ .text(() => t('QA.keepRight.ignore'))
+ .on('click.ignore', function(d) {
+ this.blur(); // avoid keeping focus on the button - #4641
+ const qaService = services.osmose;
+ if (qaService) {
+ d.newStatus = 'false';
+ qaService.postUpdate(d, (err, item) => dispatch.call('change', item));
+ }
+ });
+ }
+ // NOTE: Don't change method name until UI v3 is merged
+ osmoseEditor.error = function(val) {
+ if (!arguments.length) return _qaItem;
+ _qaItem = val;
+ return osmoseEditor;
+ };
- // update
- buttonSection = buttonSection
- .merge(buttonEnter);
-
- buttonSection.select('.close-button')
- .text(function() {
- return t('QA.keepRight.close');
- })
- .on('click.close', function(d) {
- this.blur(); // avoid keeping focus on the button - #4641
- var errorService = services.osmose;
- if (errorService) {
- d.newStatus = 'done';
- errorService.postUpdate(d, function(err, error) {
- dispatch.call('change', error);
- });
- }
- });
-
- buttonSection.select('.ignore-button')
- .text(function() {
- return t('QA.keepRight.ignore');
- })
- .on('click.ignore', function(d) {
- this.blur(); // avoid keeping focus on the button - #4641
- var errorService = services.osmose;
- if (errorService) {
- d.newStatus = 'false';
- errorService.postUpdate(d, function(err, error) {
- dispatch.call('change', error);
- });
- }
- });
- }
-
- osmoseEditor.error = function(val) {
- if (!arguments.length) return _error;
- _error = val;
- return osmoseEditor;
- };
-
-
- return utilRebind(osmoseEditor, dispatch, 'on');
-}
\ No newline at end of file
+ return utilRebind(osmoseEditor, dispatch, 'on');
+}
diff --git a/modules/ui/osmose_header.js b/modules/ui/osmose_header.js
index 4016b4b71..83dac6213 100644
--- a/modules/ui/osmose_header.js
+++ b/modules/ui/osmose_header.js
@@ -3,91 +3,76 @@ import { t } from '../util/locale';
export function uiOsmoseHeader() {
- var _error;
+ let _qaItem;
+ function issueTitle(d) {
+ const unknown = t('inspector.unknown');
- function errorTitle(d) {
- var unknown = t('inspector.unknown');
+ if (!d) return unknown;
- if (!d) return unknown;
+ // Issue titles supplied by Osmose
+ const s = services.osmose.getStrings(d.itemType);
+ return ('title' in s) ? s.title : unknown;
+ }
- // Issue titles supplied by Osmose
- var s = services.osmose.getStrings(d.error_type);
- return ('title' in s) ? s.title : unknown;
- }
+ function osmoseHeader(selection) {
+ const header = selection.selectAll('.qa-header')
+ .data(
+ (_qaItem ? [_qaItem] : []),
+ d => `${d.id}-${d.status || 0}`
+ );
+ header.exit()
+ .remove();
- function osmoseHeader(selection) {
- var header = selection.selectAll('.error-header')
- .data(
- (_error ? [_error] : []),
- function(d) { return d.id + '-' + (d.status || 0); }
- );
+ const headerEnter = header.enter()
+ .append('div')
+ .attr('class', 'qa-header');
- header.exit()
- .remove();
+ const svgEnter = headerEnter
+ .append('div')
+ .attr('class', 'qa-header-icon')
+ .classed('new', d => d.id < 0)
+ .append('svg')
+ .attr('width', '20px')
+ .attr('height', '30px')
+ .attr('viewbox', '0 0 20 30')
+ .attr('class', d => `preset-icon-28 qaItem ${d.service} itemId-${d.id} itemType-${d.itemType}`);
- var headerEnter = header.enter()
- .append('div')
- .attr('class', 'error-header');
+ svgEnter
+ .append('polygon')
+ .attr('fill', d => services.osmose.getColor(d.item))
+ .attr('class', 'qaItem-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');
- var iconEnter = headerEnter
- .append('div')
- .attr('class', 'error-header-icon')
- .classed('new', function(d) { return d.id < 0; });
+ svgEnter
+ .append('use')
+ .attr('class', 'icon-annotation')
+ .attr('width', '11px')
+ .attr('height', '11px')
+ .attr('transform', 'translate(4.5, 7)')
+ .attr('xlink:href', d => {
+ const picon = d.icon;
- 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.service,
- 'error_id-' + d.id,
- 'error_type-' + d.error_type,
- 'item-' + d.item
- ].join(' ');
- });
-
- svgEnter
- .append('polygon')
- .attr('fill', d => services.osmose.getColor(d.item))
- .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('class', 'icon-annotation')
- .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', 'error-header-label')
- .text(errorTitle);
- }
-
-
- osmoseHeader.error = function(val) {
- if (!arguments.length) return _error;
- _error = val;
- return osmoseHeader;
- };
+ if (!picon) {
+ return '';
+ } else {
+ const isMaki = /^maki-/.test(picon);
+ return `#${picon}${isMaki ? '-11' : ''}`;
+ }
+ });
+ headerEnter
+ .append('div')
+ .attr('class', 'qa-header-label')
+ .text(issueTitle);
+ }
+ osmoseHeader.issue = function(val) {
+ if (!arguments.length) return _qaItem;
+ _qaItem = val;
return osmoseHeader;
+ };
+
+ return osmoseHeader;
}
diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js
index ebf9dd6a8..a730d5aea 100644
--- a/modules/ui/sidebar.js
+++ b/modules/ui/sidebar.js
@@ -9,7 +9,7 @@ import {
selectAll as d3_selectAll
} from 'd3-selection';
import { utilArrayIdentical } from '../util/array';
-import { osmEntity, osmNote, qaError } from '../osm';
+import { osmEntity, osmNote, QAItem } from '../osm';
import { services } from '../services';
import { uiDataEditor } from './data_editor';
import { uiFeatureList } from './feature_list';
@@ -31,7 +31,7 @@ export function uiSidebar(context) {
var _current;
var _wasData = false;
var _wasNote = false;
- var _wasQAError = false;
+ var _wasQaItem = false;
function sidebar(selection) {
@@ -140,8 +140,8 @@ export function uiSidebar(context) {
selection.selectAll('.sidebar-component')
.classed('inspector-hover', true);
- } else if (datum instanceof qaError) {
- _wasQAError = true;
+ } else if (datum instanceof QAItem) {
+ _wasQaItem = true;
var errService = services[datum.service];
if (errService) {
@@ -159,7 +159,7 @@ export function uiSidebar(context) {
errEditor = improveOsmEditor;
}
- d3_selectAll('.qa_error.' + datum.service)
+ d3_selectAll('.qaItem.' + datum.service)
.classed('hover', function(d) { return d.id === datum.id; });
sidebar
@@ -193,12 +193,12 @@ export function uiSidebar(context) {
inspector
.state('hide');
- } else if (_wasData || _wasNote || _wasQAError) {
+ } else if (_wasData || _wasNote || _wasQaItem) {
_wasNote = false;
_wasData = false;
- _wasQAError = false;
+ _wasQaItem = false;
d3_selectAll('.note').classed('hover', false);
- d3_selectAll('.qa_error').classed('hover', false);
+ d3_selectAll('.qaItem').classed('hover', false);
sidebar.hide();
}
}
@@ -366,4 +366,4 @@ export function uiSidebar(context) {
sidebar.toggle = function() {};
return sidebar;
-}
\ No newline at end of file
+}
diff --git a/modules/ui/view_on_keepRight.js b/modules/ui/view_on_keepRight.js
index 62944db5d..1a33b7d9a 100644
--- a/modules/ui/view_on_keepRight.js
+++ b/modules/ui/view_on_keepRight.js
@@ -1,45 +1,42 @@
import { t } from '../util/locale';
import { services } from '../services';
import { svgIcon } from '../svg/icon';
-import { qaError } from '../osm';
-
+import { QAItem } from '../osm';
export function uiViewOnKeepRight() {
- var _error; // a keepright error
+ let _qaItem;
-
- function viewOnKeepRight(selection) {
- var url;
- if (services.keepRight && (_error instanceof qaError)) {
- url = services.keepRight.errorURL(_error);
- }
-
- var link = selection.selectAll('.view-on-keepRight')
- .data(url ? [url] : []);
-
- // exit
- link.exit()
- .remove();
-
- // enter
- var linkEnter = link.enter()
- .append('a')
- .attr('class', 'view-on-keepRight')
- .attr('target', '_blank')
- .attr('href', function(d) { return d; })
- .call(svgIcon('#iD-icon-out-link', 'inline'));
-
- linkEnter
- .append('span')
- .text(t('inspector.view_on_keepRight'));
+ function viewOnKeepRight(selection) {
+ let url;
+ if (services.keepRight && (_qaItem instanceof QAItem)) {
+ url = services.keepRight.issueURL(_qaItem);
}
+ const link = selection.selectAll('.view-on-keepRight')
+ .data(url ? [url] : []);
- viewOnKeepRight.what = function(val) {
- if (!arguments.length) return _error;
- _error = val;
- return viewOnKeepRight;
- };
+ // exit
+ link.exit()
+ .remove();
+ // enter
+ const linkEnter = link.enter()
+ .append('a')
+ .attr('class', 'view-on-keepRight')
+ .attr('target', '_blank')
+ .attr('href', function(d) { return d; })
+ .call(svgIcon('#iD-icon-out-link', 'inline'));
+
+ linkEnter
+ .append('span')
+ .text(t('inspector.view_on_keepRight'));
+ }
+
+ viewOnKeepRight.what = function(val) {
+ if (!arguments.length) return _qaItem;
+ _qaItem = val;
return viewOnKeepRight;
-}
\ No newline at end of file
+ };
+
+ return viewOnKeepRight;
+}
diff --git a/scripts/build_data.js b/scripts/build_data.js
index d57ca72d5..4d31be886 100644
--- a/scripts/build_data.js
+++ b/scripts/build_data.js
@@ -90,7 +90,7 @@ function buildData() {
'dist/data/*',
'svg/fontawesome/*.svg',
]);
- readQAErrorIcons(faIcons, tnpIcons);
+ readQAIssueIcons(faIcons, tnpIcons);
let categories = generateCategories(tstrings, faIcons, tnpIcons);
let fields = generateFields(tstrings, faIcons, tnpIcons, searchableFieldIDs);
let presets = generatePresets(tstrings, faIcons, tnpIcons, searchableFieldIDs);
@@ -128,7 +128,7 @@ function buildData() {
minifyJSON('data/languages.json', 'dist/data/languages.min.json'),
minifyJSON('data/locales.json', 'dist/data/locales.min.json'),
minifyJSON('data/phone_formats.json', 'dist/data/phone_formats.min.json'),
- minifyJSON('data/qa_errors.json', 'dist/data/qa_errors.min.json'),
+ minifyJSON('data/qa_data.json', 'dist/data/qa_data.min.json'),
minifyJSON('data/shortcuts.json', 'dist/data/shortcuts.min.json'),
minifyJSON('data/taginfo.json', 'dist/data/taginfo.min.json'),
minifyJSON('data/territory_languages.json', 'dist/data/territory_languages.min.json')
@@ -173,13 +173,12 @@ function validate(file, instance, schema) {
}
-function readQAErrorIcons(faIcons, tnpIcons) {
- const qa = read('data/qa_errors.json');
+function readQAIssueIcons(faIcons, tnpIcons) {
+ const qa = read('data/qa_data.json');
- for (const service in qa.services) {
- for (const error in qa.services[service].errorIcons) {
- const icon = qa.services[service]
- .errorIcons[error];
+ for (const service in qa) {
+ for (const item in qa[service].icons) {
+ const icon = qa[service].icons[item];
// fontawesome icon, remember for later
if (/^fa[srb]-/.test(icon)) {