mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-17 14:23:38 +02:00
Update and standardise QA implementations
- ES6ify (now using class syntax to define QAItem objects) - Fix bug with KeepRight marker rendering not updating properly - Use `qa-` prefix for the UI element classes to differentiate from iD validation error related UI element classes - Move away from "error" where possible in source - Move away from snake_case naming where possible Note that some function/method names have been untouched to make life easier for v3 development. Have added note comments where appropriate.
This commit is contained in:
+7
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
+48
-50
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+19
-19
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
+427
-449
@@ -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 '<a class="error_object_link">' + d + '</a>';
|
||||
return `<a class="error_object_link">${d}</a>`;
|
||||
}
|
||||
|
||||
function linkEntity(d) {
|
||||
return '<a class="error_entity_link">' + d + '</a>';
|
||||
return `<a class="error_entity_link">${d}</a>`;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
+454
-467
@@ -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 `<a class="error_object_link">${d}</a>`;
|
||||
}
|
||||
|
||||
function linkEntity(d) {
|
||||
return `<a class="error_entity_link">${d}</a>`;
|
||||
}
|
||||
|
||||
function linkURL(d) {
|
||||
return `<a class="kr_external_link" target="_blank" href="${d}">${d}</a>`;
|
||||
}
|
||||
|
||||
// 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 '<a class="error_object_link">' + d + '</a>';
|
||||
}
|
||||
|
||||
function linkEntity(d) {
|
||||
return '<a class="error_entity_link">' + d + '</a>';
|
||||
}
|
||||
|
||||
function linkURL(d) {
|
||||
return '<a class="kr_external_link" target="_blank" href="' + d + '">' + d + '</a>';
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
+107
-116
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+211
-235
@@ -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;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
+199
-223
@@ -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;
|
||||
}
|
||||
|
||||
+217
-241
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+113
-118
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
return improveOsmDetails;
|
||||
}
|
||||
|
||||
+165
-185
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
return improveOsmHeader;
|
||||
}
|
||||
|
||||
+113
-121
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
return keepRightDetails;
|
||||
}
|
||||
|
||||
+174
-195
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
+112
-131
@@ -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');
|
||||
}
|
||||
return utilRebind(osmoseEditor, dispatch, 'on');
|
||||
}
|
||||
|
||||
+59
-74
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
return viewOnKeepRight;
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user