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:
SilentSpike
2020-02-05 14:23:34 +00:00
parent 9faf8c0fe5
commit 51efd5b714
33 changed files with 2823 additions and 3079 deletions
+7 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+91
View File
@@ -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"
}
}
}
-93
View File
@@ -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"
}
}
}
}
+3 -3
View File
@@ -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;
+2 -2
View File
@@ -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));
+1
View File
@@ -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;
+4 -4
View File
@@ -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;
}
}
+1 -1
View File
@@ -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,
-63
View File
@@ -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)}
}
});
+46
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+5 -5
View File
@@ -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);
}
}
+69 -72
View File
@@ -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
View File
@@ -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
View File
@@ -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');
}
+63 -77
View File
@@ -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
View File
@@ -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
View File
@@ -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');
}
+44 -51
View File
@@ -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;
}
+19 -23
View File
@@ -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
View File
@@ -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
View File
@@ -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 -9
View File
@@ -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;
}
}
+31 -34
View File
@@ -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;
}
+7 -8
View File
@@ -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)) {