mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-14 21:28:11 +02:00
Merge branch 'master' into validation
This commit is contained in:
+3
-1
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
"prerequisiteTag": {
|
||||
"key": "oneway",
|
||||
"value": "yes"
|
||||
}
|
||||
},
|
||||
"snake_case": false
|
||||
}
|
||||
|
||||
Vendored
+38
-1
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
@@ -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,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 |
@@ -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;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user