Merge branch 'master' into validation

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

After

Width:  |  Height:  |  Size: 468 B

+1 -2
View File
@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="20" height="20" viewBox="0 0 20 20">
<path stroke="inherit" stroke-width="inherit" fill="currentColor" d="M15,6.5h-4.2l1.3-4.7c0.1-0.5-0.4-1-0.9-1H6.2C5.8,0.7,5.4,1,5.4,1.5l-1.2,8.7c-0.1,0.6,0.4,1,0.8,1h4.3l-1.7,7.1
c-0.1,0.5,0.4,1,0.9,1c0.3,0,0.6-0.2,0.7-0.5l6.4-11C16,7.2,15.6,6.5,15,6.5z" />
<path stroke="inherit" stroke-width="inherit" fill="currentColor" d="M15,6.5h-4.2l1.3-4.7c0.1-0.5-0.4-1-0.9-1H6.2C5.8,0.7,5.4,1,5.4,1.5l-1.2,8.7c-0.1,0.6,0.4,1,0.8,1h4.3l-1.7,7.1c-0.1,0.5,0.4,1,0.9,1c0.3,0,0.6-0.2,0.7-0.5l6.4-11C16,7.2,15.6,6.5,15,6.5z" />
</svg>

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 562 B

+9 -8
View File
@@ -26,18 +26,19 @@ describe('iD.svgLayers', function () {
it('creates default data layers', function () {
container.call(iD.svgLayers(projection, context));
var nodes = container.selectAll('svg .data-layer').nodes();
expect(nodes.length).to.eql(11);
expect(nodes.length).to.eql(12);
expect(d3.select(nodes[0]).classed('osm')).to.be.true;
expect(d3.select(nodes[1]).classed('notes')).to.be.true;
expect(d3.select(nodes[2]).classed('data')).to.be.true;
expect(d3.select(nodes[3]).classed('keepRight')).to.be.true;
expect(d3.select(nodes[4]).classed('streetside')).to.be.true;
expect(d3.select(nodes[5]).classed('mapillary-images')).to.be.true;
expect(d3.select(nodes[6]).classed('mapillary-signs')).to.be.true;
expect(d3.select(nodes[7]).classed('openstreetcam-images')).to.be.true;
expect(d3.select(nodes[8]).classed('debug')).to.be.true;
expect(d3.select(nodes[9]).classed('geolocate')).to.be.true;
expect(d3.select(nodes[10]).classed('touch')).to.be.true;
expect(d3.select(nodes[4]).classed('improveOSM')).to.be.true;
expect(d3.select(nodes[5]).classed('streetside')).to.be.true;
expect(d3.select(nodes[6]).classed('mapillary-images')).to.be.true;
expect(d3.select(nodes[7]).classed('mapillary-signs')).to.be.true;
expect(d3.select(nodes[8]).classed('openstreetcam-images')).to.be.true;
expect(d3.select(nodes[9]).classed('debug')).to.be.true;
expect(d3.select(nodes[10]).classed('geolocate')).to.be.true;
expect(d3.select(nodes[11]).classed('touch')).to.be.true;
});
});
+19
View File
@@ -209,6 +209,25 @@ describe('iD.svgTagClasses', function () {
expect(selection.attr('class')).to.equal('selected');
});
it('stroke overrides: renders areas with barriers as lines', function() {
selection
.attr('class', 'way area stroke')
.datum(iD.osmEntity({tags: {landuse: 'residential', barrier: 'hedge'}}))
.call(iD.svgTagClasses());
expect(selection.classed('area')).to.be.false;
expect(selection.classed('line')).to.be.true;
});
it('stroke overrides: renders simple multipolygon lines as areas', function() {
var multipolygon = function () { return { type: 'multipolygon' }; };
selection
.attr('class', 'way line stroke')
.datum(iD.osmEntity({tags: {}}))
.call(iD.svgTagClasses().tags(multipolygon));
expect(selection.classed('area')).to.be.true;
expect(selection.classed('line')).to.be.false;
});
it('works on SVG elements', function() {
selection = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'g'));
selection