mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-23 16:49:40 +02:00
Merge pull request #5830 from openstreetmap/validation
Issue manager and advanced validation
This commit is contained in:
+187
-12
@@ -352,6 +352,8 @@ button.secondary-action:hover {
|
||||
background: #cccccc;
|
||||
}
|
||||
|
||||
button.action.disabled,
|
||||
button.action.disabled:hover,
|
||||
button[disabled].action,
|
||||
button[disabled].action:hover {
|
||||
background: #cccccc;
|
||||
@@ -413,6 +415,19 @@ button[disabled].action:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.icon-badge {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
right: 7px;
|
||||
top: 9px;
|
||||
}
|
||||
|
||||
.icon-badge.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* Toolbar / Persistent UI Elements
|
||||
------------------------------------------------------- */
|
||||
@@ -1234,18 +1249,23 @@ img.tag-reference-wiki-image {
|
||||
|
||||
/* Entity/Preset Editor
|
||||
------------------------------------------------------- */
|
||||
.entity-issues,
|
||||
.preset-editor {
|
||||
overflow: hidden;
|
||||
padding: 10px 0px 5px 0px;
|
||||
}
|
||||
.entity-issues a.hide-toggle,
|
||||
.preset-editor a.hide-toggle {
|
||||
margin: 0 20px 5px 20px;
|
||||
}
|
||||
.entity-issues .disclosure-wrap-entity_issues,
|
||||
.preset-editor .form-fields-container {
|
||||
padding: 10px;
|
||||
margin: 0 10px 10px 10px;
|
||||
border-radius: 8px;
|
||||
background: #ececec;
|
||||
}
|
||||
.entity-issues .disclosure-wrap-entity_issues:empty,
|
||||
.preset-editor .form-fields-container:empty {
|
||||
display: none;
|
||||
}
|
||||
@@ -2418,7 +2438,8 @@ input.key-trap {
|
||||
.inspector-hover button,
|
||||
.inspector-hover input,
|
||||
.inspector-hover textarea,
|
||||
.inspector-hover label {
|
||||
.inspector-hover label,
|
||||
.inspector-hover .entity-issues .issue button {
|
||||
background: #ececec;
|
||||
}
|
||||
.inspector-hover .preset-list-button,
|
||||
@@ -2426,6 +2447,14 @@ input.key-trap {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.inspector-hover .entity-issues .issue,
|
||||
.inspector-hover .entity-issues .issue li {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.inspector-hover .entity-issues .issue .icon {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.inspector-hover a,
|
||||
.inspector-hover .form-field-input-multicombo .chips,
|
||||
.inspector-hover .form-field-input-check span {
|
||||
@@ -2452,7 +2481,8 @@ input.key-trap {
|
||||
.inspector-hover .form-field-input-radio label,
|
||||
.inspector-hover .form-field-input-radio label span,
|
||||
.inspector-hover .form-field-input-radio label.remove .icon,
|
||||
.inspector-hover .inspector-inner .add-row {
|
||||
.inspector-hover .inspector-inner .add-row,
|
||||
.inspector-hover .entity-issues .issue ul.issue-fix-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -2760,6 +2790,7 @@ input.key-trap {
|
||||
}
|
||||
|
||||
.map-control > button {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
background: rgba(0,0,0,.5);
|
||||
border-radius: 0;
|
||||
@@ -2861,11 +2892,11 @@ div.full-screen > button:hover {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.layer-list li {
|
||||
position: relative;
|
||||
.layer-list > li {
|
||||
height: 30px;
|
||||
background-color: #fff;
|
||||
color: #7092ff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.layer-list:empty {
|
||||
@@ -2961,6 +2992,157 @@ div.full-screen > button:hover {
|
||||
}
|
||||
|
||||
|
||||
/* Issues
|
||||
------------------------------------------------------- */
|
||||
.issue {
|
||||
overflow: hidden;
|
||||
}
|
||||
.issue button {
|
||||
padding: 5px 10px 5px 5px;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
font-weight: inherit;
|
||||
border-radius: 0;
|
||||
text-align: inherit;
|
||||
display: flex;
|
||||
color: inherit;
|
||||
}
|
||||
[dir='rtl'] .issue button {
|
||||
padding: 5px 5px 5px 10px;
|
||||
}
|
||||
.warnings-list,
|
||||
.issue.severity-warning,
|
||||
li.issue.severity-warning {
|
||||
border-color: #FFDF5C;
|
||||
}
|
||||
.icon-badge.warning {
|
||||
color: #FFDF5C;
|
||||
}
|
||||
.errors-list,
|
||||
.issue.severity-error,
|
||||
li.issue.severity-error {
|
||||
border-color: #f5b0ab;
|
||||
}
|
||||
.icon-badge.error {
|
||||
color: #ff0c05;
|
||||
}
|
||||
|
||||
.issue.severity-warning,
|
||||
.issue.severity-warning button,
|
||||
.mode-save .warning-section {
|
||||
background: #ffb;
|
||||
}
|
||||
.issue.severity-warning:not(.expanded) button:hover,
|
||||
.issue.severity-warning:not(.expanded) button:focus {
|
||||
background: #FFFF99;
|
||||
}
|
||||
.issue.severity-warning .issue-icon,
|
||||
.issue.severity-warning .issue-fix-item.actionable {
|
||||
color: #ff9205;
|
||||
fill: #ff9205;
|
||||
}
|
||||
.issue.severity-warning .issue-fix-item.actionable:hover,
|
||||
.issue.severity-warning .issue-fix-item.actionable button:focus {
|
||||
color: #f07504;
|
||||
fill: #f07504;
|
||||
}
|
||||
|
||||
.issue.severity-error,
|
||||
.issue.severity-error button,
|
||||
.mode-save .error-section {
|
||||
background: #FFD5D4;
|
||||
}
|
||||
.issue.severity-error:not(.expanded) button:hover,
|
||||
.issue.severity-error:not(.expanded) button:focus {
|
||||
background: #ffc9c7;
|
||||
}
|
||||
.issue.severity-error .issue-icon,
|
||||
.issue.severity-error .issue-fix-item.actionable {
|
||||
color: #DD1400;
|
||||
fill: #DD1400;
|
||||
}
|
||||
.issue.severity-error .issue-fix-item.actionable:hover,
|
||||
.issue.severity-error .issue-fix-item.actionable button:focus {
|
||||
color: #ab0f00;
|
||||
fill: #ab0f00;
|
||||
}
|
||||
|
||||
/* Issues Pane */
|
||||
|
||||
.issues-list label {
|
||||
padding: 5px;
|
||||
}
|
||||
.issues-list label > span {
|
||||
display: inline;
|
||||
white-space: normal;
|
||||
}
|
||||
.issues-list li {
|
||||
height: auto;
|
||||
color: inherit;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.issues-none {
|
||||
border-radius: 4px;
|
||||
border: 1px solid #72D979;
|
||||
background: #C6FFCA;
|
||||
padding: 5px !important;
|
||||
display: flex;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.issues-none .icon {
|
||||
color: #05AC10;
|
||||
}
|
||||
|
||||
/* Entity Issues List */
|
||||
.entity-issues .issue {
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
.entity-issues .issue:not(:last-of-type) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.issue.expanded button.message {
|
||||
cursor: auto;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
ul.issue-fix-list button {
|
||||
padding: 2px 10px 2px 26px;
|
||||
}
|
||||
.issue-fix-item:first-of-type button {
|
||||
padding-top:4px;
|
||||
}
|
||||
.issue-fix-item:last-of-type button {
|
||||
padding-bottom:7px;
|
||||
}
|
||||
.issue-fix-item:not(.actionable) button {
|
||||
cursor: auto;
|
||||
|
||||
}
|
||||
.issue-fix-item:not(.actionable) .fix-icon {
|
||||
color: #555;
|
||||
fill: #555;
|
||||
}
|
||||
|
||||
.issue:not(.expanded) ul.issue-fix-list {
|
||||
display: none;
|
||||
}
|
||||
/* don't animate right now
|
||||
.issue ul.issue-fix-list {
|
||||
max-height: 0;
|
||||
transition: max-height 200ms linear;
|
||||
-moz-transition: max-height 200ms linear;
|
||||
-webkit-transition: max-height 200ms linear;
|
||||
}
|
||||
.issue.expanded ul.issue-fix-list {
|
||||
max-height: 180px;
|
||||
transition: max-height 200ms linear;
|
||||
-moz-transition: max-height 200ms linear;
|
||||
-webkit-transition: max-height 200ms linear;
|
||||
}*/
|
||||
|
||||
|
||||
/* Background - Display Options Sliders
|
||||
------------------------------------------------------- */
|
||||
.display-options-container {
|
||||
@@ -4297,14 +4479,6 @@ svg.mouseclick use.right {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mode-save .warning-section {
|
||||
background: #ffb;
|
||||
}
|
||||
|
||||
.mode-save .error-section {
|
||||
background: #ffa5a5;
|
||||
}
|
||||
|
||||
.mode-save .warning-section .changeset-list button {
|
||||
border-left: 1px solid #ccc;
|
||||
}
|
||||
@@ -4432,6 +4606,7 @@ svg.mouseclick use.right {
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
white-space: initial;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tooltip.in {
|
||||
opacity: 0.9;
|
||||
|
||||
+119
-19
@@ -330,6 +330,8 @@ en:
|
||||
modified: Modified
|
||||
deleted: Deleted
|
||||
created: Created
|
||||
outstanding_errors_message: "Please resolve all errors first. {count} remaining."
|
||||
comment_needed_message: Please add a changeset comment first.
|
||||
about_changeset_comments: About changeset comments
|
||||
about_changeset_comments_link: //wiki.openstreetmap.org/wiki/Good_changeset_comments
|
||||
google_warning: "You mentioned Google in this comment: remember that copying from Google Maps is strictly forbidden."
|
||||
@@ -640,24 +642,6 @@ en:
|
||||
description: Description
|
||||
on_wiki: "{tag} on wiki.osm.org"
|
||||
used_with: "used with {type}"
|
||||
validations:
|
||||
disconnected_highway: Disconnected highway
|
||||
disconnected_highway_tooltip: "Roads should be connected to other roads or building entrances."
|
||||
generic_name: Possible generic name
|
||||
generic_name_tooltip: 'This feature seems to have a generic name "{name}". Please only use the name field to record the official name of a feature.'
|
||||
old_multipolygon: Multipolygon tags on outer way
|
||||
old_multipolygon_tooltip: "This style of multipolygon is deprecated. Please assign the tags to the parent multipolygon instead of the outer way."
|
||||
untagged_point: Untagged point
|
||||
untagged_point_tooltip: "Select a feature type that describes what this point is."
|
||||
untagged_line: Untagged line
|
||||
untagged_line_tooltip: "Select a feature type that describes what this line is."
|
||||
untagged_area: Untagged area
|
||||
untagged_area_tooltip: "Select a feature type that describes what this area is."
|
||||
untagged_relation: Untagged relation
|
||||
untagged_relation_tooltip: "Select a feature type that describes what this relation is."
|
||||
many_deletions: "You're deleting {n} features: {p} nodes, {l} lines, {a} areas, {r} relations. Are you sure you want to do this? This will delete them from the map that everyone else sees on openstreetmap.org."
|
||||
tag_suggests_area: "The tag {tag} suggests line should be area, but it is not an area"
|
||||
deprecated_tags: "Deprecated tags: {tags}"
|
||||
zoom:
|
||||
in: Zoom in
|
||||
out: Zoom out
|
||||
@@ -1182,6 +1166,122 @@ en:
|
||||
indirect: "**Some restrictions display the text \"(indirect)\" and are drawn lighter.**"
|
||||
indirect_example: "These restrictions exist because of another nearby restriction. For example, an \"Only Straight On\" restriction will indirectly create \"No Turn\" restrictions for all other paths through the intersection."
|
||||
indirect_noedit: "You may not edit indirect restrictions. Instead, edit the nearby direct restriction."
|
||||
issues:
|
||||
title: Issues
|
||||
key: I
|
||||
list_title: "Issues ({count})"
|
||||
errors:
|
||||
list_title: "Errors ({count})"
|
||||
warnings:
|
||||
list_title: "Warnings ({count})"
|
||||
no_issues:
|
||||
message: Everything looks fine
|
||||
info: Any issues will show up here as you edit
|
||||
almost_junction:
|
||||
message: "{feature} is very close but not connected to {feature2}"
|
||||
highway-highway:
|
||||
tip: Intersecting highways should share a junction vertex.
|
||||
crossing_ways:
|
||||
message: "{feature} crosses {feature2}"
|
||||
building-building:
|
||||
tip: "Buildings should not intersect except on different layers."
|
||||
building-highway:
|
||||
tip: "Highways crossing buildings should use bridges, tunnels, coverings, or entrances."
|
||||
building-railway:
|
||||
tip: "Railways crossing buildings should use bridges or tunnels."
|
||||
building-waterway:
|
||||
tip: "Waterways crossing buildings should use tunnels or different layers."
|
||||
highway-highway:
|
||||
tip: "Crossing highways should use bridges, tunnels, or intersections."
|
||||
highway-railway:
|
||||
tip: "Highways crossing railways should use bridges, tunnels, or level crossings."
|
||||
highway-waterway:
|
||||
tip: "Highways crossing waterways should use bridges, tunnels, or fords."
|
||||
railway-railway:
|
||||
tip: "Crossing railways should be connected or use bridges or tunnels."
|
||||
railway-waterway:
|
||||
tip: "Railways crossing waterways should use bridges or tunnels."
|
||||
waterway-waterway:
|
||||
tip: "Crossing waterways should be connected or use tunnels."
|
||||
tunnel-tunnel:
|
||||
tip: "Crossing tunnels should use different layers."
|
||||
tunnel-tunnel_connectable:
|
||||
tip: "Crossing tunnels should be connected or use different layers."
|
||||
bridge-bridge:
|
||||
tip: "Crossing bridges should use different layers."
|
||||
bridge-bridge_connectable:
|
||||
tip: "Crossing bridges should be connected or use different layers."
|
||||
deprecated_tag:
|
||||
single:
|
||||
message: '{feature} has the outdated tag "{tag}"'
|
||||
combination:
|
||||
message: '{feature} has an outdated tag combination: {tags}'
|
||||
tip: "Some tags become deprecated over time and should be replaced."
|
||||
disconnected_way:
|
||||
highway:
|
||||
message: "{highway} is disconnected from other roads and paths"
|
||||
tip: "Highways should connect to other highways or building entrances."
|
||||
generic_name:
|
||||
message: '{feature} has the generic name "{name}"'
|
||||
tip: "Names should be the actual, on-the-ground names of features."
|
||||
many_deletions:
|
||||
points-lines-areas:
|
||||
message: "Deleting {n} features: {p} points, {l} lines, and {a} areas"
|
||||
points-lines-areas-relations:
|
||||
message: "Deleting {n} features: {p} points, {l} lines, {a} areas, and {r} relations"
|
||||
tip: "Only redundant or nonexistent features should be deleted."
|
||||
missing_tag:
|
||||
any:
|
||||
message: "{feature} has no tags"
|
||||
descriptive:
|
||||
message: "{feature} has no descriptive tags"
|
||||
specific:
|
||||
message: '{feature} has no "{tag}" tag'
|
||||
tip: "Features must have tags that define what they are."
|
||||
old_multipolygon:
|
||||
message: "{multipolygon} has misplaced tags"
|
||||
tip: "Multipolygons should be tagged on their relation, not their outer way."
|
||||
tag_suggests_area:
|
||||
message: '{feature} should be a closed area based on the tag "{tag}"'
|
||||
tip: "Areas must have connected endpoints."
|
||||
fix:
|
||||
connect_almost_junction:
|
||||
annotation: Connected very close features.
|
||||
connect_crossing_features:
|
||||
annotation: Connected crossing features.
|
||||
connect_endpoints:
|
||||
title: Connect the ends
|
||||
annotation: Connected the endpoints of a way.
|
||||
connect_features:
|
||||
title: Connect the features
|
||||
continue_from_start:
|
||||
title: Continue drawing from start
|
||||
continue_from_end:
|
||||
title: Continue drawing from end
|
||||
delete_feature:
|
||||
title: Delete this feature
|
||||
move_tags:
|
||||
title: Move the tags
|
||||
annotation: Moved tags.
|
||||
remove_generic_name:
|
||||
title: Remove the name
|
||||
annotation: Removed a generic name.
|
||||
remove_tag:
|
||||
title: Remove the tag
|
||||
annotation: Removed tag.
|
||||
reposition_features:
|
||||
title: Reposition the features
|
||||
select_preset:
|
||||
title: Select a feature type
|
||||
tag_as_disconnected:
|
||||
title: Tag as disconnected
|
||||
annotation: Tagged very close features as disconnected.
|
||||
upgrade_tag:
|
||||
title: Upgrade this tag
|
||||
annotation: Upgraded an old tag.
|
||||
upgrade_tag_combo:
|
||||
title: Upgrade these tags
|
||||
annotation: Upgraded an old tag combination.
|
||||
intro:
|
||||
done: done
|
||||
ok: OK
|
||||
@@ -1551,4 +1651,4 @@ en:
|
||||
wikidata:
|
||||
identifier: "Identifier"
|
||||
label: "Label"
|
||||
description: "Description"
|
||||
description: "Description"
|
||||
|
||||
+221
-2
@@ -1,9 +1,77 @@
|
||||
{
|
||||
"dataDeprecated": [
|
||||
{
|
||||
"old": {"amenity": "advertising"},
|
||||
"replace": {"advertising": "*"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "artwork"},
|
||||
"replace": {"tourism": "artwork"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "car_repair"},
|
||||
"replace": {"shop": "car_repair"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "citymap_post"},
|
||||
"replace": {"tourism": "information"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "community_center"},
|
||||
"replace": {"amenity": "community_centre"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "ev_charging"},
|
||||
"replace": {"amenity": "charging_station"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "firepit"},
|
||||
"replace": {"leisure": "firepit"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "garage"},
|
||||
"replace": {"landuse": "garages"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "garages"},
|
||||
"replace": {"landuse": "garages"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "real_estate"},
|
||||
"replace": {"office": "estate_agent"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "register_office"},
|
||||
"replace": {"office": "government", "government": "register_office"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "sauna"},
|
||||
"replace": {"leisure": "sauna"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "scrapyard"},
|
||||
"replace": {"landuse": "industrial", "industrial": "scrap_yard"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "shop"},
|
||||
"replace": {"shop": "*"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "swimming_pool"},
|
||||
"replace": {"leisure": "swimming_pool"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "vending_machine", "vending": "news_papers"},
|
||||
"replace": {"amenity": "vending_machine", "vending": "newspapers"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "youth_center"},
|
||||
"replace": {"amenity": "community_centre", "community_centre:for": "juvenile"}
|
||||
},
|
||||
{
|
||||
"old": {"amenity": "youth_centre"},
|
||||
"replace": {"amenity": "community_centre", "community_centre:for": "juvenile"}
|
||||
},
|
||||
{
|
||||
"old": {"barrier": "wire_fence"},
|
||||
"replace": {"barrier": "fence", "fence_type": "chain"}
|
||||
@@ -12,9 +80,48 @@
|
||||
"old": {"barrier": "wood_fence"},
|
||||
"replace": {"barrier": "fence", "fence_type": "wood"}
|
||||
},
|
||||
{
|
||||
"old": {"building": "entrance"},
|
||||
"replace": {"entrance": "*"}
|
||||
},
|
||||
{
|
||||
"old": {"building:roof:colour": "*"},
|
||||
"replace": {"roof:colour": "$1"}
|
||||
},
|
||||
{
|
||||
"old": {"building:type": "*"},
|
||||
"replace": {"building": "$1"}
|
||||
},
|
||||
{
|
||||
"old": {"color": "*"},
|
||||
"replace": {"colour": "$1"}
|
||||
},
|
||||
{
|
||||
"old": {"craft": "jeweler"},
|
||||
"replace": {"shop": "jewelery"}
|
||||
},
|
||||
{
|
||||
"old": {"craft": "optician"},
|
||||
"replace": {"shop": "optician"}
|
||||
},
|
||||
{
|
||||
"old": {"drinkable": "*"},
|
||||
"replace": {"drinking_water": "$1"}
|
||||
},
|
||||
{
|
||||
"old": {"escalator": "*"},
|
||||
"replace": {"highway": "steps", "conveying": "$1"}
|
||||
},
|
||||
{
|
||||
"old": {"fenced": "yes"},
|
||||
"replace": {"barrier": "fence"}
|
||||
},
|
||||
{
|
||||
"old": {"highway": "ford"},
|
||||
"replace": {"ford": "yes"}
|
||||
"replace": {"ford": "*"}
|
||||
},
|
||||
{
|
||||
"old": {"highway": "no"}
|
||||
},
|
||||
{
|
||||
"old": {"highway": "stile"},
|
||||
@@ -32,14 +139,74 @@
|
||||
"old": {"highway": "unsurfaced"},
|
||||
"replace": {"highway": "road", "incline": "unpaved"}
|
||||
},
|
||||
{
|
||||
"old": {"landuse": "farm"},
|
||||
"replace": {"landuse": "farmland"}
|
||||
},
|
||||
{
|
||||
"old": {"landuse": "field"},
|
||||
"replace": {"landuse": "farmland"}
|
||||
},
|
||||
{
|
||||
"old": {"landuse": "pond"},
|
||||
"replace": {"natural": "water", "water": "pond"}
|
||||
},
|
||||
{
|
||||
"old": {"landuse": "wood"},
|
||||
"replace": {"landuse": "forest", "natural": "wood"}
|
||||
"replace": {"natural": "wood"}
|
||||
},
|
||||
{
|
||||
"old": {"leisure": "beach"},
|
||||
"replace": {"natural": "beach"}
|
||||
},
|
||||
{
|
||||
"old": {"leisure": "club"},
|
||||
"replace": {"club": "*"}
|
||||
},
|
||||
{
|
||||
"old": {"man_made": "jetty"},
|
||||
"replace": {"man_made": "pier"}
|
||||
},
|
||||
{
|
||||
"old": {"man_made": "mdf"},
|
||||
"replace": {"telecom": "exchange"}
|
||||
},
|
||||
{
|
||||
"old": {"man_made": "MDF"},
|
||||
"replace": {"telecom": "exchange"}
|
||||
},
|
||||
{
|
||||
"old": {"man_made": "water_tank"},
|
||||
"replace": {"man_made": "storage_tank", "content": "water"}
|
||||
},
|
||||
{
|
||||
"old": {"man_made": "well"},
|
||||
"replace": {"man_made": "water_well"}
|
||||
},
|
||||
{
|
||||
"old": {"natural": "marsh"},
|
||||
"replace": {"natural": "wetland", "wetland": "marsh"}
|
||||
},
|
||||
{
|
||||
"old": {"office": "administrative"},
|
||||
"replace": {"office": "government"}
|
||||
},
|
||||
{
|
||||
"old": {"office": "real_estate"},
|
||||
"replace": {"office": "estate_agent"}
|
||||
},
|
||||
{
|
||||
"old": {"place_name": "*"},
|
||||
"replace": {"name": "$1"}
|
||||
},
|
||||
{
|
||||
"old": {"pole": "transition"},
|
||||
"replace": {"location:transition": "yes"}
|
||||
},
|
||||
{
|
||||
"old": {"power": "sub_station"},
|
||||
"replace": {"power": "substation"}
|
||||
},
|
||||
{
|
||||
"old": {"power_source": "*"},
|
||||
"replace": {"generator:source": "$1"}
|
||||
@@ -48,9 +215,61 @@
|
||||
"old": {"power_rating": "*"},
|
||||
"replace": {"generator:output": "$1"}
|
||||
},
|
||||
{
|
||||
"old": {"roof:color": "*"},
|
||||
"replace": {"roof:colour": "$1"}
|
||||
},
|
||||
{
|
||||
"old": {"route": "ncn"},
|
||||
"replace": {"route": "bicycle", "network": "ncn"}
|
||||
},
|
||||
{
|
||||
"old": {"shop": "organic"},
|
||||
"replace": {"shop": "supermarket", "organic": "only"}
|
||||
},
|
||||
{
|
||||
"old": {"shop": "fish"},
|
||||
"replace": {"shop": "seafood"}
|
||||
},
|
||||
{
|
||||
"old": {"shop": "fishmonger"},
|
||||
"replace": {"shop": "seafood"}
|
||||
},
|
||||
{
|
||||
"old": {"shop": "furnace"},
|
||||
"replace": {"shop": "fireplace"}
|
||||
},
|
||||
{
|
||||
"old": {"shop": "gallery"},
|
||||
"replace": {"shop": "art"}
|
||||
},
|
||||
{
|
||||
"old": {"shop": "perfume"},
|
||||
"replace": {"shop": "perfumery"}
|
||||
},
|
||||
{
|
||||
"old": {"shop": "real_estate"},
|
||||
"replace": {"office": "estate_agent"}
|
||||
},
|
||||
{
|
||||
"old": {"tourism": "bed_and_breakfast"},
|
||||
"replace": {"tourism": "guest_house"}
|
||||
},
|
||||
{
|
||||
"old": {"water": "intermittent"},
|
||||
"replace": {"natural": "water", "intermittent": "yes"}
|
||||
},
|
||||
{
|
||||
"old": {"water": "riverbank"},
|
||||
"replace": {"natural": "water", "water": "river"}
|
||||
},
|
||||
{
|
||||
"old": {"water": "salt"},
|
||||
"replace": {"natural": "water", "salt": "yes"}
|
||||
},
|
||||
{
|
||||
"old": {"water": "tidal"},
|
||||
"replace": {"natural": "water", "tidal": "yes"}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+170
-19
@@ -414,6 +414,8 @@
|
||||
"modified": "Modified",
|
||||
"deleted": "Deleted",
|
||||
"created": "Created",
|
||||
"outstanding_errors_message": "Please resolve all errors first. {count} remaining.",
|
||||
"comment_needed_message": "Please add a changeset comment first.",
|
||||
"about_changeset_comments": "About changeset comments",
|
||||
"about_changeset_comments_link": "//wiki.openstreetmap.org/wiki/Good_changeset_comments",
|
||||
"google_warning": "You mentioned Google in this comment: remember that copying from Google Maps is strictly forbidden.",
|
||||
@@ -781,25 +783,6 @@
|
||||
"on_wiki": "{tag} on wiki.osm.org",
|
||||
"used_with": "used with {type}"
|
||||
},
|
||||
"validations": {
|
||||
"disconnected_highway": "Disconnected highway",
|
||||
"disconnected_highway_tooltip": "Roads should be connected to other roads or building entrances.",
|
||||
"generic_name": "Possible generic name",
|
||||
"generic_name_tooltip": "This feature seems to have a generic name \"{name}\". Please only use the name field to record the official name of a feature.",
|
||||
"old_multipolygon": "Multipolygon tags on outer way",
|
||||
"old_multipolygon_tooltip": "This style of multipolygon is deprecated. Please assign the tags to the parent multipolygon instead of the outer way.",
|
||||
"untagged_point": "Untagged point",
|
||||
"untagged_point_tooltip": "Select a feature type that describes what this point is.",
|
||||
"untagged_line": "Untagged line",
|
||||
"untagged_line_tooltip": "Select a feature type that describes what this line is.",
|
||||
"untagged_area": "Untagged area",
|
||||
"untagged_area_tooltip": "Select a feature type that describes what this area is.",
|
||||
"untagged_relation": "Untagged relation",
|
||||
"untagged_relation_tooltip": "Select a feature type that describes what this relation is.",
|
||||
"many_deletions": "You're deleting {n} features: {p} nodes, {l} lines, {a} areas, {r} relations. Are you sure you want to do this? This will delete them from the map that everyone else sees on openstreetmap.org.",
|
||||
"tag_suggests_area": "The tag {tag} suggests line should be area, but it is not an area",
|
||||
"deprecated_tags": "Deprecated tags: {tags}"
|
||||
},
|
||||
"zoom": {
|
||||
"in": "Zoom in",
|
||||
"out": "Zoom out"
|
||||
@@ -1431,6 +1414,174 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"title": "Issues",
|
||||
"key": "I",
|
||||
"list_title": "Issues ({count})",
|
||||
"errors": {
|
||||
"list_title": "Errors ({count})"
|
||||
},
|
||||
"warnings": {
|
||||
"list_title": "Warnings ({count})"
|
||||
},
|
||||
"no_issues": {
|
||||
"message": "Everything looks fine",
|
||||
"info": "Any issues will show up here as you edit"
|
||||
},
|
||||
"almost_junction": {
|
||||
"message": "{feature} is very close but not connected to {feature2}",
|
||||
"highway-highway": {
|
||||
"tip": "Intersecting highways should share a junction vertex."
|
||||
}
|
||||
},
|
||||
"crossing_ways": {
|
||||
"message": "{feature} crosses {feature2}",
|
||||
"building-building": {
|
||||
"tip": "Buildings should not intersect except on different layers."
|
||||
},
|
||||
"building-highway": {
|
||||
"tip": "Highways crossing buildings should use bridges, tunnels, coverings, or entrances."
|
||||
},
|
||||
"building-railway": {
|
||||
"tip": "Railways crossing buildings should use bridges or tunnels."
|
||||
},
|
||||
"building-waterway": {
|
||||
"tip": "Waterways crossing buildings should use tunnels or different layers."
|
||||
},
|
||||
"highway-highway": {
|
||||
"tip": "Crossing highways should use bridges, tunnels, or intersections."
|
||||
},
|
||||
"highway-railway": {
|
||||
"tip": "Highways crossing railways should use bridges, tunnels, or level crossings."
|
||||
},
|
||||
"highway-waterway": {
|
||||
"tip": "Highways crossing waterways should use bridges, tunnels, or fords."
|
||||
},
|
||||
"railway-railway": {
|
||||
"tip": "Crossing railways should be connected or use bridges or tunnels."
|
||||
},
|
||||
"railway-waterway": {
|
||||
"tip": "Railways crossing waterways should use bridges or tunnels."
|
||||
},
|
||||
"waterway-waterway": {
|
||||
"tip": "Crossing waterways should be connected or use tunnels."
|
||||
},
|
||||
"tunnel-tunnel": {
|
||||
"tip": "Crossing tunnels should use different layers."
|
||||
},
|
||||
"tunnel-tunnel_connectable": {
|
||||
"tip": "Crossing tunnels should be connected or use different layers."
|
||||
},
|
||||
"bridge-bridge": {
|
||||
"tip": "Crossing bridges should use different layers."
|
||||
},
|
||||
"bridge-bridge_connectable": {
|
||||
"tip": "Crossing bridges should be connected or use different layers."
|
||||
}
|
||||
},
|
||||
"deprecated_tag": {
|
||||
"single": {
|
||||
"message": "{feature} has the outdated tag \"{tag}\""
|
||||
},
|
||||
"combination": {
|
||||
"message": "{feature} has an outdated tag combination: {tags}"
|
||||
},
|
||||
"tip": "Some tags become deprecated over time and should be replaced."
|
||||
},
|
||||
"disconnected_way": {
|
||||
"highway": {
|
||||
"message": "{highway} is disconnected from other roads and paths",
|
||||
"tip": "Highways should connect to other highways or building entrances."
|
||||
}
|
||||
},
|
||||
"generic_name": {
|
||||
"message": "{feature} has the generic name \"{name}\"",
|
||||
"tip": "Names should be the actual, on-the-ground names of features."
|
||||
},
|
||||
"many_deletions": {
|
||||
"points-lines-areas": {
|
||||
"message": "Deleting {n} features: {p} points, {l} lines, and {a} areas"
|
||||
},
|
||||
"points-lines-areas-relations": {
|
||||
"message": "Deleting {n} features: {p} points, {l} lines, {a} areas, and {r} relations"
|
||||
},
|
||||
"tip": "Only redundant or nonexistent features should be deleted."
|
||||
},
|
||||
"missing_tag": {
|
||||
"any": {
|
||||
"message": "{feature} has no tags"
|
||||
},
|
||||
"descriptive": {
|
||||
"message": "{feature} has no descriptive tags"
|
||||
},
|
||||
"specific": {
|
||||
"message": "{feature} has no \"{tag}\" tag"
|
||||
},
|
||||
"tip": "Features must have tags that define what they are."
|
||||
},
|
||||
"old_multipolygon": {
|
||||
"message": "{multipolygon} has misplaced tags",
|
||||
"tip": "Multipolygons should be tagged on their relation, not their outer way."
|
||||
},
|
||||
"tag_suggests_area": {
|
||||
"message": "{feature} should be a closed area based on the tag \"{tag}\"",
|
||||
"tip": "Areas must have connected endpoints."
|
||||
},
|
||||
"fix": {
|
||||
"connect_almost_junction": {
|
||||
"annotation": "Connected very close features."
|
||||
},
|
||||
"connect_crossing_features": {
|
||||
"annotation": "Connected crossing features."
|
||||
},
|
||||
"connect_endpoints": {
|
||||
"title": "Connect the ends",
|
||||
"annotation": "Connected the endpoints of a way."
|
||||
},
|
||||
"connect_features": {
|
||||
"title": "Connect the features"
|
||||
},
|
||||
"continue_from_start": {
|
||||
"title": "Continue drawing from start"
|
||||
},
|
||||
"continue_from_end": {
|
||||
"title": "Continue drawing from end"
|
||||
},
|
||||
"delete_feature": {
|
||||
"title": "Delete this feature"
|
||||
},
|
||||
"move_tags": {
|
||||
"title": "Move the tags",
|
||||
"annotation": "Moved tags."
|
||||
},
|
||||
"remove_generic_name": {
|
||||
"title": "Remove the name",
|
||||
"annotation": "Removed a generic name."
|
||||
},
|
||||
"remove_tag": {
|
||||
"title": "Remove the tag",
|
||||
"annotation": "Removed tag."
|
||||
},
|
||||
"reposition_features": {
|
||||
"title": "Reposition the features"
|
||||
},
|
||||
"select_preset": {
|
||||
"title": "Select a feature type"
|
||||
},
|
||||
"tag_as_disconnected": {
|
||||
"title": "Tag as disconnected",
|
||||
"annotation": "Tagged very close features as disconnected."
|
||||
},
|
||||
"upgrade_tag": {
|
||||
"title": "Upgrade this tag",
|
||||
"annotation": "Upgraded an old tag."
|
||||
},
|
||||
"upgrade_tag_combo": {
|
||||
"title": "Upgrade these tags",
|
||||
"annotation": "Upgraded an old tag combination."
|
||||
}
|
||||
}
|
||||
},
|
||||
"intro": {
|
||||
"done": "done",
|
||||
"ok": "OK",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { geoVecAdd, geoVecScale } from '../geo';
|
||||
// 1. move all the nodes to a common location
|
||||
// 2. `actionConnect` them
|
||||
|
||||
export function actionMergeNodes(nodeIDs) {
|
||||
export function actionMergeNodes(nodeIDs, loc) {
|
||||
|
||||
// If there is a single "interesting" node, use that as the location.
|
||||
// Otherwise return the average location of all the nodes.
|
||||
@@ -31,11 +31,16 @@ export function actionMergeNodes(nodeIDs) {
|
||||
|
||||
var action = function(graph) {
|
||||
if (nodeIDs.length < 2) return graph;
|
||||
var toLoc = chooseLoc(graph);
|
||||
var toLoc = loc;
|
||||
if (!toLoc) {
|
||||
toLoc = chooseLoc(graph);
|
||||
}
|
||||
|
||||
for (var i = 0; i < nodeIDs.length; i++) {
|
||||
var node = graph.entity(nodeIDs[i]);
|
||||
graph = graph.replace(node.move(toLoc));
|
||||
if (node.loc !== toLoc) {
|
||||
graph = graph.replace(node.move(toLoc));
|
||||
}
|
||||
}
|
||||
|
||||
return actionConnect(nodeIDs)(graph);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { actionAddMember } from './add_member';
|
||||
import { geoSphericalDistance } from '../geo';
|
||||
|
||||
import {
|
||||
osmIsSimpleMultipolygonOuterMember,
|
||||
osmIsOldMultipolygonOuterMember,
|
||||
osmRelation,
|
||||
osmWay
|
||||
} from '../osm';
|
||||
@@ -93,7 +93,7 @@ export function actionSplit(nodeId, newWayIds) {
|
||||
var nodesA;
|
||||
var nodesB;
|
||||
var isArea = wayA.isArea();
|
||||
var isOuter = osmIsSimpleMultipolygonOuterMember(wayA, graph);
|
||||
var isOuter = osmIsOldMultipolygonOuterMember(wayA, graph);
|
||||
|
||||
if (wayA.isClosed()) {
|
||||
var nodes = wayA.nodes.slice(0, -1);
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
import { t } from '../util/locale';
|
||||
|
||||
import {
|
||||
event as d3_event,
|
||||
select as d3_select
|
||||
} from 'd3-selection';
|
||||
|
||||
import {
|
||||
actionAddMidpoint,
|
||||
actionMoveNode,
|
||||
actionNoop
|
||||
} from '../actions';
|
||||
|
||||
import { t } from '../util/locale';
|
||||
import { actionAddMidpoint, actionMoveNode, actionNoop } from '../actions';
|
||||
import { behaviorDraw } from './draw';
|
||||
import { geoChooseEdge, geoHasSelfIntersections } from '../geo';
|
||||
import { modeBrowse, modeSelect } from '../modes';
|
||||
import { osmNode } from '../osm';
|
||||
import { utilKeybinding } from '../util';
|
||||
|
||||
export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
|
||||
var origWay = context.entity(wayId);
|
||||
export function behaviorDrawWay(context, wayID, index, mode, startGraph) {
|
||||
var origWay = context.entity(wayID);
|
||||
|
||||
var annotation = t((origWay.isDegenerate() ?
|
||||
'operations.start.annotation.' :
|
||||
'operations.continue.annotation.') + context.geometry(wayId)
|
||||
'operations.continue.annotation.') + context.geometry(wayID)
|
||||
);
|
||||
|
||||
var behavior = behaviorDraw(context);
|
||||
@@ -31,6 +26,10 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
|
||||
|
||||
var end = osmNode({ loc: context.map().mouseCoordinates() });
|
||||
|
||||
if (context.graph() === startGraph) {
|
||||
context.history().checkpoint('drawWay-initial');
|
||||
}
|
||||
|
||||
// Push an annotated state for undo to return back to.
|
||||
// We must make sure to remove this edit later.
|
||||
context.perform(actionNoop(), annotation);
|
||||
@@ -67,10 +66,12 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function allowsVertex(d) {
|
||||
return context.presets().allowsVertex(d, context.graph());
|
||||
}
|
||||
|
||||
|
||||
// related code
|
||||
// - `mode/drag_node.js` `doMode()`
|
||||
// - `behavior/draw.js` `click()`
|
||||
@@ -122,7 +123,8 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
|
||||
|
||||
for (var i = 0; i < parents.length; i++) {
|
||||
var parent = parents[i];
|
||||
var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); });
|
||||
|
||||
var nodes = graph.childNodes(parent);
|
||||
|
||||
if (origWay.isClosed()) { // Check if Area
|
||||
if (finishDraw) {
|
||||
@@ -148,14 +150,16 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
|
||||
|
||||
function undone() {
|
||||
// Undo popped the history back to the initial annotated no-op edit.
|
||||
// Remove initial no-op edit and whatever edit happened immediately before it.
|
||||
context.pop(2);
|
||||
_tempEdits = 0;
|
||||
_tempEdits = 0; // We will deal with the temp edits here
|
||||
context.pop(1); // Remove initial no-op edit
|
||||
|
||||
if (context.hasEntity(wayId)) {
|
||||
context.enter(mode);
|
||||
if (context.graph() === startGraph) { // We've undone back to the beginning
|
||||
context.history().reset('drawWay-initial');
|
||||
context.enter(modeSelect(context, [wayID]));
|
||||
} else {
|
||||
context.enter(modeBrowse(context));
|
||||
// Remove whatever segment was drawn previously and continue drawing
|
||||
context.pop(1);
|
||||
context.enter(mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,10 +202,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
|
||||
// This can happen if the user changes modes,
|
||||
// clicks geolocate button, a hashchange event occurs, etc.
|
||||
if (_tempEdits) {
|
||||
context.pop(_tempEdits);
|
||||
while (context.graph() !== startGraph) {
|
||||
context.pop();
|
||||
}
|
||||
context.history().reset('drawWay-initial');
|
||||
}
|
||||
|
||||
context.map()
|
||||
@@ -313,7 +314,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
|
||||
context.pop(_tempEdits);
|
||||
_tempEdits = 0;
|
||||
|
||||
var way = context.hasEntity(wayId);
|
||||
var way = context.hasEntity(wayID);
|
||||
if (!way || way.isDegenerate()) {
|
||||
drawWay.cancel();
|
||||
return;
|
||||
@@ -323,7 +324,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
|
||||
context.map().dblclickEnable(true);
|
||||
}, 1000);
|
||||
var isNewFeature = !mode.isContinuing;
|
||||
context.enter(modeSelect(context, [wayId]).newFeature(isNewFeature));
|
||||
context.enter(modeSelect(context, [wayID]).newFeature(isNewFeature));
|
||||
};
|
||||
|
||||
|
||||
|
||||
+23
-2
@@ -13,6 +13,7 @@ import { select as d3_select } from 'd3-selection';
|
||||
import { t, currentLocale, addTranslation, setLocale } from '../util/locale';
|
||||
|
||||
import { coreHistory } from './history';
|
||||
import { coreValidator } from './validator';
|
||||
import { dataLocales, dataEn } from '../../data';
|
||||
import { geoRawMercator } from '../geo/raw_mercator';
|
||||
import { modeSelect } from '../modes/select';
|
||||
@@ -95,10 +96,10 @@ export function coreContext() {
|
||||
|
||||
|
||||
/* Straight accessors. Avoid using these if you can. */
|
||||
var connection, history;
|
||||
var connection, history, validator;
|
||||
context.connection = function() { return connection; };
|
||||
context.history = function() { return history; };
|
||||
|
||||
context.validator = function() { return validator; };
|
||||
|
||||
/* Connection */
|
||||
context.preauth = function(options) {
|
||||
@@ -451,10 +452,30 @@ export function coreContext() {
|
||||
}
|
||||
|
||||
history = coreHistory(context);
|
||||
|
||||
context.graph = history.graph;
|
||||
context.changes = history.changes;
|
||||
context.intersects = history.intersects;
|
||||
|
||||
validator = coreValidator(context);
|
||||
|
||||
// run validation upon restoring from page reload
|
||||
history.on('restore', function() {
|
||||
validator.validate();
|
||||
});
|
||||
// re-run validation upon a significant graph change
|
||||
history.on('annotatedChange', function(difference) {
|
||||
if (difference) {
|
||||
validator.validate();
|
||||
}
|
||||
});
|
||||
// re-run validation upon merging fetched data
|
||||
history.on('merge', function(entities) {
|
||||
if (entities && entities.length > 0) {
|
||||
validator.validate();
|
||||
}
|
||||
});
|
||||
|
||||
// Debounce save, since it's a synchronous localStorage write,
|
||||
// and history changes can happen frequently (e.g. when dragging).
|
||||
context.debouncedSave = _debounce(context.save, 350);
|
||||
|
||||
+26
-18
@@ -18,7 +18,6 @@ import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { easeLinear as d3_easeLinear } from 'd3-ease';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
|
||||
import * as Validations from '../validations/index';
|
||||
import { coreDifference } from './difference';
|
||||
import { coreGraph } from './graph';
|
||||
import { coreTree } from './tree';
|
||||
@@ -32,7 +31,7 @@ import {
|
||||
|
||||
|
||||
export function coreHistory(context) {
|
||||
var dispatch = d3_dispatch('change', 'undone', 'redone');
|
||||
var dispatch = d3_dispatch('change', 'annotatedChange', 'merge', 'restore', 'undone', 'redone');
|
||||
var lock = utilSessionMutex('lock');
|
||||
var duration = 150;
|
||||
var _imageryUsed = [];
|
||||
@@ -70,9 +69,10 @@ export function coreHistory(context) {
|
||||
function _perform(args, t) {
|
||||
var previous = _stack[_index].graph;
|
||||
_stack = _stack.slice(0, _index + 1);
|
||||
_stack.push(_act(args, t));
|
||||
var actionResult = _act(args, t);
|
||||
_stack.push(actionResult);
|
||||
_index++;
|
||||
return change(previous);
|
||||
return change(previous, actionResult.annotation);
|
||||
}
|
||||
|
||||
|
||||
@@ -80,8 +80,9 @@ export function coreHistory(context) {
|
||||
function _replace(args, t) {
|
||||
var previous = _stack[_index].graph;
|
||||
// assert(_index == _stack.length - 1)
|
||||
_stack[_index] = _act(args, t);
|
||||
return change(previous);
|
||||
var actionResult = _act(args, t);
|
||||
_stack[_index] = actionResult;
|
||||
return change(previous, actionResult.annotation);
|
||||
}
|
||||
|
||||
|
||||
@@ -93,16 +94,22 @@ export function coreHistory(context) {
|
||||
_stack.pop();
|
||||
}
|
||||
_stack = _stack.slice(0, _index + 1);
|
||||
_stack.push(_act(args, t));
|
||||
var actionResult = _act(args, t);
|
||||
_stack.push(actionResult);
|
||||
_index++;
|
||||
return change(previous);
|
||||
return change(previous, actionResult.annotation);
|
||||
}
|
||||
|
||||
|
||||
// determine difference and dispatch a change event
|
||||
function change(previous) {
|
||||
function change(previous, isAnnotated) {
|
||||
var difference = coreDifference(previous, history.graph());
|
||||
dispatch.call('change', this, difference);
|
||||
if (isAnnotated) {
|
||||
// actions like dragging a node can fire lots of changes,
|
||||
// so use 'annotatedChange' to listen for grouped undo/redo changes
|
||||
dispatch.call('annotatedChange', this, difference);
|
||||
}
|
||||
return difference;
|
||||
}
|
||||
|
||||
@@ -120,6 +127,11 @@ export function coreHistory(context) {
|
||||
},
|
||||
|
||||
|
||||
tree: function() {
|
||||
return _tree;
|
||||
},
|
||||
|
||||
|
||||
base: function() {
|
||||
return _stack[0].graph;
|
||||
},
|
||||
@@ -130,6 +142,7 @@ export function coreHistory(context) {
|
||||
_tree.rebase(entities, false);
|
||||
|
||||
dispatch.call('change', this, undefined, extent);
|
||||
dispatch.call('merge', this, entities);
|
||||
},
|
||||
|
||||
|
||||
@@ -208,7 +221,7 @@ export function coreHistory(context) {
|
||||
}
|
||||
|
||||
dispatch.call('undone', this, _stack[_index]);
|
||||
return change(previous);
|
||||
return change(previous, true);
|
||||
},
|
||||
|
||||
|
||||
@@ -227,7 +240,7 @@ export function coreHistory(context) {
|
||||
}
|
||||
}
|
||||
|
||||
return change(previous);
|
||||
return change(previous, true);
|
||||
},
|
||||
|
||||
|
||||
@@ -279,13 +292,6 @@ export function coreHistory(context) {
|
||||
},
|
||||
|
||||
|
||||
validate: function(changes) {
|
||||
return _flatten(_map(Validations, function(fn) {
|
||||
return fn()(changes, _stack[_index].graph);
|
||||
}));
|
||||
},
|
||||
|
||||
|
||||
hasChanges: function() {
|
||||
return this.difference().length() > 0;
|
||||
},
|
||||
@@ -511,6 +517,7 @@ export function coreHistory(context) {
|
||||
loading.close();
|
||||
context.redrawEnable(true);
|
||||
dispatch.call('change');
|
||||
dispatch.call('restore', this);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -565,6 +572,7 @@ export function coreHistory(context) {
|
||||
|
||||
if (loadComplete) {
|
||||
dispatch.call('change');
|
||||
dispatch.call('restore', this);
|
||||
}
|
||||
|
||||
return history;
|
||||
|
||||
@@ -3,3 +3,4 @@ export { coreDifference } from './difference';
|
||||
export { coreGraph } from './graph';
|
||||
export { coreHistory } from './history';
|
||||
export { coreTree } from './tree';
|
||||
export { coreValidator } from './validator';
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import _isFunction from 'lodash-es/isFunction';
|
||||
import _map from 'lodash-es/map';
|
||||
import _filter from 'lodash-es/filter';
|
||||
import _flatten from 'lodash-es/flatten';
|
||||
import _flattenDeep from 'lodash-es/flattenDeep';
|
||||
import _uniq from 'lodash-es/uniq';
|
||||
import _uniqWith from 'lodash-es/uniqWith';
|
||||
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
|
||||
import { geoExtent } from '../geo';
|
||||
import { osmEntity } from '../osm';
|
||||
import { utilRebind } from '../util/rebind';
|
||||
import * as Validations from '../validations/index';
|
||||
|
||||
|
||||
export function coreValidator(context) {
|
||||
var dispatch = d3_dispatch('reload');
|
||||
var self = {};
|
||||
var _issues = [];
|
||||
var _issuesByEntityID = {};
|
||||
|
||||
var validations = _filter(Validations, _isFunction).reduce(function(obj, validation) {
|
||||
var func = validation();
|
||||
obj[func.type] = func;
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
var entityValidationIDs = [];
|
||||
var changesValidationIDs = [];
|
||||
|
||||
for (var key in validations) {
|
||||
var validation = validations[key];
|
||||
if (validation.inputType && validation.inputType === 'changes') {
|
||||
changesValidationIDs.push(key);
|
||||
} else {
|
||||
entityValidationIDs.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
//self.featureApplicabilityOptions = ['edited', 'all'];
|
||||
|
||||
/*var featureApplicability = context.storage('issue-features') || 'edited';
|
||||
|
||||
self.getFeatureApplicability = function() {
|
||||
return featureApplicability;
|
||||
};
|
||||
|
||||
self.setFeatureApplicability = function(applicability) {
|
||||
featureApplicability = applicability;
|
||||
context.storage('issue-features', applicability);
|
||||
};*/
|
||||
|
||||
self.getIssues = function() {
|
||||
return _issues;
|
||||
};
|
||||
|
||||
self.getWarnings = function() {
|
||||
return _issues.filter(function(d) { return d.severity === 'warning'; });
|
||||
};
|
||||
|
||||
self.getErrors = function() {
|
||||
return _issues.filter(function(d) { return d.severity === 'error'; });
|
||||
};
|
||||
|
||||
self.getIssuesForEntityWithID = function(entityID) {
|
||||
if (!context.hasEntity(entityID)) return [];
|
||||
var entity = context.entity(entityID);
|
||||
var key = osmEntity.key(entity);
|
||||
|
||||
if (!_issuesByEntityID[key]) {
|
||||
_issuesByEntityID[key] = validateEntity(entity);
|
||||
}
|
||||
return _issuesByEntityID[key];
|
||||
};
|
||||
|
||||
|
||||
function validateEntity(entity) {
|
||||
var _issues = [];
|
||||
var ran = {};
|
||||
|
||||
// runs validation and appends resulting issues, returning true if validation passed
|
||||
function runValidation(which) {
|
||||
if (ran[which]) return true;
|
||||
|
||||
var fn = validations[which];
|
||||
var typeIssues = fn(entity, context);
|
||||
_issues = _issues.concat(typeIssues);
|
||||
ran[which] = true; // mark this validation as having run
|
||||
return !typeIssues.length;
|
||||
}
|
||||
|
||||
if (entity.type === 'relation') {
|
||||
if (!runValidation('old_multipolygon')) {
|
||||
// don't flag missing tags if they are on the outer way
|
||||
ran.missing_tag = true;
|
||||
}
|
||||
}
|
||||
|
||||
// other validations require feature to be tagged
|
||||
if (!runValidation('missing_tag')) return _issues;
|
||||
|
||||
if (entity.type === 'way') {
|
||||
runValidation('crossing_ways');
|
||||
|
||||
// only check for disconnected way if no almost junctions
|
||||
if (runValidation('almost_junction')) {
|
||||
runValidation('disconnected_way');
|
||||
} else {
|
||||
ran.disconnected_way = true;
|
||||
}
|
||||
|
||||
runValidation('tag_suggests_area');
|
||||
}
|
||||
|
||||
// run all validations not yet run manually
|
||||
entityValidationIDs.forEach(runValidation);
|
||||
|
||||
return _issues;
|
||||
}
|
||||
|
||||
|
||||
self.validate = function() {
|
||||
_issuesByEntityID = {}; // clear cached
|
||||
_issues = [];
|
||||
|
||||
var history = context.history();
|
||||
var changes = history.changes();
|
||||
var entitiesToCheck = changes.created.concat(changes.modified);
|
||||
var graph = history.graph();
|
||||
|
||||
_issues = _flatten(_map(changesValidationIDs, function(ruleID) {
|
||||
var validation = validations[ruleID];
|
||||
return validation(changes, context);
|
||||
}));
|
||||
|
||||
entitiesToCheck = _uniq(_flattenDeep(_map(entitiesToCheck, function(entity) {
|
||||
var entities = [entity];
|
||||
if (entity.type === 'node') { // validate ways if their nodes have changed
|
||||
entities = entities.concat(graph.parentWays(entity));
|
||||
}
|
||||
entities = _map(entities, function(entity) {
|
||||
if (entity.type !== 'relation') { // validate relations if their geometries have changed
|
||||
return [entity].concat(graph.parentRelations(entity));
|
||||
}
|
||||
return entity;
|
||||
});
|
||||
return entities;
|
||||
})));
|
||||
|
||||
for (var entityIndex in entitiesToCheck) {
|
||||
var entity = entitiesToCheck[entityIndex];
|
||||
var entityIssues = validateEntity(entity);
|
||||
_issuesByEntityID[entity.id] = entityIssues;
|
||||
_issues = _issues.concat(entityIssues);
|
||||
}
|
||||
|
||||
_issues = _uniqWith(_issues, function(issue1, issue2) {
|
||||
return issue1.id() === issue2.id();
|
||||
});
|
||||
|
||||
dispatch.call('reload', self, _issues);
|
||||
};
|
||||
|
||||
return utilRebind(self, dispatch, 'on');
|
||||
}
|
||||
|
||||
|
||||
export function validationIssue(attrs) {
|
||||
this.type = attrs.type; // required
|
||||
this.severity = attrs.severity; // required - 'warning' or 'error'
|
||||
this.message = attrs.message; // required - localized string
|
||||
this.tooltip = attrs.tooltip; // required - localized string
|
||||
this.entities = attrs.entities; // optional - array of entities
|
||||
this.loc = attrs.loc; // optional - expect a [lon, lat] array
|
||||
this.info = attrs.info; // optional - object containing arbitrary extra information
|
||||
this.fixes = attrs.fixes; // optional - array of validationIssueFix objects
|
||||
this.hash = attrs.hash; // optional - string to further differentiate the issue
|
||||
|
||||
// A unique, deterministic string hash.
|
||||
// Issues with identical id values are considered identical.
|
||||
this.id = function() {
|
||||
var id = this.type;
|
||||
|
||||
if (this.hash) { // subclasses can pass in their own differentiator
|
||||
id += this.hash;
|
||||
}
|
||||
|
||||
// factor in the entities this issue is for
|
||||
// (sort them so the id is deterministic)
|
||||
var entityKeys = this.entities.map(osmEntity.key);
|
||||
id += entityKeys.sort().join();
|
||||
|
||||
// factor in loc since two separate issues can have an
|
||||
// idential type and entities, e.g. in crossing_ways
|
||||
if (this.loc) {
|
||||
id += this.loc.join();
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
|
||||
this.extent = function(resolver) {
|
||||
if (this.loc) {
|
||||
return geoExtent(this.loc);
|
||||
}
|
||||
if (this.entities && this.entities.length) {
|
||||
return this.entities.reduce(function(extent, entity) {
|
||||
return extent.extend(entity.extent(resolver));
|
||||
}, geoExtent());
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
if (this.fixes) { // add a reference in the fixes to the issue for use in fix actions
|
||||
for (var i = 0; i < this.fixes.length; i++) {
|
||||
this.fixes[i].issue = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function validationIssueFix(attrs) {
|
||||
this.icon = attrs.icon;
|
||||
this.title = attrs.title;
|
||||
this.onClick = attrs.onClick;
|
||||
this.entityIds = attrs.entityIds || []; // Used for hover-higlighting.
|
||||
this.issue = null; // the issue this fix is for
|
||||
}
|
||||
@@ -66,3 +66,23 @@ export function geoZoomToScale(z, tileSize) {
|
||||
return tileSize * Math.pow(2, z) / TAU;
|
||||
}
|
||||
|
||||
|
||||
// returns info about the node from `nodes` closest to the given `point`
|
||||
export function geoSphericalClosestNode(nodes, point) {
|
||||
var minDistance = Infinity, distance;
|
||||
var indexOfMin;
|
||||
|
||||
for (var i in nodes) {
|
||||
distance = geoSphericalDistance(nodes[i].loc, point);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
indexOfMin = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (indexOfMin !== undefined) {
|
||||
return { index: indexOfMin, distance: minDistance, node: nodes[indexOfMin] };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export { geoMetersToLon } from './geo.js';
|
||||
export { geoMetersToOffset } from './geo.js';
|
||||
export { geoOffsetToMeters } from './geo.js';
|
||||
export { geoScaleToZoom } from './geo.js';
|
||||
export { geoSphericalClosestNode } from './geo.js';
|
||||
export { geoSphericalDistance } from './geo.js';
|
||||
export { geoZoomToScale } from './geo.js';
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export * from './ui/settings/index';
|
||||
export * from './ui/index';
|
||||
export * from './util/index';
|
||||
export * from './validations/index';
|
||||
export { coreValidator } from './core/validator';
|
||||
|
||||
/* export some legacy symbols: */
|
||||
import { services } from './services/index';
|
||||
|
||||
@@ -220,8 +220,7 @@ export function modeDragNode(context) {
|
||||
}
|
||||
|
||||
context.replace(
|
||||
actionMoveNode(entity.id, loc),
|
||||
moveAnnotation(entity)
|
||||
actionMoveNode(entity.id, loc)
|
||||
);
|
||||
|
||||
// Below here: validations
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
|
||||
import { t } from '../util/locale';
|
||||
|
||||
import { actionMove } from '../actions';
|
||||
import {
|
||||
actionMove,
|
||||
actionNoop
|
||||
} from '../actions';
|
||||
import { behaviorEdit } from '../behavior';
|
||||
import { geoViewportEdge, geoVecSubtract } from '../geo';
|
||||
import { modeBrowse, modeSelect } from './index';
|
||||
@@ -63,7 +66,7 @@ export function modeMove(context, entityIDs, baseGraph) {
|
||||
var origMouse = context.projection(_origin);
|
||||
var delta = geoVecSubtract(geoVecSubtract(currMouse, origMouse), nudge);
|
||||
|
||||
fn(actionMove(entityIDs, delta, context.projection, _cache), annotation);
|
||||
fn(actionMove(entityIDs, delta, context.projection, _cache));
|
||||
_prevGraph = context.graph();
|
||||
}
|
||||
|
||||
@@ -98,6 +101,7 @@ export function modeMove(context, entityIDs, baseGraph) {
|
||||
|
||||
function finish() {
|
||||
d3_event.stopPropagation();
|
||||
context.replace(actionNoop(), annotation);
|
||||
context.enter(modeSelect(context, entityIDs));
|
||||
stopNudge();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from 'd3-polygon';
|
||||
|
||||
import { t } from '../util/locale';
|
||||
import { actionRotate } from '../actions';
|
||||
import {
|
||||
actionRotate,
|
||||
actionNoop
|
||||
} from '../actions';
|
||||
import { behaviorEdit } from '../behavior';
|
||||
import { geoVecInterp } from '../geo';
|
||||
import { modeBrowse, modeSelect } from './index';
|
||||
@@ -88,7 +91,7 @@ export function modeRotate(context, entityIDs) {
|
||||
if (typeof _prevAngle === 'undefined') _prevAngle = currAngle;
|
||||
var delta = currAngle - _prevAngle;
|
||||
|
||||
fn(actionRotate(entityIDs, _pivot, delta, projection), annotation);
|
||||
fn(actionRotate(entityIDs, _pivot, delta, projection));
|
||||
|
||||
_prevTransform = currTransform;
|
||||
_prevAngle = currAngle;
|
||||
@@ -98,6 +101,7 @@ export function modeRotate(context, entityIDs) {
|
||||
|
||||
function finish() {
|
||||
d3_event.stopPropagation();
|
||||
context.replace(actionNoop(), annotation);
|
||||
context.enter(modeSelect(context, entityIDs));
|
||||
}
|
||||
|
||||
|
||||
+11
-9
@@ -1,6 +1,6 @@
|
||||
import _clone from 'lodash-es/clone';
|
||||
import _keys from 'lodash-es/keys';
|
||||
import _toPairs from 'lodash-es/toPairs';
|
||||
import _every from 'lodash-es/every';
|
||||
import _union from 'lodash-es/union';
|
||||
import _without from 'lodash-es/without';
|
||||
|
||||
@@ -165,17 +165,19 @@ osmEntity.prototype = {
|
||||
},
|
||||
|
||||
deprecatedTags: function() {
|
||||
var tags = _toPairs(this.tags);
|
||||
var deprecated = {};
|
||||
var tags = this.tags;
|
||||
|
||||
// if there are no tags, none can be deprecated
|
||||
if (Object.keys(tags).length === 0) return [];
|
||||
|
||||
var deprecated = [];
|
||||
dataDeprecated.forEach(function(d) {
|
||||
var match = _toPairs(d.old)[0];
|
||||
tags.forEach(function(t) {
|
||||
if (t[0] === match[0] &&
|
||||
(t[1] === match[1] || match[1] === '*')) {
|
||||
deprecated[t[0]] = t[1];
|
||||
}
|
||||
var matchesDeprecatedTags = _every(Object.keys(d.old), function(key) {
|
||||
return tags[key] && (d.old[key] === tags[key] || d.old[key] === '*');
|
||||
});
|
||||
if (matchesDeprecatedTags) {
|
||||
deprecated.push(d);
|
||||
}
|
||||
});
|
||||
|
||||
return deprecated;
|
||||
|
||||
@@ -17,8 +17,9 @@ export {
|
||||
} from './lanes';
|
||||
|
||||
export {
|
||||
osmIsSimpleMultipolygonOuterMember,
|
||||
osmSimpleMultipolygonOuterMember,
|
||||
osmOldMultipolygonOuterMemberOfRelation,
|
||||
osmIsOldMultipolygonOuterMember,
|
||||
osmOldMultipolygonOuterMember,
|
||||
osmJoinWays
|
||||
} from './multipolygon';
|
||||
|
||||
|
||||
@@ -3,9 +3,44 @@ import { osmIsInterestingTag } from './tags';
|
||||
import { osmWay } from './way';
|
||||
|
||||
|
||||
// "Old" multipolyons, previously known as "simple" multipolygons, are as follows:
|
||||
//
|
||||
// 1. Relation tagged with `type=multipolygon` and no interesting tags.
|
||||
// 2. One and only one member with the `outer` role. Must be a way with interesting tags.
|
||||
// 3. No members without a role.
|
||||
//
|
||||
// Old multipolygons are no longer recommended but are still rendered as areas by iD.
|
||||
|
||||
export function osmOldMultipolygonOuterMemberOfRelation(entity, graph) {
|
||||
if (entity.type !== 'relation' ||
|
||||
!entity.isMultipolygon()
|
||||
|| Object.keys(entity.tags).filter(osmIsInterestingTag).length > 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var outerMember;
|
||||
for (var memberIndex in entity.members) {
|
||||
var member = entity.members[memberIndex];
|
||||
if (!member.role) return false;
|
||||
if (member.role === 'outer') {
|
||||
if (outerMember) return false;
|
||||
if (member.type !== 'way') return false;
|
||||
if (!graph.hasEntity(member.id)) return false;
|
||||
|
||||
outerMember = graph.entity(member.id);
|
||||
|
||||
if (Object.keys(outerMember.tags).filter(osmIsInterestingTag).length === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outerMember;
|
||||
}
|
||||
|
||||
// For fixing up rendering of multipolygons with tags on the outer member.
|
||||
// https://github.com/openstreetmap/iD/issues/613
|
||||
export function osmIsSimpleMultipolygonOuterMember(entity, graph) {
|
||||
export function osmIsOldMultipolygonOuterMember(entity, graph) {
|
||||
if (entity.type !== 'way' || Object.keys(entity.tags).filter(osmIsInterestingTag).length === 0)
|
||||
return false;
|
||||
|
||||
@@ -30,7 +65,7 @@ export function osmIsSimpleMultipolygonOuterMember(entity, graph) {
|
||||
}
|
||||
|
||||
|
||||
export function osmSimpleMultipolygonOuterMember(entity, graph) {
|
||||
export function osmOldMultipolygonOuterMember(entity, graph) {
|
||||
if (entity.type !== 'way')
|
||||
return false;
|
||||
|
||||
|
||||
+21
-10
@@ -193,8 +193,12 @@ _extend(osmWay.prototype, {
|
||||
return true;
|
||||
},
|
||||
|
||||
// returns an objects with the tag that implies this is an area, if any
|
||||
tagSuggestingArea: function() {
|
||||
|
||||
if (this.tags.area === 'yes') return { area: 'yes' };
|
||||
if (this.tags.area === 'no') return null;
|
||||
|
||||
isArea: function() {
|
||||
// `highway` and `railway` are typically linear features, but there
|
||||
// are a few exceptions that should be treated as areas, even in the
|
||||
// absence of a proper `area=yes` or `areaKeys` tag.. see #4194
|
||||
@@ -211,20 +215,27 @@ _extend(osmWay.prototype, {
|
||||
wash: true
|
||||
}
|
||||
};
|
||||
var returnTags = {};
|
||||
for (var key in this.tags) {
|
||||
if (key in areaKeys && !(this.tags[key] in areaKeys[key])) {
|
||||
returnTags[key] = this.tags[key];
|
||||
return returnTags;
|
||||
}
|
||||
if (key in lineKeys && this.tags[key] in lineKeys[key]) {
|
||||
returnTags[key] = this.tags[key];
|
||||
return returnTags;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
isArea: function() {
|
||||
|
||||
if (this.tags.area === 'yes')
|
||||
return true;
|
||||
if (!this.isClosed() || this.tags.area === 'no')
|
||||
return false;
|
||||
for (var key in this.tags) {
|
||||
if (key in areaKeys && !(this.tags[key] in areaKeys[key])) {
|
||||
return true;
|
||||
}
|
||||
if (key in lineKeys && this.tags[key] in lineKeys[key]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return this.tagSuggestingArea() !== null;
|
||||
},
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import _reduce from 'lodash-es/reduce';
|
||||
import _every from 'lodash-es/every';
|
||||
import { areaKeys } from '../core/context';
|
||||
|
||||
import {
|
||||
validationIssue
|
||||
} from '../core/validator';
|
||||
|
||||
var buildRuleChecks = function() {
|
||||
return {
|
||||
@@ -212,14 +215,17 @@ export default {
|
||||
}
|
||||
},
|
||||
// when geometries match and tag matches are present, return a warning...
|
||||
findWarnings: function (entity, graph, warnings) {
|
||||
findIssues: function (entity, graph, issues) {
|
||||
if (this.geometryMatches(entity, graph) && this.matches(entity)) {
|
||||
var type = Object.keys(selector).indexOf('error') > -1 ? 'error' : 'warning';
|
||||
warnings.push({
|
||||
severity: type,
|
||||
message: selector[type],
|
||||
entity: entity
|
||||
});
|
||||
var severity = Object.keys(selector).indexOf('error') > -1
|
||||
? 'error'
|
||||
: 'warning';
|
||||
issues.push(new validationIssue({
|
||||
type: 'maprules',
|
||||
severity: severity,
|
||||
message: selector[severity],
|
||||
entities: [entity],
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import _values from 'lodash-es/values';
|
||||
|
||||
import { bisector as d3_bisector } from 'd3-array';
|
||||
|
||||
import { osmEntity, osmIsSimpleMultipolygonOuterMember } from '../osm';
|
||||
import { osmEntity, osmIsOldMultipolygonOuterMember } from '../osm';
|
||||
import { svgPath, svgSegmentWay, svgTagClasses } from './index';
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ export function svgAreas(projection, context) {
|
||||
var entity = entities[i];
|
||||
if (entity.geometry(graph) !== 'area') continue;
|
||||
|
||||
multipolygon = osmIsSimpleMultipolygonOuterMember(entity, graph);
|
||||
multipolygon = osmIsOldMultipolygonOuterMember(entity, graph);
|
||||
if (multipolygon) {
|
||||
areas[multipolygon.id] = {
|
||||
entity: multipolygon.mergeTags(entity.tags),
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
export function svgIcon(name, svgklass, useklass) {
|
||||
return function drawIcon(selection) {
|
||||
selection.selectAll('svg')
|
||||
selection.selectAll('svg.icon')
|
||||
.data([0])
|
||||
.enter()
|
||||
.append('svg')
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
svgTagClasses
|
||||
} from './index';
|
||||
|
||||
import { osmEntity, osmSimpleMultipolygonOuterMember } from '../osm';
|
||||
import { osmEntity, osmOldMultipolygonOuterMember } from '../osm';
|
||||
import { utilDetect } from '../util/detect';
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ export function svgLines(projection, context) {
|
||||
|
||||
for (var i = 0; i < entities.length; i++) {
|
||||
var entity = entities[i];
|
||||
var outer = osmSimpleMultipolygonOuterMember(entity, graph);
|
||||
var outer = osmOldMultipolygonOuterMember(entity, graph);
|
||||
if (outer) {
|
||||
ways.push(entity.mergeTags(outer.tags));
|
||||
oldMultiPolygonOuters[outer.id] = true;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { uiBackgroundOffset } from './background_offset';
|
||||
import { uiCmd } from './cmd';
|
||||
import { uiDisclosure } from './disclosure';
|
||||
import { uiHelp } from './help';
|
||||
import { uiIssues } from './issues';
|
||||
import { uiMapData } from './map_data';
|
||||
import { uiMapInMap } from './map_in_map';
|
||||
import { uiSettingsCustomBackground } from './settings/custom_background';
|
||||
@@ -80,7 +81,7 @@ export function uiBackground(context) {
|
||||
return context.background().showsLayer(d);
|
||||
}
|
||||
|
||||
selection.selectAll('.layer')
|
||||
selection.selectAll('li')
|
||||
.classed('active', active)
|
||||
.classed('switch', function(d) { return d === _previousBackground; })
|
||||
.call(setTooltips)
|
||||
@@ -135,7 +136,7 @@ export function uiBackground(context) {
|
||||
.sources(context.map().extent())
|
||||
.filter(filter);
|
||||
|
||||
var layerLinks = layerList.selectAll('li.layer')
|
||||
var layerLinks = layerList.selectAll('li')
|
||||
.data(sources, function(d) { return d.name(); });
|
||||
|
||||
layerLinks.exit()
|
||||
@@ -143,7 +144,6 @@ export function uiBackground(context) {
|
||||
|
||||
var enter = layerLinks.enter()
|
||||
.append('li')
|
||||
.attr('class', 'layer')
|
||||
.classed('layer-custom', function(d) { return d.id === 'custom'; })
|
||||
.classed('best', function(d) { return d.best(); });
|
||||
|
||||
@@ -181,9 +181,9 @@ export function uiBackground(context) {
|
||||
.text(function(d) { return d.name(); });
|
||||
|
||||
|
||||
layerList.selectAll('li.layer')
|
||||
layerList.selectAll('li')
|
||||
.sort(sortSources)
|
||||
.style('display', layerList.selectAll('li.layer').data().length > 0 ? 'block' : 'none');
|
||||
.style('display', layerList.selectAll('li').data().length > 0 ? 'block' : 'none');
|
||||
|
||||
layerList
|
||||
.call(updateLayerSelections);
|
||||
@@ -217,7 +217,7 @@ export function uiBackground(context) {
|
||||
.append('ul')
|
||||
.attr('class', 'layer-list minimap-toggle-list')
|
||||
.append('li')
|
||||
.attr('class', 'layer minimap-toggle-item');
|
||||
.attr('class', 'minimap-toggle-item');
|
||||
|
||||
var minimapLabelEnter = minimapEnter
|
||||
.append('label')
|
||||
@@ -329,8 +329,9 @@ export function uiBackground(context) {
|
||||
_shown = show;
|
||||
|
||||
if (show) {
|
||||
uiMapData.hidePane();
|
||||
uiHelp.hidePane();
|
||||
uiIssues.hidePane();
|
||||
uiMapData.hidePane();
|
||||
update();
|
||||
|
||||
pane
|
||||
|
||||
+36
-10
@@ -14,6 +14,7 @@ import { uiCommitChanges } from './commit_changes';
|
||||
import { uiCommitWarnings } from './commit_warnings';
|
||||
import { uiRawTagEditor } from './raw_tag_editor';
|
||||
import { utilDetect } from '../util/detect';
|
||||
import { tooltip } from '../util/tooltip';
|
||||
import { utilRebind } from '../util';
|
||||
import { modeBrowse } from '../modes';
|
||||
import { svgIcon } from '../svg';
|
||||
@@ -264,13 +265,16 @@ export function uiCommit(context) {
|
||||
.attr('class', 'label')
|
||||
.text(t('commit.cancel'));
|
||||
|
||||
buttonEnter
|
||||
var uploadButton = buttonEnter
|
||||
.append('button')
|
||||
.attr('class', 'action button save-button')
|
||||
.append('span')
|
||||
.attr('class', 'action button save-button');
|
||||
|
||||
uploadButton.append('span')
|
||||
.attr('class', 'label')
|
||||
.text(t('commit.save'));
|
||||
|
||||
var uploadBlockerTooltipText = getUploadBlockerMessage();
|
||||
|
||||
// update
|
||||
buttonSection = buttonSection
|
||||
.merge(buttonEnter);
|
||||
@@ -282,15 +286,21 @@ export function uiCommit(context) {
|
||||
});
|
||||
|
||||
buttonSection.selectAll('.save-button')
|
||||
.attr('disabled', function() {
|
||||
var n = d3_select('#preset-input-comment').node();
|
||||
return (n && n.value.length) ? null : true;
|
||||
})
|
||||
.classed('disabled', uploadBlockerTooltipText !== null)
|
||||
.on('click.save', function() {
|
||||
this.blur(); // avoid keeping focus on the button - #4641
|
||||
dispatch.call('save', this, _changeset);
|
||||
if (!d3_select(this).classed('disabled')) {
|
||||
this.blur(); // avoid keeping focus on the button - #4641
|
||||
dispatch.call('save', this, _changeset);
|
||||
}
|
||||
});
|
||||
|
||||
// remove any existing tooltip
|
||||
tooltip().destroyAny(buttonSection.selectAll('.save-button'));
|
||||
|
||||
if (uploadBlockerTooltipText) {
|
||||
buttonSection.selectAll('.save-button')
|
||||
.call(tooltip().title(uploadBlockerTooltipText).placement('top'));
|
||||
}
|
||||
|
||||
// Raw Tag Editor
|
||||
var tagSection = body.selectAll('.tag-section.raw-tag-editor')
|
||||
@@ -329,6 +339,22 @@ export function uiCommit(context) {
|
||||
}
|
||||
|
||||
|
||||
function getUploadBlockerMessage() {
|
||||
var errorCount = context.validator().getErrors().length;
|
||||
if (errorCount > 0) {
|
||||
return t('commit.outstanding_errors_message', { count: errorCount });
|
||||
|
||||
} else {
|
||||
var n = d3_select('#preset-input-comment').node();
|
||||
var hasChangesetComment = n && n.value.length > 0;
|
||||
if (!hasChangesetComment) {
|
||||
return t('commit.comment_needed_message');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function changeTags(changed, onInput) {
|
||||
if (changed.hasOwnProperty('comment')) {
|
||||
if (changed.comment === undefined) {
|
||||
@@ -467,4 +493,4 @@ export function uiCommit(context) {
|
||||
|
||||
|
||||
return utilRebind(commit, dispatch, 'on');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import _map from 'lodash-es/map';
|
||||
|
||||
import { t } from '../util/locale';
|
||||
import { modeSelect } from '../modes';
|
||||
import { svgIcon } from '../svg';
|
||||
@@ -11,26 +13,24 @@ export function uiCommitWarnings(context) {
|
||||
|
||||
function commitWarnings(selection) {
|
||||
|
||||
var changes = context.history().changes();
|
||||
var validations = context.history().validate(changes);
|
||||
var issues = context.validator().getIssues();
|
||||
|
||||
validations = _reduce(validations, function(validations, val) {
|
||||
issues = _reduce(issues, function(issues, val) {
|
||||
var severity = val.severity;
|
||||
if (validations.hasOwnProperty(severity)) {
|
||||
validations[severity].push(val);
|
||||
if (issues.hasOwnProperty(severity)) {
|
||||
issues[severity].push(val);
|
||||
} else {
|
||||
validations[severity] = [val];
|
||||
issues[severity] = [val];
|
||||
}
|
||||
return validations;
|
||||
return issues;
|
||||
}, {});
|
||||
|
||||
_forEach(validations, function(instances, type) {
|
||||
_forEach(issues, function(instances, severity) {
|
||||
instances = _uniqBy(instances, function(val) {
|
||||
return val.entity || (val.id + '_' + val.message.replace(/\s+/g,''));
|
||||
});
|
||||
|
||||
var section = type + '-section';
|
||||
var instanceItem = type + '-item';
|
||||
var section = severity + '-section';
|
||||
var instanceItem = severity + '-item';
|
||||
|
||||
var container = selection.selectAll('.' + section)
|
||||
.data(instances.length ? [0] : []);
|
||||
@@ -44,7 +44,7 @@ export function uiCommitWarnings(context) {
|
||||
|
||||
containerEnter
|
||||
.append('h3')
|
||||
.text(type === 'warning' ? t('commit.warnings') : t('commit.errors'));
|
||||
.text(severity === 'warning' ? t('commit.warnings') : t('commit.errors'));
|
||||
|
||||
containerEnter
|
||||
.append('ul')
|
||||
@@ -80,31 +80,35 @@ export function uiCommitWarnings(context) {
|
||||
items = itemsEnter
|
||||
.merge(items);
|
||||
|
||||
|
||||
items
|
||||
.on('mouseover', mouseover)
|
||||
.on('mouseout', mouseout)
|
||||
.on('click', warningClick);
|
||||
|
||||
|
||||
function mouseover(d) {
|
||||
if (d.entity) {
|
||||
if (d.entities) {
|
||||
context.surface().selectAll(
|
||||
utilEntityOrMemberSelector([d.entity.id], context.graph())
|
||||
utilEntityOrMemberSelector(
|
||||
_map(d.entities, function(e) { return e.id; }),
|
||||
context.graph()
|
||||
)
|
||||
).classed('hover', true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function mouseout() {
|
||||
context.surface().selectAll('.hover')
|
||||
.classed('hover', false);
|
||||
}
|
||||
|
||||
|
||||
function warningClick(d) {
|
||||
if (d.entity) {
|
||||
context.map().zoomTo(d.entity);
|
||||
context.enter(modeSelect(context, [d.entity.id]));
|
||||
if (d.entities && d.entities.length > 0) {
|
||||
context.map().zoomTo(d.entities[0]);
|
||||
context.enter(modeSelect(
|
||||
context,
|
||||
_map(d.entities, function(e) { return e.id; })
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,13 +14,14 @@ import { tooltip } from '../util/tooltip';
|
||||
import { actionChangeTags } from '../actions';
|
||||
import { modeBrowse } from '../modes';
|
||||
import { svgIcon } from '../svg';
|
||||
import { uiPresetEditor } from './preset_editor';
|
||||
import { uiPresetIcon } from './preset_icon';
|
||||
import { uiQuickLinks } from './quick_links';
|
||||
import { uiRawMemberEditor } from './raw_member_editor';
|
||||
import { uiRawMembershipEditor } from './raw_membership_editor';
|
||||
import { uiRawTagEditor } from './raw_tag_editor';
|
||||
import { uiTagReference } from './tag_reference';
|
||||
import { uiPresetEditor } from './preset_editor';
|
||||
import { uiEntityIssues } from './entity_issues';
|
||||
import { uiTooltipHtml } from './tooltipHtml';
|
||||
import { utilCleanTags, utilRebind } from '../util';
|
||||
|
||||
@@ -35,6 +36,7 @@ export function uiEntityEditor(context) {
|
||||
var _activePreset;
|
||||
var _tagReference;
|
||||
|
||||
var entityIssues = uiEntityIssues(context);
|
||||
var quickLinks = uiQuickLinks();
|
||||
var presetEditor = uiPresetEditor(context).on('change', changeTags);
|
||||
var rawTagEditor = uiRawTagEditor(context).on('change', changeTags);
|
||||
@@ -105,6 +107,10 @@ export function uiEntityEditor(context) {
|
||||
.append('div')
|
||||
.attr('class', 'preset-quick-links');
|
||||
|
||||
bodyEnter
|
||||
.append('div')
|
||||
.attr('class', 'entity-issues');
|
||||
|
||||
bodyEnter
|
||||
.append('div')
|
||||
.attr('class', 'preset-editor');
|
||||
@@ -165,7 +171,6 @@ export function uiEntityEditor(context) {
|
||||
.attr('class', 'namepart')
|
||||
.text(function(d) { return d; });
|
||||
|
||||
|
||||
// update quick links
|
||||
var choices = [{
|
||||
id: 'zoom_to',
|
||||
@@ -183,6 +188,11 @@ export function uiEntityEditor(context) {
|
||||
|
||||
|
||||
// update editor sections
|
||||
body.select('.entity-issues')
|
||||
.call(entityIssues
|
||||
.entityID(_entityID)
|
||||
);
|
||||
|
||||
body.select('.preset-editor')
|
||||
.call(presetEditor
|
||||
.preset(_activePreset)
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
|
||||
import { svgIcon } from '../svg';
|
||||
import { t } from '../util/locale';
|
||||
import { tooltip } from '../util/tooltip';
|
||||
import { uiDisclosure } from './disclosure';
|
||||
import { uiTooltipHtml } from './tooltipHtml';
|
||||
import { utilHighlightEntities } from '../util';
|
||||
|
||||
|
||||
export function uiEntityIssues(context) {
|
||||
var _selection = d3_select(null);
|
||||
var _expandedIssueID;
|
||||
var _entityID;
|
||||
|
||||
// Listen for validation reload even though the entity editor is reloaded on
|
||||
// every graph change since the graph change event may happen before the issue
|
||||
// cache is refreshed
|
||||
context.validator().on('reload.entity_issues', function() {
|
||||
|
||||
_selection.selectAll('.disclosure-wrap-entity_issues')
|
||||
.call(render);
|
||||
|
||||
update();
|
||||
});
|
||||
|
||||
|
||||
function entityIssues(selection) {
|
||||
_selection = selection;
|
||||
|
||||
selection
|
||||
.call(uiDisclosure(context, 'entity_issues', true)
|
||||
.content(render)
|
||||
);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
|
||||
function update() {
|
||||
var issues = context.validator().getIssuesForEntityWithID(_entityID);
|
||||
|
||||
_selection
|
||||
.classed('hide', issues.length === 0);
|
||||
|
||||
_selection.selectAll('.hide-toggle-entity_issues span')
|
||||
.text(t('issues.list_title', { count: issues.length }));
|
||||
}
|
||||
|
||||
|
||||
function render(selection) {
|
||||
var issues = context.validator().getIssuesForEntityWithID(_entityID);
|
||||
_expandedIssueID = issues.length > 0 ? issues[0].id() : null;
|
||||
|
||||
var items = selection.selectAll('.issue')
|
||||
.data(issues, function(d) { return d.id(); });
|
||||
|
||||
// Exit
|
||||
items.exit()
|
||||
.remove();
|
||||
|
||||
// Enter
|
||||
var itemsEnter = items.enter()
|
||||
.append('div')
|
||||
.attr('class', function(d) { return 'issue severity-' + d.severity; })
|
||||
.call(tooltip()
|
||||
.html(true)
|
||||
.title(function(d) { return uiTooltipHtml(d.tooltip); })
|
||||
.placement('top')
|
||||
)
|
||||
.on('mouseover.highlight', function(d) {
|
||||
// don't hover-highlight the selected entity
|
||||
var ids = d.entities.filter(function(e) { return e.id !== _entityID; })
|
||||
.map(function(e) { return e.id; });
|
||||
utilHighlightEntities(ids, true, context);
|
||||
})
|
||||
.on('mouseout.highlight', function(d) {
|
||||
var ids = d.entities.filter(function(e) { return e.id !== _entityID; })
|
||||
.map(function(e) { return e.id; });
|
||||
utilHighlightEntities(ids, false, context);
|
||||
});
|
||||
|
||||
var messagesEnter = itemsEnter
|
||||
.append('button')
|
||||
.attr('class', 'message')
|
||||
.on('click', function(d) {
|
||||
|
||||
_expandedIssueID = d.id(); // expand only the clicked item
|
||||
selection.selectAll('.issue')
|
||||
.classed('expanded', function(d) { return d.id() === _expandedIssueID; });
|
||||
|
||||
var extent = d.extent(context.graph());
|
||||
if (extent) {
|
||||
var view = context.map().trimmedExtent();
|
||||
var zoom = context.map().zoom();
|
||||
if (!view.contains(extent) || zoom < 19) {
|
||||
context.map().centerZoomEase(extent.center(), Math.max(zoom, 19));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
messagesEnter
|
||||
.append('span')
|
||||
.attr('class', 'issue-icon')
|
||||
.call(svgIcon('', 'pre-text'));
|
||||
|
||||
messagesEnter
|
||||
.append('strong')
|
||||
.attr('class', 'issue-text');
|
||||
|
||||
itemsEnter
|
||||
.append('ul')
|
||||
.attr('class', 'issue-fix-list');
|
||||
|
||||
|
||||
// Update
|
||||
items = items
|
||||
.merge(itemsEnter)
|
||||
.classed('expanded', function(d) { return d.id() === _expandedIssueID; });
|
||||
|
||||
items.select('.issue-icon svg use') // propagate bound data
|
||||
.attr('href', function(d) {
|
||||
return '#iD-icon-' + (d.severity === 'warning' ? 'alert' : 'error');
|
||||
});
|
||||
|
||||
items.select('.issue-text') // propagate bound data
|
||||
.text(function(d) { return d.message; });
|
||||
|
||||
|
||||
// fixes
|
||||
var fixLists = items.selectAll('.issue-fix-list');
|
||||
|
||||
var fixes = fixLists.selectAll('.issue-fix-item')
|
||||
.data(function(d) { return d.fixes; })
|
||||
.enter()
|
||||
.append('li')
|
||||
.attr('class', function(d) {
|
||||
return 'issue-fix-item ' + (d.onClick ? 'actionable' : '');
|
||||
})
|
||||
.append('button')
|
||||
.on('click', function(d) {
|
||||
if (d.onClick) {
|
||||
utilHighlightEntities(d.entityIds, false, context);
|
||||
d.onClick();
|
||||
}
|
||||
})
|
||||
.on('mouseover.highlight', function(d) {
|
||||
utilHighlightEntities(d.entityIds, true, context);
|
||||
})
|
||||
.on('mouseout.highlight', function(d) {
|
||||
utilHighlightEntities(d.entityIds, false, context);
|
||||
});
|
||||
|
||||
fixes.append('span')
|
||||
.attr('class', 'fix-icon')
|
||||
.each(function(d) {
|
||||
var iconName = d.icon || 'iD-icon-wrench';
|
||||
if (iconName.startsWith('maki')) {
|
||||
iconName += '-15';
|
||||
}
|
||||
d3_select(this).call(svgIcon('#' + iconName, 'pre-text'));
|
||||
});
|
||||
|
||||
fixes.append('span')
|
||||
.text(function(d) { return d.title; });
|
||||
}
|
||||
|
||||
|
||||
entityIssues.entityID = function(val) {
|
||||
if (!arguments.length) return _entityID;
|
||||
if (_entityID !== val) {
|
||||
_entityID = val;
|
||||
_expandedIssueID = null;
|
||||
}
|
||||
return entityIssues;
|
||||
};
|
||||
|
||||
|
||||
return entityIssues;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { uiCmd } from './cmd';
|
||||
import { uiBackground } from './background';
|
||||
import { uiIntro } from './intro';
|
||||
import { uiMapData } from './map_data';
|
||||
import { uiIssues } from './issues';
|
||||
import { uiShortcuts } from './shortcuts';
|
||||
import { uiTooltipHtml } from './tooltipHtml';
|
||||
|
||||
@@ -303,6 +304,7 @@ export function uiHelp(context) {
|
||||
|
||||
if (show) {
|
||||
uiBackground.hidePane();
|
||||
uiIssues.hidePane();
|
||||
uiMapData.hidePane();
|
||||
|
||||
pane.style('display', 'block')
|
||||
|
||||
@@ -21,6 +21,7 @@ import { uiGeolocate } from './geolocate';
|
||||
import { uiHelp } from './help';
|
||||
import { uiInfo } from './info';
|
||||
import { uiIntro } from './intro';
|
||||
import { uiIssues } from './issues';
|
||||
import { uiLoading } from './loading';
|
||||
import { uiMapData } from './map_data';
|
||||
import { uiMapInMap } from './map_in_map';
|
||||
@@ -165,6 +166,11 @@ export function uiInit(context) {
|
||||
.attr('class', 'map-control map-data-control')
|
||||
.call(uiMapData(context));
|
||||
|
||||
controls
|
||||
.append('div')
|
||||
.attr('class', 'map-control map-issues-control')
|
||||
.call(uiIssues(context));
|
||||
|
||||
controls
|
||||
.append('div')
|
||||
.attr('class', 'map-control help-control')
|
||||
|
||||
+22
-13
@@ -1,5 +1,5 @@
|
||||
import { interpolate as d3_interpolate } from 'd3-interpolate';
|
||||
import { selectAll as d3_selectAll } from 'd3-selection';
|
||||
import { select as d3_select, selectAll as d3_selectAll } from 'd3-selection';
|
||||
|
||||
import { uiEntityEditor } from './entity_editor';
|
||||
import { uiPresetList } from './preset_list';
|
||||
@@ -9,6 +9,9 @@ import { uiViewOnOSM } from './view_on_osm';
|
||||
export function uiInspector(context) {
|
||||
var presetList = uiPresetList(context);
|
||||
var entityEditor = uiEntityEditor(context);
|
||||
var wrap = d3_select(null),
|
||||
presetPane = d3_select(null),
|
||||
editorPane = d3_select(null);
|
||||
var _state = 'select';
|
||||
var _entityID;
|
||||
var _newFeature = false;
|
||||
@@ -18,14 +21,14 @@ export function uiInspector(context) {
|
||||
presetList
|
||||
.entityID(_entityID)
|
||||
.autofocus(_newFeature)
|
||||
.on('choose', setPreset);
|
||||
.on('choose', inspector.setPreset);
|
||||
|
||||
entityEditor
|
||||
.state(_state)
|
||||
.entityID(_entityID)
|
||||
.on('choose', showList);
|
||||
.on('choose', inspector.showList);
|
||||
|
||||
var wrap = selection.selectAll('.panewrap')
|
||||
wrap = selection.selectAll('.panewrap')
|
||||
.data([0]);
|
||||
|
||||
var enter = wrap.enter()
|
||||
@@ -41,8 +44,8 @@ export function uiInspector(context) {
|
||||
.attr('class', 'entity-editor-pane pane');
|
||||
|
||||
wrap = wrap.merge(enter);
|
||||
var presetPane = wrap.selectAll('.preset-list-pane');
|
||||
var editorPane = wrap.selectAll('.entity-editor-pane');
|
||||
presetPane = wrap.selectAll('.preset-list-pane');
|
||||
editorPane = wrap.selectAll('.entity-editor-pane');
|
||||
|
||||
var entity = context.entity(_entityID);
|
||||
|
||||
@@ -71,26 +74,32 @@ export function uiInspector(context) {
|
||||
.call(uiViewOnOSM(context)
|
||||
.what(context.hasEntity(_entityID))
|
||||
);
|
||||
}
|
||||
|
||||
inspector.showList = function(preset) {
|
||||
wrap.transition()
|
||||
.styleTween('right', function() { return d3_interpolate('0%', '-100%'); });
|
||||
|
||||
function showList(preset) {
|
||||
wrap.transition()
|
||||
.styleTween('right', function() { return d3_interpolate('0%', '-100%'); });
|
||||
presetPane
|
||||
.call(presetList.preset(preset).autofocus(true));
|
||||
};
|
||||
|
||||
inspector.setPreset = function(preset) {
|
||||
|
||||
// upon setting multipolygon, go to the area preset list instead of the editor
|
||||
if (preset.id === 'type/multipolygon') {
|
||||
presetPane
|
||||
.call(presetList.preset(preset).autofocus(true));
|
||||
}
|
||||
|
||||
|
||||
function setPreset(preset) {
|
||||
} else {
|
||||
wrap.transition()
|
||||
.styleTween('right', function() { return d3_interpolate('-100%', '0%'); });
|
||||
|
||||
editorPane
|
||||
.call(entityEditor.preset(preset));
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
inspector.state = function(val) {
|
||||
if (!arguments.length) return _state;
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
import {
|
||||
event as d3_event,
|
||||
select as d3_select
|
||||
} from 'd3-selection';
|
||||
|
||||
import { svgIcon } from '../svg';
|
||||
import { t, textDirection } from '../util/locale';
|
||||
import { tooltip } from '../util/tooltip';
|
||||
import { modeSelect } from '../modes';
|
||||
import { uiBackground } from './background';
|
||||
import { uiDisclosure } from './disclosure';
|
||||
import { uiHelp } from './help';
|
||||
import { uiMapData } from './map_data';
|
||||
import { uiTooltipHtml } from './tooltipHtml';
|
||||
import { utilHighlightEntities } from '../util';
|
||||
|
||||
|
||||
export function uiIssues(context) {
|
||||
var key = t('issues.key');
|
||||
//var _featureApplicabilityList = d3_select(null);
|
||||
var _errorsList = d3_select(null);
|
||||
var _warningsList = d3_select(null);
|
||||
var _pane = d3_select(null);
|
||||
var _button = d3_select(null);
|
||||
var _shown = false;
|
||||
|
||||
context.validator().on('reload.issues_pane', update);
|
||||
|
||||
/*function renderIssuesOptions(selection) {
|
||||
var container = selection.selectAll('.issues-options-container')
|
||||
.data([0]);
|
||||
|
||||
container = container.enter()
|
||||
.append('div')
|
||||
.attr('class', 'issues-options-container')
|
||||
.merge(container);
|
||||
|
||||
_featureApplicabilityList = container.selectAll('.feature-applicability-list')
|
||||
.data([0]);
|
||||
|
||||
_featureApplicabilityList = _featureApplicabilityList.enter()
|
||||
.append('ul')
|
||||
.attr('class', 'layer-list feature-applicability-list')
|
||||
.merge(_featureApplicabilityList);
|
||||
|
||||
updateFeatureApplicabilityList();
|
||||
}*/
|
||||
|
||||
function addIconBadge(selection) {
|
||||
var d = 10;
|
||||
selection.selectAll('svg.icon-badge')
|
||||
.data([0])
|
||||
.enter()
|
||||
.append('svg')
|
||||
.attr('viewbox', '0 0 ' + d + ' ' + d)
|
||||
.attr('class', 'icon-badge')
|
||||
.append('circle')
|
||||
.attr('cx', d / 2)
|
||||
.attr('cy', d / 2)
|
||||
.attr('r', (d / 2) - 1)
|
||||
.attr('fill', 'currentColor');
|
||||
}
|
||||
|
||||
function renderErrorsList(selection) {
|
||||
_errorsList = selection.selectAll('.errors-list')
|
||||
.data([0]);
|
||||
|
||||
_errorsList = _errorsList.enter()
|
||||
.append('ul')
|
||||
.attr('class', 'layer-list errors-list issues-list')
|
||||
.merge(_errorsList);
|
||||
|
||||
updateErrorsList();
|
||||
}
|
||||
|
||||
function renderWarningsList(selection) {
|
||||
_warningsList = selection.selectAll('.warnings-list')
|
||||
.data([0]);
|
||||
|
||||
_warningsList = _warningsList.enter()
|
||||
.append('ul')
|
||||
.attr('class', 'layer-list warnings-list issues-list')
|
||||
.merge(_warningsList);
|
||||
|
||||
updateWarningsList();
|
||||
}
|
||||
|
||||
|
||||
function drawIssuesList(selection, issues) {
|
||||
var items = selection.selectAll('li')
|
||||
.data(issues, function(d) { return d.id(); });
|
||||
|
||||
// Exit
|
||||
items.exit()
|
||||
.remove();
|
||||
|
||||
// Enter
|
||||
var itemsEnter = items.enter()
|
||||
.append('li')
|
||||
.attr('class', function (d) { return 'issue severity-' + d.severity; })
|
||||
.on('click', function(d) {
|
||||
var extent = d.extent(context.graph());
|
||||
if (extent) {
|
||||
var msec = 0;
|
||||
var view = context.map().trimmedExtent();
|
||||
var zoom = context.map().zoom();
|
||||
|
||||
// make sure user can see the issue
|
||||
if (!view.contains(extent) || zoom < 19) {
|
||||
msec = 250;
|
||||
context.map().centerZoomEase(extent.center(), Math.max(zoom, 19), msec);
|
||||
}
|
||||
|
||||
// select the first entity
|
||||
if (d.entities && d.entities.length) {
|
||||
window.setTimeout(function() {
|
||||
var ids = d.entities.map(function(e) { return e.id; });
|
||||
context.enter(modeSelect(context, [ids[0]]));
|
||||
utilHighlightEntities(ids, true, context);
|
||||
}, msec);
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('mouseover', function(d) {
|
||||
var ids = d.entities.map(function(e) { return e.id; });
|
||||
utilHighlightEntities(ids, true, context);
|
||||
})
|
||||
.on('mouseout', function(d) {
|
||||
var ids = d.entities.map(function(e) { return e.id; });
|
||||
utilHighlightEntities(ids, false, context);
|
||||
});
|
||||
|
||||
|
||||
var messagesEnter = itemsEnter
|
||||
.append('button')
|
||||
.attr('class', 'message');
|
||||
|
||||
messagesEnter
|
||||
.call(tooltip()
|
||||
.html(true)
|
||||
.title(function(d) { return uiTooltipHtml(d.tooltip); })
|
||||
.placement('top')
|
||||
);
|
||||
|
||||
messagesEnter
|
||||
.append('span')
|
||||
.attr('class', 'issue-icon')
|
||||
.call(svgIcon('', 'pre-text'));
|
||||
|
||||
messagesEnter
|
||||
.append('span')
|
||||
.attr('class', 'issue-text');
|
||||
|
||||
|
||||
// Update
|
||||
items = items
|
||||
.merge(itemsEnter);
|
||||
|
||||
items.select('.issue-icon svg use') // propagate bound data
|
||||
.attr('href', function(d) {
|
||||
return '#iD-icon-' + (d.severity === 'warning' ? 'alert' : 'error');
|
||||
});
|
||||
|
||||
items.select('.issue-text') // propagate bound data
|
||||
.text(function(d) { return d.message; });
|
||||
}
|
||||
|
||||
|
||||
function renderNoIssuesBox(selection) {
|
||||
selection
|
||||
.append('div')
|
||||
.call(svgIcon('#iD-icon-apply', 'pre-text'));
|
||||
|
||||
var noIssuesMessage = selection
|
||||
.append('span');
|
||||
|
||||
noIssuesMessage
|
||||
.append('strong')
|
||||
.text(t('issues.no_issues.message'));
|
||||
|
||||
noIssuesMessage
|
||||
.append('br');
|
||||
|
||||
noIssuesMessage
|
||||
.append('span')
|
||||
.text(t('issues.no_issues.info'));
|
||||
}
|
||||
|
||||
/*
|
||||
function showsFeatureApplicability(d) {
|
||||
return context.validator().getFeatureApplicability() === d;
|
||||
}
|
||||
|
||||
function setFeatureApplicability(d) {
|
||||
context.validator().setFeatureApplicability(d);
|
||||
update();
|
||||
}
|
||||
|
||||
function updateFeatureApplicabilityList() {
|
||||
_featureApplicabilityList
|
||||
.call(
|
||||
drawListItems,
|
||||
context.validator().featureApplicabilityOptions,
|
||||
'radio',
|
||||
'features_to_validate',
|
||||
setFeatureApplicability,
|
||||
showsFeatureApplicability
|
||||
);
|
||||
}*/
|
||||
|
||||
function updateErrorsList() {
|
||||
var errors = context.validator().getErrors();
|
||||
_errorsList
|
||||
.call(drawIssuesList, errors);
|
||||
}
|
||||
|
||||
|
||||
function updateWarningsList() {
|
||||
var warnings = context.validator().getWarnings();
|
||||
_warningsList
|
||||
.call(drawIssuesList, warnings);
|
||||
}
|
||||
|
||||
|
||||
function update() {
|
||||
var errors = context.validator().getErrors();
|
||||
var warnings = context.validator().getWarnings();
|
||||
|
||||
_button.selectAll('.icon-badge')
|
||||
.classed('error', (errors.length > 0))
|
||||
.classed('warning', (errors.length === 0 && warnings.length > 0))
|
||||
.classed('hide', (errors.length === 0 && warnings.length === 0));
|
||||
|
||||
_pane.select('.issues-errors')
|
||||
.classed('hide', errors.length === 0);
|
||||
|
||||
if (errors.length > 0) {
|
||||
_pane.select('.hide-toggle-issues_errors .hide-toggle-text')
|
||||
.text(t('issues.errors.list_title', { count: errors.length }));
|
||||
if (!_pane.select('.disclosure-wrap-issues_errors').classed('hide')) {
|
||||
updateErrorsList();
|
||||
}
|
||||
}
|
||||
|
||||
_pane.select('.issues-warnings')
|
||||
.classed('hide', warnings.length === 0);
|
||||
|
||||
if (warnings.length > 0) {
|
||||
_pane.select('.hide-toggle-issues_warnings .hide-toggle-text')
|
||||
.text(t('issues.warnings.list_title', { count: warnings.length }));
|
||||
if (!_pane.select('.disclosure-wrap-issues_warnings').classed('hide')) {
|
||||
updateWarningsList();
|
||||
}
|
||||
}
|
||||
|
||||
_pane.select('.issues-none')
|
||||
.classed('hide', warnings.length > 0 || errors.length > 0);
|
||||
|
||||
//if (!_pane.select('.disclosure-wrap-issues_options').classed('hide')) {
|
||||
// updateFeatureApplicabilityList();
|
||||
//}
|
||||
}
|
||||
|
||||
function issues(selection) {
|
||||
|
||||
function hidePane() {
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
function togglePane() {
|
||||
if (d3_event) d3_event.preventDefault();
|
||||
setVisible(!_button.classed('active'));
|
||||
}
|
||||
|
||||
function setVisible(show) {
|
||||
if (show !== _shown) {
|
||||
_button.classed('active', show);
|
||||
_shown = show;
|
||||
|
||||
if (show) {
|
||||
uiBackground.hidePane();
|
||||
uiHelp.hidePane();
|
||||
uiMapData.hidePane();
|
||||
update();
|
||||
|
||||
_pane
|
||||
.style('display', 'block')
|
||||
.style('right', '-300px')
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('right', '0px');
|
||||
|
||||
} else {
|
||||
_pane
|
||||
.style('display', 'block')
|
||||
.style('right', '0px')
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('right', '-300px')
|
||||
.on('end', function() {
|
||||
d3_select(this).style('display', 'none');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_pane = selection
|
||||
.append('div')
|
||||
.attr('class', 'fillL map-pane hide');
|
||||
|
||||
var paneTooltip = tooltip()
|
||||
.placement((textDirection === 'rtl') ? 'right' : 'left')
|
||||
.html(true)
|
||||
.title(uiTooltipHtml(t('issues.title'), key));
|
||||
|
||||
_button = selection
|
||||
.append('button')
|
||||
.attr('tabindex', -1)
|
||||
.on('click', togglePane)
|
||||
.call(svgIcon('#iD-icon-alert', 'light'))
|
||||
.call(addIconBadge)
|
||||
.call(paneTooltip);
|
||||
|
||||
var heading = _pane
|
||||
.append('div')
|
||||
.attr('class', 'pane-heading');
|
||||
|
||||
heading
|
||||
.append('h2')
|
||||
.text(t('issues.title'));
|
||||
|
||||
heading
|
||||
.append('button')
|
||||
.on('click', function() { uiIssues.hidePane(); })
|
||||
.call(svgIcon('#iD-icon-close'));
|
||||
|
||||
var content = _pane
|
||||
.append('div')
|
||||
.attr('class', 'pane-content');
|
||||
|
||||
content
|
||||
.append('div')
|
||||
.attr('class', 'issues-none')
|
||||
.call(renderNoIssuesBox);
|
||||
|
||||
// errors
|
||||
content
|
||||
.append('div')
|
||||
.attr('class', 'issues-errors')
|
||||
.call(uiDisclosure(context, 'issues_errors', true)
|
||||
.content(renderErrorsList)
|
||||
);
|
||||
|
||||
// warnings
|
||||
content
|
||||
.append('div')
|
||||
.attr('class', 'issues-warnings')
|
||||
.call(uiDisclosure(context, 'issues_warnings', true)
|
||||
.content(renderWarningsList)
|
||||
);
|
||||
|
||||
// options
|
||||
/*
|
||||
// add this back to core.yaml when re-enabling the options
|
||||
options:
|
||||
title: Options
|
||||
features_to_validate:
|
||||
edited:
|
||||
description: Edited features only
|
||||
tooltip: Flag issues with features you create and modify
|
||||
all:
|
||||
description: All features
|
||||
tooltip: Flag issues with all nearby features
|
||||
|
||||
content
|
||||
.append('div')
|
||||
.attr('class', 'issues-options')
|
||||
.call(uiDisclosure(context, 'issues_options', true)
|
||||
.title(t('issues.options.title'))
|
||||
.content(renderIssuesOptions)
|
||||
);
|
||||
*/
|
||||
update();
|
||||
|
||||
context.keybinding()
|
||||
.on(key, togglePane);
|
||||
|
||||
uiIssues.hidePane = hidePane;
|
||||
uiIssues.togglePane = togglePane;
|
||||
uiIssues.setVisible = setVisible;
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { modeBrowse } from '../modes';
|
||||
import { uiBackground } from './background';
|
||||
import { uiDisclosure } from './disclosure';
|
||||
import { uiHelp } from './help';
|
||||
import { uiIssues } from './issues';
|
||||
import { uiSettingsCustomData } from './settings/custom_data';
|
||||
import { uiTooltipHtml } from './tooltipHtml';
|
||||
|
||||
@@ -494,7 +495,6 @@ export function uiMapData(context) {
|
||||
// Enter
|
||||
var enter = items.enter()
|
||||
.append('li')
|
||||
.attr('class', 'layer')
|
||||
.call(tooltip()
|
||||
.html(true)
|
||||
.title(function(d) {
|
||||
@@ -647,6 +647,7 @@ export function uiMapData(context) {
|
||||
if (show) {
|
||||
uiBackground.hidePane();
|
||||
uiHelp.hidePane();
|
||||
uiIssues.hidePane();
|
||||
update();
|
||||
|
||||
pane
|
||||
|
||||
@@ -10,12 +10,7 @@ import { osmEntity } from '../osm';
|
||||
import { svgIcon } from '../svg';
|
||||
import { services } from '../services';
|
||||
import { uiCombobox, uiDisclosure } from './index';
|
||||
import {
|
||||
utilDisplayName,
|
||||
utilDisplayType,
|
||||
utilNoAuto,
|
||||
utilHighlightEntity
|
||||
} from '../util';
|
||||
import { utilDisplayName, utilDisplayType, utilHighlightEntities, utilNoAuto } from '../util';
|
||||
|
||||
|
||||
export function uiRawMemberEditor(context) {
|
||||
@@ -37,7 +32,7 @@ export function uiRawMemberEditor(context) {
|
||||
context.map().zoomTo(entity);
|
||||
|
||||
// highlight the feature in case it wasn't previously on-screen
|
||||
utilHighlightEntity(d.id, true, context);
|
||||
utilHighlightEntities([d.id], true, context);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +40,7 @@ export function uiRawMemberEditor(context) {
|
||||
d3_event.preventDefault();
|
||||
|
||||
// remove the hover-highlight styling
|
||||
utilHighlightEntity(d.id, false, context);
|
||||
utilHighlightEntities([d.id], false, context);
|
||||
|
||||
var entity = context.entity(d.id);
|
||||
var mapExtent = context.map().extent();
|
||||
@@ -82,7 +77,7 @@ export function uiRawMemberEditor(context) {
|
||||
context.enter(modeBrowse(context));
|
||||
}
|
||||
|
||||
utilHighlightEntity(d.id, false, context);
|
||||
utilHighlightEntities([d.id], false, context);
|
||||
}
|
||||
|
||||
|
||||
@@ -152,10 +147,10 @@ export function uiRawMemberEditor(context) {
|
||||
// highlight the member feature in the map while hovering on the list item
|
||||
item
|
||||
.on('mouseover', function() {
|
||||
utilHighlightEntity(d.id, true, context);
|
||||
utilHighlightEntities([d.id], true, context);
|
||||
})
|
||||
.on('mouseout', function() {
|
||||
utilHighlightEntity(d.id, false, context);
|
||||
utilHighlightEntities([d.id], false, context);
|
||||
});
|
||||
|
||||
var labelLink = label
|
||||
|
||||
@@ -21,7 +21,7 @@ import { osmEntity, osmRelation } from '../osm';
|
||||
import { services } from '../services';
|
||||
import { svgIcon } from '../svg';
|
||||
import { uiCombobox, uiDisclosure } from './index';
|
||||
import { utilDisplayName, utilNoAuto, utilHighlightEntity } from '../util';
|
||||
import { utilDisplayName, utilNoAuto, utilHighlightEntities } from '../util';
|
||||
|
||||
|
||||
export function uiRawMembershipEditor(context) {
|
||||
@@ -38,7 +38,7 @@ export function uiRawMembershipEditor(context) {
|
||||
d3_event.preventDefault();
|
||||
|
||||
// remove the hover-highlight styling
|
||||
utilHighlightEntity(d.relation.id, false, context);
|
||||
utilHighlightEntities([d.relation.id], false, context);
|
||||
|
||||
context.enter(modeSelect(context, [d.relation.id]));
|
||||
}
|
||||
@@ -194,10 +194,10 @@ export function uiRawMembershipEditor(context) {
|
||||
// highlight the relation in the map while hovering on the list item
|
||||
d3_select(this)
|
||||
.on('mouseover', function() {
|
||||
utilHighlightEntity(d.relation.id, true, context);
|
||||
utilHighlightEntities([d.relation.id], true, context);
|
||||
})
|
||||
.on('mouseout', function() {
|
||||
utilHighlightEntity(d.relation.id, false, context);
|
||||
utilHighlightEntities([d.relation.id], false, context);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { t } from '../util/locale';
|
||||
import { modeSelect } from '../modes';
|
||||
import { osmEntity } from '../osm';
|
||||
import { svgIcon } from '../svg';
|
||||
import { utilDisplayName, utilHighlightEntity } from '../util';
|
||||
import { utilDisplayName, utilHighlightEntities } from '../util';
|
||||
|
||||
|
||||
export function uiSelectionList(context, selectedIDs) {
|
||||
@@ -69,14 +69,13 @@ export function uiSelectionList(context, selectedIDs) {
|
||||
|
||||
enter
|
||||
.each(function(d) {
|
||||
// highlight the feature in the map while hovering on the list item
|
||||
d3_select(this).on('mouseover', function() {
|
||||
utilHighlightEntity(d.id, true, context);
|
||||
d3_select(this).on('mouseover', function() {
|
||||
utilHighlightEntities([d.id], true, context);
|
||||
});
|
||||
d3_select(this).on('mouseout', function() {
|
||||
utilHighlightEntities([d.id], false, context);
|
||||
});
|
||||
});
|
||||
d3_select(this).on('mouseout', function() {
|
||||
utilHighlightEntity(d.id, false, context);
|
||||
});
|
||||
});
|
||||
|
||||
var label = enter
|
||||
.append('button')
|
||||
|
||||
@@ -196,7 +196,6 @@ export function uiSidebar(context) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sidebar.hover = _throttle(hover, 200);
|
||||
|
||||
|
||||
@@ -213,9 +212,9 @@ export function uiSidebar(context) {
|
||||
sidebar.hide();
|
||||
|
||||
if (id) {
|
||||
var entity = context.entity(id);
|
||||
// uncollapse the sidebar
|
||||
if (selection.classed('collapsed')) {
|
||||
var entity = context.entity(id);
|
||||
var extent = entity.extent(context.graph());
|
||||
sidebar.expand(sidebar.intersects(extent));
|
||||
}
|
||||
@@ -237,6 +236,10 @@ export function uiSidebar(context) {
|
||||
.call(inspector, newFeature);
|
||||
}
|
||||
|
||||
sidebar.showPresetList = function() {
|
||||
inspector.showList(context.presets().match(entity, context.graph()));
|
||||
};
|
||||
|
||||
} else {
|
||||
inspector
|
||||
.state('hide');
|
||||
@@ -342,7 +345,7 @@ export function uiSidebar(context) {
|
||||
resizer.on('dblclick', sidebar.toggle);
|
||||
}
|
||||
|
||||
|
||||
sidebar.showPresetList = function() {};
|
||||
sidebar.hover = function() {};
|
||||
sidebar.hover.cancel = function() {};
|
||||
sidebar.intersects = function() {};
|
||||
|
||||
@@ -4,6 +4,7 @@ export { utilCleanTags } from './clean_tags';
|
||||
export { utilDisplayName } from './util';
|
||||
export { utilDisplayNameForPath } from './util';
|
||||
export { utilDisplayType } from './util';
|
||||
export { utilDisplayLabel } from './util';
|
||||
export { utilEntityRoot } from './util';
|
||||
export { utilEditDistance } from './util';
|
||||
export { utilEntitySelector } from './util';
|
||||
@@ -15,12 +16,13 @@ export { utilGetAllNodes } from './util';
|
||||
export { utilGetPrototypeOf } from './util';
|
||||
export { utilGetSetValue } from './get_set_value';
|
||||
export { utilHashcode } from './util';
|
||||
export { utilHighlightEntity } from './util';
|
||||
export { utilHighlightEntities } from './util';
|
||||
export { utilIdleWorker } from './idle_worker';
|
||||
export { utilKeybinding } from './keybinding';
|
||||
export { utilNoAuto } from './util';
|
||||
export { utilPrefixCSSProperty } from './util';
|
||||
export { utilPrefixDOMProperty } from './util';
|
||||
export { utilPreset } from './util';
|
||||
export { utilQsString } from './util';
|
||||
export { utilRebind } from './rebind';
|
||||
export { utilSetTransform } from './util';
|
||||
|
||||
+29
-7
@@ -62,6 +62,14 @@ export function utilEntityOrDeepMemberSelector(ids, graph) {
|
||||
}
|
||||
|
||||
|
||||
// Adds or removes highlight styling for the specified entities
|
||||
export function utilHighlightEntities(ids, highlighted, context) {
|
||||
context.surface()
|
||||
.selectAll(utilEntityOrDeepMemberSelector(ids, context.graph()))
|
||||
.classed('highlighted', highlighted);
|
||||
}
|
||||
|
||||
|
||||
export function utilGetAllNodes(ids, graph) {
|
||||
var seen = {};
|
||||
var nodes = [];
|
||||
@@ -123,6 +131,27 @@ export function utilDisplayType(id) {
|
||||
}
|
||||
|
||||
|
||||
export function utilDisplayLabel(entity, context) {
|
||||
var displayName = utilDisplayName(entity);
|
||||
if (displayName) {
|
||||
// use the display name if there is one
|
||||
return displayName;
|
||||
}
|
||||
var preset = utilPreset(entity, context);
|
||||
if (preset && preset.name()) {
|
||||
// use the preset name if there is a match
|
||||
return preset.name();
|
||||
}
|
||||
// fallback to the display type (node/way/relation)
|
||||
return utilDisplayType(entity.id);
|
||||
}
|
||||
|
||||
|
||||
export function utilPreset(entity, context) {
|
||||
return context.presets().match(entity, context.graph());
|
||||
}
|
||||
|
||||
|
||||
export function utilEntityRoot(entityType) {
|
||||
return {
|
||||
node: 'n',
|
||||
@@ -324,10 +353,3 @@ export function utilHashcode(str) {
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
// Adds or removes highlight styling for the specified entity's SVG elements in the map.
|
||||
export function utilHighlightEntity(id, highlighted, context) {
|
||||
context.surface()
|
||||
.selectAll(utilEntityOrDeepMemberSelector([id], context.graph()))
|
||||
.classed('highlighted', highlighted);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import _cloneDeep from 'lodash-es/cloneDeep';
|
||||
import {
|
||||
geoExtent,
|
||||
geoLineIntersection,
|
||||
geoMetersToLat,
|
||||
geoMetersToLon,
|
||||
geoSphericalDistance,
|
||||
geoVecInterp,
|
||||
geoHasSelfIntersections,
|
||||
geoSphericalClosestNode
|
||||
} from '../geo';
|
||||
|
||||
import { actionAddMidpoint, actionChangeTags, actionMergeNodes } from '../actions';
|
||||
import { t } from '../util/locale';
|
||||
import { utilDisplayLabel } from '../util';
|
||||
import { validationIssue, validationIssueFix } from '../core/validator';
|
||||
|
||||
|
||||
/**
|
||||
* Look for roads that can be connected to other roads with a short extension
|
||||
*/
|
||||
export function validationAlmostJunction() {
|
||||
var type = 'almost_junction';
|
||||
|
||||
|
||||
function isHighway(entity) {
|
||||
return entity.type === 'way' && entity.tags.highway && entity.tags.highway !== 'no';
|
||||
}
|
||||
|
||||
function isNoexit(node) {
|
||||
return node.tags.noexit && node.tags.noexit === 'yes';
|
||||
}
|
||||
|
||||
function findConnectableEndNodesByExtension(way, graph, tree) {
|
||||
var results = [];
|
||||
var nidFirst = way.nodes[0];
|
||||
var nidLast = way.nodes[way.nodes.length - 1];
|
||||
var nodeFirst = graph.entity(nidFirst);
|
||||
var nodeLast = graph.entity(nidLast);
|
||||
|
||||
if (nidFirst === nidLast) return results;
|
||||
|
||||
var testNodes;
|
||||
|
||||
if (!isNoexit(nodeFirst) && graph.parentWays(nodeFirst).length === 1) {
|
||||
var connNearFirst = canConnectByExtend(way, 0, graph, tree);
|
||||
if (connNearFirst !== null) {
|
||||
testNodes = _cloneDeep(graph.childNodes(way));
|
||||
testNodes[0].loc = connNearFirst.cross_loc;
|
||||
// don't flag issue if connecting the ways would cause self-intersection
|
||||
if (!geoHasSelfIntersections(testNodes, nodeFirst.id)) {
|
||||
results.push({
|
||||
node: nodeFirst,
|
||||
wid: connNearFirst.wid,
|
||||
edge: connNearFirst.edge,
|
||||
cross_loc: connNearFirst.cross_loc
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNoexit(nodeLast) && graph.parentWays(nodeLast).length === 1) {
|
||||
var connNearLast = canConnectByExtend(way, way.nodes.length - 1, graph, tree);
|
||||
if (connNearLast !== null) {
|
||||
testNodes = _cloneDeep(graph.childNodes(way));
|
||||
testNodes[testNodes.length-1].loc = connNearLast.cross_loc;
|
||||
// don't flag issue if connecting the ways would cause self-intersection
|
||||
if (!geoHasSelfIntersections(testNodes, nodeLast.id)) {
|
||||
results.push({
|
||||
node: nodeLast,
|
||||
wid: connNearLast.wid,
|
||||
edge: connNearLast.edge,
|
||||
cross_loc: connNearLast.cross_loc
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
function canConnectByExtend(way, endNodeIdx, graph, tree) {
|
||||
var EXTEND_TH_METERS = 5;
|
||||
var tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point
|
||||
var midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge
|
||||
var tipNode = graph.entity(tipNid);
|
||||
var midNode = graph.entity(midNid);
|
||||
var lon = tipNode.loc[0];
|
||||
var lat = tipNode.loc[1];
|
||||
var lon_range = geoMetersToLon(EXTEND_TH_METERS, lat) / 2;
|
||||
var lat_range = geoMetersToLat(EXTEND_TH_METERS) / 2;
|
||||
var queryExtent = geoExtent([
|
||||
[lon - lon_range, lat - lat_range],
|
||||
[lon + lon_range, lat + lat_range]
|
||||
]);
|
||||
|
||||
// first, extend the edge of [midNode -> tipNode] by EXTEND_TH_METERS and find the "extended tip" location
|
||||
var edgeLen = geoSphericalDistance(midNode.loc, tipNode.loc);
|
||||
var t = EXTEND_TH_METERS / edgeLen + 1.0;
|
||||
var extTipLoc = geoVecInterp(midNode.loc, tipNode.loc, t);
|
||||
|
||||
// then, check if the extension part [tipNode.loc -> extTipLoc] intersects any other ways
|
||||
var intersected = tree.intersects(queryExtent, graph);
|
||||
for (var i = 0; i < intersected.length; i++) {
|
||||
if (!isHighway(intersected[i]) || intersected[i].id === way.id) continue;
|
||||
|
||||
var way2 = intersected[i];
|
||||
for (var j = 0; j < way2.nodes.length - 1; j++) {
|
||||
var nA = graph.entity(way2.nodes[j]);
|
||||
var nB = graph.entity(way2.nodes[j + 1]);
|
||||
var crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]);
|
||||
if (crossLoc !== null) {
|
||||
return {
|
||||
wid: way2.id,
|
||||
edge: [nA.id, nB.id],
|
||||
cross_loc: crossLoc
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
var validation = function(endHighway, context) {
|
||||
if (!isHighway(endHighway)) return [];
|
||||
|
||||
var graph = context.graph();
|
||||
var tree = context.history().tree();
|
||||
var issues = [];
|
||||
|
||||
var extendableNodeInfos = findConnectableEndNodesByExtension(endHighway, graph, tree);
|
||||
extendableNodeInfos.forEach(function(extendableNodeInfo) {
|
||||
var node = extendableNodeInfo.node;
|
||||
var edgeHighway = graph.entity(extendableNodeInfo.wid);
|
||||
|
||||
var fixes = [new validationIssueFix({
|
||||
title: t('issues.fix.connect_features.title'),
|
||||
onClick: function() {
|
||||
var endNode = this.issue.entities[1];
|
||||
var targetEdge = this.issue.info.edge;
|
||||
var crossLoc = this.issue.info.cross_loc;
|
||||
var edgeNodes = [context.graph().entity(targetEdge[0]), context.graph().entity(targetEdge[1])];
|
||||
var closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc);
|
||||
|
||||
var annotation = t('issues.fix.connect_almost_junction.annotation');
|
||||
// already a point nearby, just connect to that
|
||||
if (closestNodeInfo.distance < 0.75) {
|
||||
context.perform(
|
||||
actionMergeNodes([closestNodeInfo.node.id, endNode.id], closestNodeInfo.node.loc),
|
||||
annotation
|
||||
);
|
||||
// else add the end node to the edge way
|
||||
} else {
|
||||
context.perform(
|
||||
actionAddMidpoint({loc: crossLoc, edge: targetEdge}, endNode),
|
||||
annotation
|
||||
);
|
||||
}
|
||||
}
|
||||
})];
|
||||
|
||||
if (Object.keys(node.tags).length === 0) {
|
||||
// node has no tags, suggest noexit fix
|
||||
fixes.push(new validationIssueFix({
|
||||
icon: 'maki-barrier',
|
||||
title: t('issues.fix.tag_as_disconnected.title'),
|
||||
onClick: function() {
|
||||
var nodeID = this.issue.entities[1].id;
|
||||
context.perform(
|
||||
actionChangeTags(nodeID, { noexit: 'yes' }),
|
||||
t('issues.fix.tag_as_disconnected.annotation')
|
||||
);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
issues.push(new validationIssue({
|
||||
type: type,
|
||||
severity: 'warning',
|
||||
message: t('issues.almost_junction.message', {
|
||||
feature: utilDisplayLabel(endHighway, context),
|
||||
feature2: utilDisplayLabel(edgeHighway, context)
|
||||
}),
|
||||
tooltip: t('issues.almost_junction.highway-highway.tip'),
|
||||
entities: [endHighway, node, edgeHighway],
|
||||
loc: extendableNodeInfo.node.loc,
|
||||
info: {
|
||||
edge: extendableNodeInfo.edge,
|
||||
cross_loc: extendableNodeInfo.cross_loc
|
||||
},
|
||||
fixes: fixes
|
||||
}));
|
||||
});
|
||||
|
||||
return issues;
|
||||
};
|
||||
|
||||
validation.type = type;
|
||||
|
||||
return validation;
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
import _clone from 'lodash-es/clone';
|
||||
import _map from 'lodash-es/map';
|
||||
import _flattenDeep from 'lodash-es/flatten';
|
||||
|
||||
import { actionAddMidpoint, actionMergeNodes } from '../actions';
|
||||
import { geoExtent, geoLineIntersection, geoSphericalClosestNode } from '../geo';
|
||||
import { osmNode } from '../osm';
|
||||
import { t } from '../util/locale';
|
||||
import { utilDisplayLabel } from '../util';
|
||||
import { validationIssue, validationIssueFix } from '../core/validator';
|
||||
|
||||
|
||||
export function validationCrossingWays() {
|
||||
var type = 'crossing_ways';
|
||||
|
||||
|
||||
// Check if the edge going from n1 to n2 crosses (without a connection node)
|
||||
// any edge on way. Return the cross point if so.
|
||||
function findEdgeToWayCrossCoords(n1, n2, way, graph) {
|
||||
var crossCoords = [];
|
||||
var nA, nB;
|
||||
var segment1 = [n1.loc, n2.loc];
|
||||
var segment2;
|
||||
|
||||
var nodes = graph.childNodes(way);
|
||||
for (var j = 0; j < nodes.length - 1; j++) {
|
||||
nA = nodes[j];
|
||||
nB = nodes[j + 1];
|
||||
if (nA.id === n1.id || nA.id === n2.id ||
|
||||
nB.id === n1.id || nB.id === n2.id) {
|
||||
// n1 or n2 is a connection node; skip
|
||||
continue;
|
||||
}
|
||||
segment2 = [nA.loc, nB.loc];
|
||||
var point = geoLineIntersection(segment1, segment2);
|
||||
if (point) {
|
||||
crossCoords.push({ edge: [nA.id, nB.id], point: point });
|
||||
}
|
||||
}
|
||||
return crossCoords;
|
||||
}
|
||||
|
||||
|
||||
// returns the way or its parent relation, whichever has a useful feature type
|
||||
function getFeatureWithFeatureTypeTagsForWay(way, graph) {
|
||||
if (getFeatureTypeForTags(way.tags) === null) {
|
||||
// if the way doesn't match a feature type, check is parent relations
|
||||
var parentRels = graph.parentRelations(way);
|
||||
for (var i = 0; i < parentRels.length; i++) {
|
||||
var rel = parentRels[i];
|
||||
if (getFeatureTypeForTags(rel.tags) !== null) {
|
||||
return rel;
|
||||
}
|
||||
}
|
||||
}
|
||||
return way;
|
||||
}
|
||||
|
||||
|
||||
function hasTag(tags, key) {
|
||||
return tags[key] !== undefined && tags[key] !== 'no';
|
||||
}
|
||||
|
||||
|
||||
function getFeatureTypeForCrossingCheck(way, graph) {
|
||||
var tags = getFeatureWithFeatureTypeTagsForWay(way, graph).tags;
|
||||
return getFeatureTypeForTags(tags);
|
||||
}
|
||||
|
||||
|
||||
// only validate certain waterway features
|
||||
var waterways = ['canal', 'ditch', 'drain', 'river', 'stream'];
|
||||
// ignore certain highway and railway features
|
||||
var ignoredHighways = ['rest_area', 'services'];
|
||||
var ignoredRailways = ['train_wash'];
|
||||
|
||||
|
||||
function getFeatureTypeForTags(tags) {
|
||||
if (hasTag(tags, 'building')) return 'building';
|
||||
|
||||
// don't check non-building areas
|
||||
if (hasTag(tags, 'area')) return null;
|
||||
|
||||
if (hasTag(tags, 'highway') && ignoredHighways.indexOf(tags.highway) === -1) return 'highway';
|
||||
if (hasTag(tags, 'railway') && ignoredRailways.indexOf(tags.railway) === -1) return 'railway';
|
||||
if (hasTag(tags, 'waterway') && waterways.indexOf(tags.waterway) !== -1) return 'waterway';
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function extendTagsByInferredLayer(tags, way) {
|
||||
if (!hasTag(tags, 'layer')) {
|
||||
tags.layer = way.layer().toString();
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
|
||||
function isLegitCrossing(way1, featureType1, way2, featureType2, graph) {
|
||||
var tags1 = _clone(getFeatureWithFeatureTypeTagsForWay(way1, graph).tags);
|
||||
var tags2 = _clone(getFeatureWithFeatureTypeTagsForWay(way2, graph).tags);
|
||||
tags1 = extendTagsByInferredLayer(tags1, way1);
|
||||
tags2 = extendTagsByInferredLayer(tags2, way2);
|
||||
|
||||
// For better readability, not chaining all the true conditions into one if statement.
|
||||
if ((featureType1 === 'highway' && featureType2 === 'highway') ||
|
||||
(featureType1 === 'highway' && featureType2 === 'railway') ||
|
||||
(featureType1 === 'railway' && featureType2 === 'railway')) {
|
||||
// Legit cases:
|
||||
// (1) they're on different layers
|
||||
// (2) only one of the two ways is on a bridge
|
||||
// (3) only one of the two ways is in a tunnel
|
||||
if (tags1.layer !== tags2.layer) return true;
|
||||
if (hasTag(tags1, 'bridge') && !hasTag(tags2, 'bridge')) return true;
|
||||
if (!hasTag(tags1, 'bridge') && hasTag(tags2, 'bridge')) return true;
|
||||
if (hasTag(tags1, 'tunnel') && !hasTag(tags2, 'tunnel')) return true;
|
||||
if (!hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel')) return true;
|
||||
}
|
||||
if ((featureType1 === 'highway' && featureType2 === 'waterway') ||
|
||||
(featureType1 === 'railway' && featureType2 === 'waterway')) {
|
||||
// Legit cases:
|
||||
// (1) highway/railway is on a bridge
|
||||
// (2) only one of the two ways is in a tunnel
|
||||
// (3) both are in tunnels but on different layers
|
||||
if (hasTag(tags1, 'bridge')) return true;
|
||||
if (hasTag(tags1, 'tunnel') && !hasTag(tags2, 'tunnel')) return true;
|
||||
if (!hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel')) return true;
|
||||
if (hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel') && tags1.layer !== tags2.layer) return true;
|
||||
}
|
||||
if ((featureType1 === 'highway' && featureType2 === 'building') ||
|
||||
(featureType1 === 'railway' && featureType2 === 'building')) {
|
||||
// Legit cases:
|
||||
// (1) highway/railway has a bridge or tunnel tag
|
||||
// (2) highway/railway has a covered tag
|
||||
if (hasTag(tags1, 'bridge') || hasTag(tags1, 'tunnel') || hasTag(tags1, 'covered')) return true;
|
||||
}
|
||||
if (featureType1 === 'waterway' && featureType2 === 'waterway') {
|
||||
// Legit cases:
|
||||
// (1) only one of the water is in a tunnel
|
||||
// (2) both are in tunnels but on differnt layers
|
||||
if (hasTag(tags1, 'tunnel') && !hasTag(tags2, 'tunnel')) return true;
|
||||
if (!hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel')) return true;
|
||||
if (hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel') && tags1.layer !== tags2.layer) return true;
|
||||
}
|
||||
if (featureType1 === 'waterway' && featureType2 === 'building') {
|
||||
// Legit cases:
|
||||
// (1) water is in a tunnel
|
||||
// (2) water has a covered tag
|
||||
if (hasTag(tags1, 'tunnel') || hasTag(tags1, 'covered')) return true;
|
||||
}
|
||||
if (featureType1 === 'building' && featureType2 === 'building') {
|
||||
// Legit case: they're on different layers
|
||||
if (tags1.layer !== tags2.layer) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// highway values for which we shouldn't recommend connecting to waterways
|
||||
var highwaysDisallowingFords = [
|
||||
'motorway', 'motorway_link', 'trunk', 'trunk_link',
|
||||
'primary', 'primary_link', 'secondary', 'secondary_link'
|
||||
];
|
||||
var pathHighways = [
|
||||
'path', 'footway', 'cycleway', 'bridleway', 'pedestrian', 'steps'
|
||||
];
|
||||
|
||||
function tagsForConnectionNodeIfAllowed(entity1, entity2) {
|
||||
var featureType1 = getFeatureTypeForTags(entity1.tags);
|
||||
var featureType2 = getFeatureTypeForTags(entity2.tags);
|
||||
if (featureType1 === featureType2) {
|
||||
if (featureType1 === 'highway') {
|
||||
var entity1IsPath = pathHighways.indexOf(entity1.tags.highway) !== -1;
|
||||
var entity2IsPath = pathHighways.indexOf(entity2.tags.highway) !== -1;
|
||||
if ((entity1IsPath || entity2IsPath) && entity1IsPath !== entity2IsPath) {
|
||||
// one feature is a path but not both, use a crossing
|
||||
|
||||
var pathFeature = entity1IsPath ? entity1 : entity2;
|
||||
if (pathFeature.tags.highway === 'footway' &&
|
||||
pathFeature.tags.footway === 'crossing' &&
|
||||
['marked', 'unmarked'].indexOf(pathFeature.tags.crossing) !== -1) {
|
||||
// if the path is a crossing, match the crossing type
|
||||
return { highway: 'crossing', crossing: pathFeature.tags.crossing };
|
||||
}
|
||||
return { highway: 'crossing' };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (featureType1 === 'waterway') return {};
|
||||
if (featureType1 === 'railway') return {};
|
||||
|
||||
} else {
|
||||
var featureTypes = [featureType1, featureType2];
|
||||
if (featureTypes.indexOf('highway') !== -1) {
|
||||
if (featureTypes.indexOf('building') !== -1) return {};
|
||||
if (featureTypes.indexOf('railway') !== -1) {
|
||||
if (pathHighways.indexOf(entity1.tags.highway) !== -1 ||
|
||||
pathHighways.indexOf(entity2.tags.highway) !== -1) {
|
||||
// path-rail connections use this tag
|
||||
return { railway: 'crossing' };
|
||||
} else {
|
||||
// road-rail connections use this tag
|
||||
return { railway: 'level_crossing' };
|
||||
}
|
||||
}
|
||||
|
||||
if (featureTypes.indexOf('waterway') !== -1) {
|
||||
// do not allow fords on structures
|
||||
if (hasTag(entity1.tags, 'tunnel') && hasTag(entity2.tags, 'tunnel')) return null;
|
||||
if (hasTag(entity1.tags, 'bridge') && hasTag(entity2.tags, 'bridge')) return null;
|
||||
|
||||
if (highwaysDisallowingFords.indexOf(entity1.tags.highway) !== -1 ||
|
||||
highwaysDisallowingFords.indexOf(entity2.tags.highway) !== -1) {
|
||||
// do not allow fords on major highways
|
||||
return null;
|
||||
}
|
||||
return { ford: 'yes' };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function findCrossingsByWay(primaryWay, graph, tree) {
|
||||
var edgeCrossInfos = [];
|
||||
if (primaryWay.type !== 'way') return edgeCrossInfos;
|
||||
|
||||
var primaryFeatureType = getFeatureTypeForCrossingCheck(primaryWay, graph);
|
||||
if (primaryFeatureType === null) return edgeCrossInfos;
|
||||
|
||||
for (var i = 0; i < primaryWay.nodes.length - 1; i++) {
|
||||
var nid1 = primaryWay.nodes[i];
|
||||
var nid2 = primaryWay.nodes[i + 1];
|
||||
var n1 = graph.entity(nid1);
|
||||
var n2 = graph.entity(nid2);
|
||||
var extent = geoExtent([
|
||||
[
|
||||
Math.min(n1.loc[0], n2.loc[0]),
|
||||
Math.min(n1.loc[1], n2.loc[1])
|
||||
],
|
||||
[
|
||||
Math.max(n1.loc[0], n2.loc[0]),
|
||||
Math.max(n1.loc[1], n2.loc[1])
|
||||
]
|
||||
]);
|
||||
|
||||
var intersected = tree.intersects(extent, graph);
|
||||
for (var j = 0; j < intersected.length; j++) {
|
||||
if (intersected[j].type !== 'way') continue;
|
||||
|
||||
// only check crossing highway, waterway, building, and railway
|
||||
var way = intersected[j];
|
||||
var wayFeatureType = getFeatureTypeForCrossingCheck(way, graph);
|
||||
if (wayFeatureType === null ||
|
||||
isLegitCrossing(primaryWay, primaryFeatureType, way, wayFeatureType, graph) ||
|
||||
isLegitCrossing(way, wayFeatureType, primaryWay, primaryFeatureType, graph)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var crossCoords = findEdgeToWayCrossCoords(n1, n2, way, graph);
|
||||
for (var k = 0; k < crossCoords.length; k++) {
|
||||
var crossingInfo = crossCoords[k];
|
||||
edgeCrossInfos.push({
|
||||
ways: [primaryWay, way],
|
||||
featureTypes: [primaryFeatureType, wayFeatureType],
|
||||
edges: [[n1.id, n2.id], crossingInfo.edge],
|
||||
crossPoint: crossingInfo.point
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return edgeCrossInfos;
|
||||
}
|
||||
|
||||
|
||||
var validation = function(entity, context) {
|
||||
var graph = context.graph();
|
||||
var tree = context.history().tree();
|
||||
|
||||
var waysToCheck = _flattenDeep(_map([entity], function(entity) {
|
||||
if (!getFeatureTypeForTags(entity.tags)) {
|
||||
return [];
|
||||
}
|
||||
if (entity.type === 'way') {
|
||||
return entity;
|
||||
} else if (entity.type === 'relation' &&
|
||||
entity.tags.type === 'multipolygon' &&
|
||||
// only check multipolygons if they are buildings
|
||||
hasTag(entity.tags, 'building')) {
|
||||
return _map(entity.members, function(member) {
|
||||
if (context.hasEntity(member.id)) {
|
||||
var entity = context.entity(member.id);
|
||||
if (entity.type === 'way') {
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}));
|
||||
|
||||
var crossings = waysToCheck.reduce(function(array, way) {
|
||||
return array.concat(findCrossingsByWay(way, graph, tree));
|
||||
}, []);
|
||||
|
||||
var issues = [];
|
||||
crossings.forEach(function(crossing) {
|
||||
issues.push(createIssue(crossing, context));
|
||||
});
|
||||
return issues;
|
||||
};
|
||||
|
||||
|
||||
function createIssue(crossing, context) {
|
||||
var graph = context.graph();
|
||||
|
||||
// use the entities with the tags that define the feature type
|
||||
var entities = crossing.ways.sort(function(entity1, entity2) {
|
||||
var type1 = getFeatureTypeForCrossingCheck(entity1, graph);
|
||||
var type2 = getFeatureTypeForCrossingCheck(entity2, graph);
|
||||
if (type1 === type2) {
|
||||
return utilDisplayLabel(entity1, context) > utilDisplayLabel(entity2, context);
|
||||
} else if (type1 === 'waterway') {
|
||||
return true;
|
||||
} else if (type2 === 'waterway') {
|
||||
return false;
|
||||
}
|
||||
return type1 < type2;
|
||||
});
|
||||
entities = _map(entities, function(way) {
|
||||
return getFeatureWithFeatureTypeTagsForWay(way, graph);
|
||||
});
|
||||
|
||||
var connectionTags = tagsForConnectionNodeIfAllowed(entities[0], entities[1]);
|
||||
|
||||
var crossingTypeID;
|
||||
if (hasTag(entities[0].tags, 'tunnel') && hasTag(entities[1].tags, 'tunnel')) {
|
||||
crossingTypeID = 'tunnel-tunnel';
|
||||
if (connectionTags) {
|
||||
crossingTypeID += '_connectable';
|
||||
}
|
||||
} else if (hasTag(entities[0].tags, 'bridge') && hasTag(entities[1].tags, 'bridge')) {
|
||||
crossingTypeID = 'bridge-bridge';
|
||||
if (connectionTags) {
|
||||
crossingTypeID += '_connectable';
|
||||
}
|
||||
} else {
|
||||
crossingTypeID = crossing.featureTypes.sort().join('-');
|
||||
}
|
||||
|
||||
var messageDict = {
|
||||
feature: utilDisplayLabel(entities[0], context),
|
||||
feature2: utilDisplayLabel(entities[1], context)
|
||||
};
|
||||
|
||||
var fixes = [];
|
||||
if (connectionTags) {
|
||||
fixes.push(new validationIssueFix({
|
||||
title: t('issues.fix.connect_features.title'),
|
||||
onClick: function() {
|
||||
var loc = this.issue.loc;
|
||||
var connectionTags = this.issue.info.connectionTags;
|
||||
var edges = this.issue.info.edges;
|
||||
|
||||
context.perform(
|
||||
function actionConnectCrossingWays(graph) {
|
||||
// create the new node for the points
|
||||
var node = osmNode({ loc: loc, tags: connectionTags });
|
||||
graph = graph.replace(node);
|
||||
|
||||
var nodesToMerge = [node.id];
|
||||
var mergeThresholdInMeters = 0.75;
|
||||
|
||||
edges.forEach(function(edge) {
|
||||
var edgeNodes = [graph.entity(edge[0]), graph.entity(edge[1])];
|
||||
var closestNodeInfo = geoSphericalClosestNode(edgeNodes, loc);
|
||||
// if there is already a point nearby, use that
|
||||
if (closestNodeInfo.distance < mergeThresholdInMeters) {
|
||||
nodesToMerge.push(closestNodeInfo.node.id);
|
||||
// else add the new node to the way
|
||||
} else {
|
||||
graph = actionAddMidpoint({loc: loc, edge: edge}, node)(graph);
|
||||
}
|
||||
});
|
||||
|
||||
if (nodesToMerge.length > 1) {
|
||||
// if we're using nearby nodes, merge them with the new node
|
||||
graph = actionMergeNodes(nodesToMerge, loc)(graph);
|
||||
}
|
||||
|
||||
return graph;
|
||||
},
|
||||
t('issues.fix.connect_crossing_features.annotation')
|
||||
);
|
||||
}
|
||||
}));
|
||||
}
|
||||
fixes.push(new validationIssueFix({
|
||||
title: t('issues.fix.reposition_features.title')
|
||||
}));
|
||||
return new validationIssue({
|
||||
type: type,
|
||||
severity: 'warning',
|
||||
message: t('issues.crossing_ways.message', messageDict),
|
||||
tooltip: t('issues.crossing_ways.'+crossingTypeID+'.tip'),
|
||||
entities: entities,
|
||||
info: { edges: crossing.edges, connectionTags: connectionTags },
|
||||
loc: crossing.crossPoint,
|
||||
fixes: fixes
|
||||
});
|
||||
}
|
||||
|
||||
validation.type = type;
|
||||
|
||||
|
||||
return validation;
|
||||
}
|
||||
@@ -1,30 +1,90 @@
|
||||
import _isEmpty from 'lodash-es/isEmpty';
|
||||
import _clone from 'lodash-es/clone';
|
||||
|
||||
import { t } from '../util/locale';
|
||||
import { utilTagText } from '../util/index';
|
||||
|
||||
import { actionChangeTags } from '../actions';
|
||||
import { utilDisplayLabel, utilTagText } from '../util';
|
||||
import { validationIssue, validationIssueFix } from '../core/validator';
|
||||
|
||||
export function validationDeprecatedTag() {
|
||||
var type = 'deprecated_tag';
|
||||
|
||||
var validation = function(changes) {
|
||||
var warnings = [];
|
||||
for (var i = 0; i < changes.created.length; i++) {
|
||||
var change = changes.created[i];
|
||||
var deprecatedTags = change.deprecatedTags();
|
||||
|
||||
if (!_isEmpty(deprecatedTags)) {
|
||||
var tags = utilTagText({ tags: deprecatedTags });
|
||||
warnings.push({
|
||||
id: 'deprecated_tags',
|
||||
message: t('validations.deprecated_tags', { tags: tags }),
|
||||
entity: change
|
||||
});
|
||||
var validation = function(entity, context) {
|
||||
var issues = [];
|
||||
var deprecatedTagsArray = entity.deprecatedTags();
|
||||
if (deprecatedTagsArray.length > 0) {
|
||||
for (var deprecatedTagIndex in deprecatedTagsArray) {
|
||||
var deprecatedTags = deprecatedTagsArray[deprecatedTagIndex];
|
||||
var tagsLabel = utilTagText({ tags: deprecatedTags.old });
|
||||
var featureLabel = utilDisplayLabel(entity, context);
|
||||
var isCombo = Object.keys(deprecatedTags.old).length > 1;
|
||||
var messageObj = { feature: featureLabel };
|
||||
if (isCombo) {
|
||||
messageObj.tags = tagsLabel;
|
||||
} else {
|
||||
messageObj.tag = tagsLabel;
|
||||
}
|
||||
var tagMessageID = isCombo ? 'combination' : 'single';
|
||||
issues.push(new validationIssue({
|
||||
type: type,
|
||||
severity: 'warning',
|
||||
message: t('issues.deprecated_tag.' + tagMessageID + '.message', messageObj),
|
||||
tooltip: t('issues.deprecated_tag.tip'),
|
||||
entities: [entity],
|
||||
hash: tagsLabel,
|
||||
info: {
|
||||
oldTags: deprecatedTags.old,
|
||||
replaceTags: deprecatedTags.replace
|
||||
},
|
||||
fixes: [
|
||||
new validationIssueFix({
|
||||
icon: 'iD-icon-up',
|
||||
title: t('issues.fix.' + (isCombo ? 'upgrade_tag_combo' : 'upgrade_tag') + '.title'),
|
||||
onClick: function() {
|
||||
var entity = this.issue.entities[0];
|
||||
var tags = _clone(entity.tags);
|
||||
var replaceTags = this.issue.info.replaceTags;
|
||||
var oldTags = this.issue.info.oldTags;
|
||||
var fixTextID = Object.keys(oldTags).length > 1 ? 'upgrade_tag_combo' : 'upgrade_tag';
|
||||
var transferValue;
|
||||
for (var oldTagKey in oldTags) {
|
||||
if (oldTags[oldTagKey] === '*') {
|
||||
transferValue = tags[oldTagKey];
|
||||
}
|
||||
delete tags[oldTagKey];
|
||||
}
|
||||
for (var replaceKey in replaceTags) {
|
||||
var replaceValue = replaceTags[replaceKey];
|
||||
if (replaceValue === '*') {
|
||||
if (tags[replaceKey]) {
|
||||
// any value is okay and there already
|
||||
// is one, so don't update it
|
||||
continue;
|
||||
} else {
|
||||
// otherwise assume `yes` is okay
|
||||
tags[replaceKey] = 'yes';
|
||||
}
|
||||
} else if (replaceValue === '$1') {
|
||||
tags[replaceKey] = transferValue;
|
||||
} else {
|
||||
tags[replaceKey] = replaceValue;
|
||||
}
|
||||
}
|
||||
context.perform(
|
||||
actionChangeTags(entity.id, tags),
|
||||
t('issues.fix.' + fixTextID + '.annotation')
|
||||
);
|
||||
}
|
||||
})
|
||||
]
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
return issues;
|
||||
};
|
||||
|
||||
validation.type = type;
|
||||
|
||||
return validation;
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { t } from '../util/locale';
|
||||
|
||||
|
||||
export function validationDisconnectedHighway() {
|
||||
|
||||
function isDisconnectedHighway(entity, graph) {
|
||||
if (!entity.tags.highway) return false;
|
||||
if (entity.geometry(graph) !== 'line') return false;
|
||||
|
||||
return graph.childNodes(entity)
|
||||
.every(function(vertex) {
|
||||
var parents = graph.parentWays(vertex);
|
||||
if (parents.length === 1) { // standalone vertex
|
||||
return true;
|
||||
} else { // shared vertex
|
||||
return !vertex.tags.entrance &&
|
||||
parents.filter(function(parent) {
|
||||
return parent.tags.highway && parent !== entity;
|
||||
}).length === 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var validation = function(changes, graph) {
|
||||
var warnings = [];
|
||||
for (var i = 0; i < changes.created.length; i++) {
|
||||
var entity = changes.created[i];
|
||||
|
||||
if (isDisconnectedHighway(entity, graph)) {
|
||||
warnings.push({
|
||||
id: 'disconnected_highway',
|
||||
message: t('validations.disconnected_highway'),
|
||||
tooltip: t('validations.disconnected_highway_tooltip'),
|
||||
entity: entity
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
};
|
||||
|
||||
|
||||
return validation;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { t } from '../util/locale';
|
||||
import { modeDrawLine } from '../modes';
|
||||
import { operationDelete } from '../operations/index';
|
||||
import { utilDisplayLabel } from '../util';
|
||||
import { validationIssue, validationIssueFix } from '../core/validator';
|
||||
|
||||
|
||||
export function validationDisconnectedWay() {
|
||||
var type = 'disconnected_way';
|
||||
|
||||
|
||||
function isDisconnectedHighway(entity, graph) {
|
||||
if (!entity.tags.highway) return false;
|
||||
if (entity.geometry(graph) !== 'line') return false;
|
||||
|
||||
return graph.childNodes(entity)
|
||||
.every(function(vertex) {
|
||||
var parents = graph.parentWays(vertex);
|
||||
if (parents.length === 1) { // standalone vertex
|
||||
return true;
|
||||
} else { // shared vertex
|
||||
return !vertex.tags.entrance &&
|
||||
parents.filter(function(parent) {
|
||||
return parent.tags.highway && parent !== entity;
|
||||
}).length === 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var validation = function(entity, context) {
|
||||
var issues = [];
|
||||
var graph = context.graph();
|
||||
|
||||
if (isDisconnectedHighway(entity, graph)) {
|
||||
var entityLabel = utilDisplayLabel(entity, context);
|
||||
var fixes = [];
|
||||
|
||||
if (!entity.isClosed()) {
|
||||
fixes.push(new validationIssueFix({
|
||||
icon: 'iD-operation-continue-left',
|
||||
title: t('issues.fix.continue_from_start.title'),
|
||||
entityIds: [entity.first()],
|
||||
onClick: function() {
|
||||
var vertex = context.entity(entity.first());
|
||||
continueDrawing(entity, vertex, context);
|
||||
}
|
||||
}));
|
||||
fixes.push(new validationIssueFix({
|
||||
icon: 'iD-operation-continue',
|
||||
title: t('issues.fix.continue_from_end.title'),
|
||||
entityIds: [entity.last()],
|
||||
onClick: function() {
|
||||
var vertex = context.entity(entity.last());
|
||||
continueDrawing(entity, vertex, context);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
fixes.push(new validationIssueFix({
|
||||
icon: 'iD-operation-delete',
|
||||
title: t('issues.fix.delete_feature.title'),
|
||||
entityIds: [entity.id],
|
||||
onClick: function() {
|
||||
var id = this.issue.entities[0].id;
|
||||
operationDelete([id], context)();
|
||||
}
|
||||
}));
|
||||
|
||||
issues.push(new validationIssue({
|
||||
type: type,
|
||||
severity: 'warning',
|
||||
message: t('issues.disconnected_way.highway.message', { highway: entityLabel }),
|
||||
tooltip: t('issues.disconnected_way.highway.tip'),
|
||||
entities: [entity],
|
||||
fixes: fixes
|
||||
}));
|
||||
}
|
||||
|
||||
return issues;
|
||||
|
||||
|
||||
function continueDrawing(way, vertex) {
|
||||
// make sure the vertex is actually visible and editable
|
||||
var map = context.map();
|
||||
if (!map.editable() || !map.trimmedExtent().contains(vertex.loc)) {
|
||||
map.zoomToEase(vertex);
|
||||
}
|
||||
|
||||
context.enter(
|
||||
modeDrawLine(context, way.id, context.graph(), way.affix(vertex.id), true)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
validation.type = type;
|
||||
|
||||
return validation;
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
import _clone from 'lodash-es/clone';
|
||||
import { t } from '../util/locale';
|
||||
import { utilPreset } from '../util';
|
||||
import { validationIssue, validationIssueFix } from '../core/validator';
|
||||
import { actionChangeTags } from '../actions';
|
||||
import { discardNames } from '../../node_modules/name-suggestion-index/config/filters.json';
|
||||
|
||||
|
||||
export function validationGenericName() {
|
||||
var type = 'generic_name';
|
||||
|
||||
|
||||
function isGenericName(entity) {
|
||||
var name = entity.tags.name;
|
||||
@@ -30,22 +37,39 @@ export function validationGenericName() {
|
||||
}
|
||||
|
||||
|
||||
return function validation(changes) {
|
||||
var warnings = [];
|
||||
|
||||
for (var i = 0; i < changes.created.length; i++) {
|
||||
var change = changes.created[i];
|
||||
var generic = isGenericName(change);
|
||||
if (generic) {
|
||||
warnings.push({
|
||||
id: 'generic_name',
|
||||
message: t('validations.generic_name'),
|
||||
tooltip: t('validations.generic_name_tooltip', { name: generic }),
|
||||
entity: change
|
||||
});
|
||||
}
|
||||
var validation = function(entity, context) {
|
||||
var issues = [];
|
||||
var generic = isGenericName(entity);
|
||||
if (generic) {
|
||||
var preset = utilPreset(entity, context);
|
||||
issues.push(new validationIssue({
|
||||
type: type,
|
||||
severity: 'warning',
|
||||
message: t('issues.generic_name.message', {feature: preset.name(), name: generic}),
|
||||
tooltip: t('issues.generic_name.tip'),
|
||||
entities: [entity],
|
||||
fixes: [
|
||||
new validationIssueFix({
|
||||
icon: 'iD-operation-delete',
|
||||
title: t('issues.fix.remove_generic_name.title'),
|
||||
onClick: function() {
|
||||
var entity = this.issue.entities[0];
|
||||
var tags = _clone(entity.tags);
|
||||
delete tags.name;
|
||||
context.perform(
|
||||
actionChangeTags(entity.id, tags),
|
||||
t('issues.fix.remove_generic_name.annotation')
|
||||
);
|
||||
}
|
||||
})
|
||||
]
|
||||
}));
|
||||
}
|
||||
|
||||
return warnings;
|
||||
return issues;
|
||||
};
|
||||
|
||||
validation.type = type;
|
||||
|
||||
return validation;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export { validationAlmostJunction } from './almost_junction';
|
||||
export { validationCrossingWays } from './crossing_ways';
|
||||
export { validationDeprecatedTag } from './deprecated_tag';
|
||||
export { validationDisconnectedHighway } from './disconnected_highway';
|
||||
export { validationGenericName } from './generic_name.js';
|
||||
export { validationDisconnectedWay } from './disconnected_way';
|
||||
export { validationGenericName } from './generic_name';
|
||||
export { validationManyDeletions } from './many_deletions';
|
||||
export { validationMapCSSChecks } from './mapcss_checks';
|
||||
export { validationMaprules } from './maprules';
|
||||
export { validationMissingTag } from './missing_tag';
|
||||
export { validationOldMultipolygon } from './old_multipolygon';
|
||||
export { validationTagSuggestsArea } from './tag_suggests_area';
|
||||
|
||||
@@ -1,31 +1,58 @@
|
||||
import { t } from '../util/locale';
|
||||
import { validationIssue } from '../core/validator';
|
||||
|
||||
|
||||
export function validationManyDeletions() {
|
||||
var threshold = 100;
|
||||
var totalOtherGeomThreshold = 50;
|
||||
var relationThreshold = 10; // relations are less common so use a lower threshold
|
||||
|
||||
var validation = function(changes, graph) {
|
||||
var warnings = [];
|
||||
var nodes = 0, ways = 0, areas = 0, relations = 0;
|
||||
var type = 'many_deletions';
|
||||
|
||||
changes.deleted.forEach(function(c) {
|
||||
if (c.type === 'node') { nodes++; }
|
||||
else if (c.type === 'way' && c.geometry(graph) === 'line') { ways++; }
|
||||
else if (c.type === 'way' && c.geometry(graph) === 'area') { areas++; }
|
||||
else if (c.type === 'relation') { relations++; }
|
||||
var validation = function(changes, context) {
|
||||
var issues = [];
|
||||
var points = 0, lines = 0, areas = 0, relations = 0;
|
||||
var base = context.history().base();
|
||||
var geometry;
|
||||
|
||||
changes.deleted.forEach(function(entity) {
|
||||
if (entity.type === 'node' && entity.geometry(base) === 'point') {
|
||||
points++;
|
||||
} else if (entity.type === 'way') {
|
||||
geometry = entity.geometry(base);
|
||||
if (geometry === 'line') {
|
||||
lines++;
|
||||
} else if (geometry === 'area') {
|
||||
areas++;
|
||||
}
|
||||
} else if (entity.type === 'relation') {
|
||||
relations++;
|
||||
}
|
||||
});
|
||||
if (changes.deleted.length > threshold) {
|
||||
warnings.push({
|
||||
id: 'many_deletions',
|
||||
message: t('validations.many_deletions',
|
||||
{ n: changes.deleted.length, p: nodes, l: ways, a:areas, r: relations }
|
||||
)
|
||||
});
|
||||
|
||||
if (points + lines + areas >= totalOtherGeomThreshold || relations >= relationThreshold) {
|
||||
var totalFeatures = points + lines + areas + relations;
|
||||
|
||||
var messageType = 'points-lines-areas';
|
||||
if (relations > 0) {
|
||||
messageType += '-relations';
|
||||
}
|
||||
issues.push(new validationIssue({
|
||||
type: type,
|
||||
severity: 'warning',
|
||||
message: t(
|
||||
'issues.many_deletions.'+messageType+'.message',
|
||||
{ n: totalFeatures, p: points, l: lines, a:areas, r: relations }
|
||||
),
|
||||
tooltip: t('issues.many_deletions.tip'),
|
||||
hash: [points, lines, areas, relations].join()
|
||||
}));
|
||||
}
|
||||
|
||||
return warnings;
|
||||
return issues;
|
||||
};
|
||||
|
||||
validation.type = type;
|
||||
validation.inputType = 'changes';
|
||||
|
||||
return validation;
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { services } from '../services';
|
||||
|
||||
export function validationMapCSSChecks() {
|
||||
var validation = function(changes, graph) {
|
||||
if (!services.maprules) return [];
|
||||
|
||||
var rules = services.maprules.validationRules();
|
||||
var warnings = [];
|
||||
var createdModified = ['created', 'modified'];
|
||||
|
||||
for (var i = 0; i < rules.length; i++) {
|
||||
var rule = rules[i];
|
||||
for (var j = 0; j < createdModified.length; j++) {
|
||||
var type = createdModified[j];
|
||||
var entities = changes[type];
|
||||
for (var k = 0; k < entities.length; k++) {
|
||||
rule.findWarnings(entities[k], graph, warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
};
|
||||
|
||||
|
||||
return validation;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { services } from '../services';
|
||||
|
||||
|
||||
export function validationMaprules() {
|
||||
var type = 'maprules';
|
||||
|
||||
|
||||
var validation = function(entity, context) {
|
||||
if (!services.maprules) return [];
|
||||
|
||||
var graph = context.graph();
|
||||
|
||||
var rules = services.maprules.validationRules();
|
||||
var issues = [];
|
||||
|
||||
for (var i = 0; i < rules.length; i++) {
|
||||
var rule = rules[i];
|
||||
rule.findIssues(entity, graph, issues);
|
||||
}
|
||||
|
||||
return issues;
|
||||
};
|
||||
|
||||
|
||||
validation.type = type;
|
||||
|
||||
|
||||
return validation;
|
||||
}
|
||||
@@ -1,36 +1,84 @@
|
||||
import _without from 'lodash-es/without';
|
||||
import _isEmpty from 'lodash-es/isEmpty';
|
||||
|
||||
import { operationDelete } from '../operations/index';
|
||||
import { osmIsInterestingTag } from '../osm/tags';
|
||||
import { t } from '../util/locale';
|
||||
import { utilDisplayLabel } from '../util';
|
||||
import { validationIssue, validationIssueFix } from '../core/validator';
|
||||
|
||||
|
||||
export function validationMissingTag() {
|
||||
var type = 'missing_tag';
|
||||
|
||||
// Slightly stricter check than Entity#isUsed (#3091)
|
||||
function hasTags(entity, graph) {
|
||||
return _without(Object.keys(entity.tags), 'area', 'name').length > 0 ||
|
||||
graph.parentRelations(entity).length > 0;
|
||||
|
||||
function hasDescriptiveTags(entity) {
|
||||
var keys = _without(Object.keys(entity.tags), 'area', 'name').filter(osmIsInterestingTag);
|
||||
if (entity.type === 'relation' && keys.length === 1) {
|
||||
return entity.tags.type !== 'multipolygon';
|
||||
}
|
||||
return keys.length > 0;
|
||||
}
|
||||
|
||||
var validation = function(changes, graph) {
|
||||
var types = ['point', 'line', 'area', 'relation'];
|
||||
var warnings = [];
|
||||
|
||||
for (var i = 0; i < changes.created.length; i++) {
|
||||
var change = changes.created[i];
|
||||
var geometry = change.geometry(graph);
|
||||
var validation = function(entity, context) {
|
||||
var graph = context.graph();
|
||||
|
||||
if (types.indexOf(geometry) !== -1 && !hasTags(change, graph)) {
|
||||
warnings.push({
|
||||
id: 'missing_tag',
|
||||
message: t('validations.untagged_' + geometry),
|
||||
tooltip: t('validations.untagged_' + geometry + '_tooltip'),
|
||||
entity: change
|
||||
});
|
||||
}
|
||||
// ignore vertex features and relation members
|
||||
if (entity.geometry(graph) === 'vertex' || entity.hasParentRelations(graph)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return warnings;
|
||||
var messageObj = {};
|
||||
var missingTagType;
|
||||
|
||||
if (_isEmpty(entity.tags)) {
|
||||
missingTagType = 'any';
|
||||
} else if (!hasDescriptiveTags(entity)) {
|
||||
missingTagType = 'descriptive';
|
||||
} else if (entity.type === 'relation' && !entity.tags.type) {
|
||||
missingTagType = 'specific';
|
||||
messageObj.tag = 'type';
|
||||
}
|
||||
|
||||
if (!missingTagType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
messageObj.feature = utilDisplayLabel(entity, context);
|
||||
|
||||
var issues = [];
|
||||
|
||||
issues.push(new validationIssue({
|
||||
type: type,
|
||||
// error if created or modified, else warning
|
||||
severity: !entity.version || entity.v ? 'error' : 'warning',
|
||||
message: t('issues.missing_tag.' + missingTagType + '.message', messageObj),
|
||||
tooltip: t('issues.missing_tag.tip'),
|
||||
entities: [entity],
|
||||
fixes: [
|
||||
new validationIssueFix({
|
||||
icon: 'iD-icon-search',
|
||||
title: t('issues.fix.select_preset.title'),
|
||||
onClick: function() {
|
||||
context.ui().sidebar.showPresetList();
|
||||
}
|
||||
}),
|
||||
new validationIssueFix({
|
||||
icon: 'iD-operation-delete',
|
||||
title: t('issues.fix.delete_feature.title'),
|
||||
onClick: function() {
|
||||
var id = this.issue.entities[0].id;
|
||||
operationDelete([id], context)();
|
||||
}
|
||||
})
|
||||
]
|
||||
}));
|
||||
|
||||
return issues;
|
||||
};
|
||||
|
||||
validation.type = type;
|
||||
|
||||
return validation;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,62 @@
|
||||
import { t } from '../util/locale';
|
||||
import { osmIsSimpleMultipolygonOuterMember } from '../osm';
|
||||
|
||||
import { actionChangeTags } from '../actions';
|
||||
import { osmIsOldMultipolygonOuterMember, osmOldMultipolygonOuterMemberOfRelation } from '../osm';
|
||||
import { utilDisplayLabel } from '../util';
|
||||
import { validationIssue, validationIssueFix } from '../core/validator';
|
||||
|
||||
|
||||
export function validationOldMultipolygon() {
|
||||
var type = 'old_multipolygon';
|
||||
|
||||
return function validation(changes, graph) {
|
||||
var warnings = [];
|
||||
for (var i = 0; i < changes.created.length; i++) {
|
||||
var entity = changes.created[i];
|
||||
var parent = osmIsSimpleMultipolygonOuterMember(entity, graph);
|
||||
if (parent) {
|
||||
warnings.push({
|
||||
id: 'old_multipolygon',
|
||||
message: t('validations.old_multipolygon'),
|
||||
tooltip: t('validations.old_multipolygon_tooltip'),
|
||||
entity: parent
|
||||
});
|
||||
}
|
||||
|
||||
var validation = function(entity, context) {
|
||||
var issues = [];
|
||||
var graph = context.graph();
|
||||
|
||||
var multipolygon, outerWay;
|
||||
if (entity.type === 'relation') {
|
||||
outerWay = osmOldMultipolygonOuterMemberOfRelation(entity, graph);
|
||||
multipolygon = entity;
|
||||
} else if (entity.type === 'way') {
|
||||
multipolygon = osmIsOldMultipolygonOuterMember(entity, graph);
|
||||
outerWay = entity;
|
||||
} else {
|
||||
return issues;
|
||||
}
|
||||
return warnings;
|
||||
|
||||
if (multipolygon && outerWay) {
|
||||
var multipolygonLabel = utilDisplayLabel(multipolygon, context);
|
||||
issues.push(new validationIssue({
|
||||
type: type,
|
||||
severity: 'warning',
|
||||
message: t('issues.old_multipolygon.message', { multipolygon: multipolygonLabel }),
|
||||
tooltip: t('issues.old_multipolygon.tip'),
|
||||
entities: [outerWay, multipolygon],
|
||||
fixes: [
|
||||
new validationIssueFix({
|
||||
title: t('issues.fix.move_tags.title'),
|
||||
onClick: function() {
|
||||
var outerWay = this.issue.entities[0];
|
||||
var multipolygon = this.issue.entities[1];
|
||||
context.perform(
|
||||
function(graph) {
|
||||
multipolygon = multipolygon.mergeTags(outerWay.tags);
|
||||
graph = graph.replace(multipolygon);
|
||||
graph = actionChangeTags(outerWay.id, {})(graph);
|
||||
return graph;
|
||||
},
|
||||
t('issues.fix.move_tags.annotation')
|
||||
);
|
||||
}
|
||||
})
|
||||
]
|
||||
}));
|
||||
}
|
||||
return issues;
|
||||
};
|
||||
|
||||
validation.type = type;
|
||||
|
||||
return validation;
|
||||
}
|
||||
|
||||
@@ -1,48 +1,104 @@
|
||||
import _isEmpty from 'lodash-es/isEmpty';
|
||||
import _clone from 'lodash-es/clone';
|
||||
|
||||
import { actionAddVertex, actionChangeTags, actionMergeNodes } from '../actions';
|
||||
import { geoHasSelfIntersections, geoSphericalDistance } from '../geo';
|
||||
import { t } from '../util/locale';
|
||||
import { utilDisplayLabel, utilTagText } from '../util';
|
||||
import { validationIssue, validationIssueFix } from '../core/validator';
|
||||
|
||||
|
||||
// https://github.com/openstreetmap/josm/blob/mirror/src/org/
|
||||
// openstreetmap/josm/data/validation/tests/UnclosedWays.java#L80
|
||||
export function validationTagSuggestsArea() {
|
||||
var type = 'tag_suggests_area';
|
||||
|
||||
function tagSuggestsArea(tags) {
|
||||
if (_isEmpty(tags)) return false;
|
||||
|
||||
var presence = ['landuse', 'amenities', 'tourism', 'shop'];
|
||||
for (var i = 0; i < presence.length; i++) {
|
||||
if (tags[presence[i]] !== undefined) {
|
||||
if (presence[i] === 'tourism' && tags[presence[i]] === 'artwork') {
|
||||
continue; // exception for tourism=artwork - #5206
|
||||
} else {
|
||||
return presence[i] + '=' + tags[presence[i]];
|
||||
var validation = function(entity, context) {
|
||||
if (entity.type !== 'way') return [];
|
||||
|
||||
var issues = [];
|
||||
var graph = context.graph();
|
||||
var tagSuggestingArea = entity.tagSuggestingArea();
|
||||
var tagSuggestsArea = !entity.isClosed() && tagSuggestingArea;
|
||||
|
||||
if (tagSuggestsArea) {
|
||||
var tagText = utilTagText({ tags: tagSuggestingArea });
|
||||
var fixes = [];
|
||||
var nodes = graph.childNodes(entity), testNodes;
|
||||
var firstToLastDistanceMeters = geoSphericalDistance(nodes[0].loc, nodes[nodes.length-1].loc);
|
||||
var connectEndpointsOnClick;
|
||||
|
||||
// if the distance is very small, attempt to merge the endpoints
|
||||
if (firstToLastDistanceMeters < 0.75) {
|
||||
testNodes = _clone(nodes);
|
||||
testNodes.pop();
|
||||
testNodes.push(testNodes[0]);
|
||||
// make sure this will not create a self-intersection
|
||||
if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {
|
||||
connectEndpointsOnClick = function() {
|
||||
var way = this.issue.entities[0];
|
||||
context.perform(
|
||||
actionMergeNodes([way.nodes[0], way.nodes[way.nodes.length-1]], nodes[0].loc),
|
||||
t('issues.fix.connect_endpoints.annotation')
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tags.building && tags.building === 'yes') return 'building=yes';
|
||||
}
|
||||
|
||||
|
||||
var validation = function(changes, graph) {
|
||||
var warnings = [];
|
||||
for (var i = 0; i < changes.created.length; i++) {
|
||||
var change = changes.created[i];
|
||||
var geometry = change.geometry(graph);
|
||||
var suggestion = (geometry === 'line' ? tagSuggestsArea(change.tags) : undefined);
|
||||
|
||||
if (suggestion) {
|
||||
warnings.push({
|
||||
id: 'tag_suggests_area',
|
||||
message: t('validations.tag_suggests_area', { tag: suggestion }),
|
||||
entity: change
|
||||
});
|
||||
if (!connectEndpointsOnClick) {
|
||||
// if the points were not merged, attempt to close the way
|
||||
testNodes = _clone(nodes);
|
||||
testNodes.push(testNodes[0]);
|
||||
// make sure this will not create a self-intersection
|
||||
if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {
|
||||
connectEndpointsOnClick = function() {
|
||||
var way = this.issue.entities[0];
|
||||
var nodeId = way.nodes[0];
|
||||
var index = way.nodes.length;
|
||||
context.perform(
|
||||
actionAddVertex(way.id, nodeId, index),
|
||||
t('issues.fix.connect_endpoints.annotation')
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (connectEndpointsOnClick) {
|
||||
fixes.push(new validationIssueFix({
|
||||
title: t('issues.fix.connect_endpoints.title'),
|
||||
onClick: connectEndpointsOnClick
|
||||
}));
|
||||
}
|
||||
|
||||
fixes.push(new validationIssueFix({
|
||||
icon: 'iD-operation-delete',
|
||||
title: t('issues.fix.remove_tag.title'),
|
||||
onClick: function() {
|
||||
var entity = this.issue.entities[0];
|
||||
var tags = _clone(entity.tags);
|
||||
for (var key in tagSuggestingArea) {
|
||||
delete tags[key];
|
||||
}
|
||||
context.perform(
|
||||
actionChangeTags(entity.id, tags),
|
||||
t('issues.fix.remove_tag.annotation')
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
var featureLabel = utilDisplayLabel(entity, context);
|
||||
issues.push(new validationIssue({
|
||||
type: type,
|
||||
severity: 'warning',
|
||||
message: t('issues.tag_suggests_area.message', { feature: featureLabel, tag: tagText }),
|
||||
tooltip: t('issues.tag_suggests_area.tip'),
|
||||
entities: [entity],
|
||||
fixes: fixes
|
||||
}));
|
||||
}
|
||||
|
||||
return warnings;
|
||||
return issues;
|
||||
};
|
||||
|
||||
validation.type = type;
|
||||
|
||||
return validation;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M6.89339828,2.5 L13.1066017,2.5 L17.5,6.89339828 L17.5,13.1066017 L13.1066017,17.5 L6.89339828,17.5 L2.5,13.1066017 L2.5,6.89339828 L6.89339828,2.5 Z M10.933,12.916 C10.933,12.828 10.908,12.755 10.857,12.696 C10.808,12.639 10.737,12.61 10.647,12.61 L9.347,12.61 C9.267,12.61 9.199,12.637 9.139,12.693 C9.079,12.748 9.05,12.823 9.05,12.916 L9.05,14.137 C9.05,14.225 9.079,14.296 9.139,14.348 C9.199,14.4 9.267,14.426 9.347,14.426 L10.647,14.426 C10.843,14.426 10.94,14.329 10.933,14.137 L10.933,12.916 Z M10.572,11.832 C10.65,11.832 10.719,11.802 10.779,11.745 C10.837,11.686 10.867,11.619 10.867,11.542 L10.994,6.289 C10.994,6.211 10.964,6.144 10.904,6.087 C10.846,6.028 10.777,6 10.697,6 L9.295,6 C9.207,6 9.132,6.028 9.08,6.087 C9.027,6.144 9,6.211 9,6.289 L9.103,11.542 C9.103,11.619 9.132,11.686 9.191,11.745 C9.249,11.802 9.318,11.832 9.397,11.832 L10.572,11.832 Z" fill="currentColor"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M10.6879121,7.42604898 C10.2489814,6.19270935 10.5231466,4.76238732 11.5104076,3.77512627 C12.8772427,2.40829124 15.0933201,2.40829124 16.4601551,3.77512627 C16.6542987,3.96926984 16.8208663,4.1805469 16.9598581,4.4040902 L14.1066017,5.11740429 L14.1066017,7.11740429 L17.0920951,7.86377764 C16.9309697,8.17376943 16.720323,8.46470585 16.4601551,8.72487373 C15.4728941,9.71213479 14.042572,9.98629999 12.8092324,9.54736932 L7.9750032,14.3815985 C8.08186131,15.1334839 7.84611589,15.9246994 7.26776695,16.5030483 C6.29145622,17.4793591 4.70854378,17.4793591 3.73223305,16.5030483 C2.75592232,15.5267376 2.75592232,13.9438252 3.73223305,12.9675144 C4.31058199,12.3891655 5.10179747,12.1534201 5.85368286,12.2602782 L10.6879121,7.42604898 Z M6.20710678,15.4423882 C6.59763107,15.0518639 6.59763107,14.4186989 6.20710678,14.0281746 C5.81658249,13.6376503 5.18341751,13.6376503 4.79289322,14.0281746 C4.40236893,14.4186989 4.40236893,15.0518639 4.79289322,15.4423882 C5.18341751,15.8329124 5.81658249,15.8329124 6.20710678,15.4423882 Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M6,4 C9.314,4 12,6.686 12,10 C12,13.314 9.314,16 6,16 C2.686,16 0,13.314 0,10 C0,6.686 2.686,4 6,4 Z M7,6 L5,6 L5,9 L2,9 L2,11 L5,11 L5,14 L7,14 L7,11 L10,11 L10,9 L7,9 L7,6 Z M15,9 L15,11 L13,11 L13,9 L15,9 Z M18,8 C19.105,8 20,8.895 20,10 C20,11.105 19.105,12 18,12 C16.895,12 16,11.105 16,10 C16,8.895 16.895,8 18,8 Z" fill="inherit"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 543 B |
@@ -144,6 +144,14 @@
|
||||
<script src='spec/util/session_mutex.js'></script>
|
||||
<script src='spec/util/util.js'></script>
|
||||
|
||||
<script src='spec/validations/validator.js'></script>
|
||||
<script src='spec/validations/deprecated_tag.js'></script>
|
||||
<script src='spec/validations/missing_tag.js'></script>
|
||||
<script src='spec/validations/disconnected_way.js'></script>
|
||||
<script src='spec/validations/tag_suggests_area.js'></script>
|
||||
<script src='spec/validations/crossing_ways.js'></script>
|
||||
<script src='spec/validations/almost_junction.js'></script>
|
||||
|
||||
<script src='spec/operations/detach_node.js'></script>
|
||||
<script>
|
||||
window.mocha.run();
|
||||
|
||||
@@ -223,11 +223,14 @@ describe('iD.osmEntity', function () {
|
||||
|
||||
describe('#hasDeprecatedTags', function () {
|
||||
it('returns false if entity has no tags', function () {
|
||||
expect(iD.Entity().deprecatedTags()).to.eql({});
|
||||
expect(iD.Entity().deprecatedTags()).to.eql([]);
|
||||
});
|
||||
|
||||
it('returns true if entity has deprecated tags', function () {
|
||||
expect(iD.Entity({ tags: { barrier: 'wire_fence' } }).deprecatedTags()).to.eql({ barrier: 'wire_fence' });
|
||||
expect(iD.Entity({ tags: { amenity: 'swimming_pool' } }).deprecatedTags()).to.eql([{
|
||||
old: { amenity: 'swimming_pool' },
|
||||
replace: { leisure: 'swimming_pool' }
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
describe('iD.osmIsSimpleMultipolygonOuterMember', function() {
|
||||
describe('iD.osmIsOldMultipolygonOuterMember', function() {
|
||||
it('returns the parent relation of a simple multipolygon outer', function() {
|
||||
var outer = iD.osmWay({tags: {'natural':'wood'}});
|
||||
var relation = iD.osmRelation(
|
||||
{tags: {type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]}
|
||||
);
|
||||
var graph = iD.coreGraph([outer, relation]);
|
||||
expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.equal(relation);
|
||||
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.equal(relation);
|
||||
});
|
||||
|
||||
it('returns the parent relation of a simple multipolygon outer, assuming role outer if unspecified', function() {
|
||||
@@ -14,7 +14,7 @@ describe('iD.osmIsSimpleMultipolygonOuterMember', function() {
|
||||
{tags: {type: 'multipolygon'}, members: [{id: outer.id}]}
|
||||
);
|
||||
var graph = iD.coreGraph([outer, relation]);
|
||||
expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.equal(relation);
|
||||
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.equal(relation);
|
||||
});
|
||||
|
||||
it('returns false if entity is not a way', function() {
|
||||
@@ -23,7 +23,7 @@ describe('iD.osmIsSimpleMultipolygonOuterMember', function() {
|
||||
{tags: {type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]}
|
||||
);
|
||||
var graph = iD.coreGraph([outer, relation]);
|
||||
expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.be.false;
|
||||
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.be.false;
|
||||
});
|
||||
|
||||
it('returns false if entity does not have interesting tags', function() {
|
||||
@@ -32,13 +32,13 @@ describe('iD.osmIsSimpleMultipolygonOuterMember', function() {
|
||||
{tags: {type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]}
|
||||
);
|
||||
var graph = iD.coreGraph([outer, relation]);
|
||||
expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.be.false;
|
||||
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.be.false;
|
||||
});
|
||||
|
||||
it('returns false if entity does not have a parent relation', function() {
|
||||
var outer = iD.osmWay({tags: {'natural':'wood'}});
|
||||
var graph = iD.coreGraph([outer]);
|
||||
expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.be.false;
|
||||
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.be.false;
|
||||
});
|
||||
|
||||
it('returns false if the parent is not a multipolygon', function() {
|
||||
@@ -47,7 +47,7 @@ describe('iD.osmIsSimpleMultipolygonOuterMember', function() {
|
||||
{tags: {type: 'route'}, members: [{id: outer.id, role: 'outer'}]}
|
||||
);
|
||||
var graph = iD.coreGraph([outer, relation]);
|
||||
expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.be.false;
|
||||
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.be.false;
|
||||
});
|
||||
|
||||
it('returns false if the parent has interesting tags', function() {
|
||||
@@ -56,7 +56,7 @@ describe('iD.osmIsSimpleMultipolygonOuterMember', function() {
|
||||
{tags: {natural: 'wood', type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]}
|
||||
);
|
||||
var graph = iD.coreGraph([outer, relation]);
|
||||
expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.be.false;
|
||||
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.be.false;
|
||||
});
|
||||
|
||||
it('returns the parent relation of a simple multipolygon outer, ignoring uninteresting parent tags', function() {
|
||||
@@ -65,7 +65,7 @@ describe('iD.osmIsSimpleMultipolygonOuterMember', function() {
|
||||
{tags: {'tiger:reviewed':'no', type: 'multipolygon'}, members: [{id: outer.id, role: 'outer'}]}
|
||||
);
|
||||
var graph = iD.coreGraph([outer, relation]);
|
||||
expect(iD.osmIsSimpleMultipolygonOuterMember(outer, graph)).to.equal(relation);
|
||||
expect(iD.osmIsOldMultipolygonOuterMember(outer, graph)).to.equal(relation);
|
||||
});
|
||||
|
||||
it('returns false if the parent has multiple outer ways', function() {
|
||||
@@ -75,8 +75,8 @@ describe('iD.osmIsSimpleMultipolygonOuterMember', function() {
|
||||
{tags: {type: 'multipolygon'}, members: [{id: outer1.id, role: 'outer'}, {id: outer2.id, role: 'outer'}]}
|
||||
);
|
||||
var graph = iD.coreGraph([outer1, outer2, relation]);
|
||||
expect(iD.osmIsSimpleMultipolygonOuterMember(outer1, graph)).to.be.false;
|
||||
expect(iD.osmIsSimpleMultipolygonOuterMember(outer2, graph)).to.be.false;
|
||||
expect(iD.osmIsOldMultipolygonOuterMember(outer1, graph)).to.be.false;
|
||||
expect(iD.osmIsOldMultipolygonOuterMember(outer2, graph)).to.be.false;
|
||||
});
|
||||
|
||||
it('returns false if the parent has multiple outer ways, assuming role outer if unspecified', function() {
|
||||
@@ -86,8 +86,8 @@ describe('iD.osmIsSimpleMultipolygonOuterMember', function() {
|
||||
{tags: {type: 'multipolygon'}, members: [{id: outer1.id}, {id: outer2.id}]}
|
||||
);
|
||||
var graph = iD.coreGraph([outer1, outer2, relation]);
|
||||
expect(iD.osmIsSimpleMultipolygonOuterMember(outer1, graph)).to.be.false;
|
||||
expect(iD.osmIsSimpleMultipolygonOuterMember(outer2, graph)).to.be.false;
|
||||
expect(iD.osmIsOldMultipolygonOuterMember(outer1, graph)).to.be.false;
|
||||
expect(iD.osmIsOldMultipolygonOuterMember(outer2, graph)).to.be.false;
|
||||
});
|
||||
|
||||
it('returns false if the entity is not an outer', function() {
|
||||
@@ -96,12 +96,12 @@ describe('iD.osmIsSimpleMultipolygonOuterMember', function() {
|
||||
{tags: {type: 'multipolygon'}, members: [{id: inner.id, role: 'inner'}]}
|
||||
);
|
||||
var graph = iD.coreGraph([inner, relation]);
|
||||
expect(iD.osmIsSimpleMultipolygonOuterMember(inner, graph)).to.be.false;
|
||||
expect(iD.osmIsOldMultipolygonOuterMember(inner, graph)).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('iD.osmSimpleMultipolygonOuterMember', function() {
|
||||
describe('iD.osmOldMultipolygonOuterMember', function() {
|
||||
it('returns the outer member of a simple multipolygon', function() {
|
||||
var inner = iD.osmWay();
|
||||
var outer = iD.osmWay({tags: {'natural':'wood'}});
|
||||
@@ -111,8 +111,8 @@ describe('iD.osmSimpleMultipolygonOuterMember', function() {
|
||||
});
|
||||
var graph = iD.coreGraph([inner, outer, relation]);
|
||||
|
||||
expect(iD.osmSimpleMultipolygonOuterMember(inner, graph)).to.equal(outer);
|
||||
expect(iD.osmSimpleMultipolygonOuterMember(outer, graph)).to.equal(outer);
|
||||
expect(iD.osmOldMultipolygonOuterMember(inner, graph)).to.equal(outer);
|
||||
expect(iD.osmOldMultipolygonOuterMember(outer, graph)).to.equal(outer);
|
||||
});
|
||||
|
||||
it('returns falsy for a complex multipolygon', function() {
|
||||
@@ -126,9 +126,9 @@ describe('iD.osmSimpleMultipolygonOuterMember', function() {
|
||||
});
|
||||
var graph = iD.coreGraph([inner, outer1, outer2, relation]);
|
||||
|
||||
expect(iD.osmSimpleMultipolygonOuterMember(inner, graph)).not.to.be.ok;
|
||||
expect(iD.osmSimpleMultipolygonOuterMember(outer1, graph)).not.to.be.ok;
|
||||
expect(iD.osmSimpleMultipolygonOuterMember(outer2, graph)).not.to.be.ok;
|
||||
expect(iD.osmOldMultipolygonOuterMember(inner, graph)).not.to.be.ok;
|
||||
expect(iD.osmOldMultipolygonOuterMember(outer1, graph)).not.to.be.ok;
|
||||
expect(iD.osmOldMultipolygonOuterMember(outer2, graph)).not.to.be.ok;
|
||||
});
|
||||
|
||||
it('handles incomplete relations', function() {
|
||||
@@ -139,7 +139,7 @@ describe('iD.osmSimpleMultipolygonOuterMember', function() {
|
||||
});
|
||||
var graph = iD.coreGraph([way, relation]);
|
||||
|
||||
expect(iD.osmSimpleMultipolygonOuterMember(way, graph)).not.to.be.ok;
|
||||
expect(iD.osmOldMultipolygonOuterMember(way, graph)).not.to.be.ok;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ describe('maprules', function() {
|
||||
expect(rule.matches(entity)).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('#findWarnings', function() {
|
||||
describe('#findIssues', function() {
|
||||
var selectors, entities, _graph;
|
||||
|
||||
before(function() {
|
||||
@@ -549,24 +549,23 @@ describe('maprules', function() {
|
||||
selectors.forEach(function(selector) { iD.serviceMapRules.addRule(selector); });
|
||||
validationRules = iD.serviceMapRules.validationRules();
|
||||
});
|
||||
it('finds warnings', function() {
|
||||
it('finds issues', function() {
|
||||
validationRules.forEach(function(rule, i) {
|
||||
var warnings = [];
|
||||
var issues = [];
|
||||
var entity = entities[i];
|
||||
var selector = selectors[i];
|
||||
|
||||
rule.findWarnings(entity, _graph, warnings);
|
||||
rule.findIssues(entity, _graph, issues);
|
||||
|
||||
var warning = warnings[0];
|
||||
var issue = issues[0];
|
||||
var type = Object.keys(selector).indexOf('error') ? 'error' : 'warning';
|
||||
|
||||
expect(warnings.length).to.eql(1);
|
||||
expect(warning.entity).to.eql(entity);
|
||||
expect(warning.message).to.eql(selector[type]);
|
||||
expect(type).to.eql(warning.severity);
|
||||
expect(issues.length).to.eql(1);
|
||||
expect(issue.entities).to.eql([entity]);
|
||||
expect(issue.message).to.eql(selector[type]);
|
||||
expect(type).to.eql(issue.severity);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
describe('iD.validations.almost_junction', function () {
|
||||
var context;
|
||||
|
||||
beforeEach(function() {
|
||||
context = iD.coreContext();
|
||||
});
|
||||
|
||||
function horizontalVertialCloserThanThd() {
|
||||
// horizontal road
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [22.42357, 0]});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [22.42367, 0]});
|
||||
var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: { highway: 'residential' }});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(w1)
|
||||
);
|
||||
|
||||
// vertical road to the west of w1 by 0.00001 logitude degree
|
||||
// 5th digit after decimal point has a resolution of ~1 meter
|
||||
var n3 = iD.osmNode({id: 'n-3', loc: [22.42356, 0.001]});
|
||||
var n4 = iD.osmNode({id: 'n-4', loc: [22.42356, -0.001]});
|
||||
var w2 = iD.osmWay({id: 'w-2', nodes: ['n-3', 'n-4'], tags: { highway: 'residential' }});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n3),
|
||||
iD.actionAddEntity(n4),
|
||||
iD.actionAddEntity(w2)
|
||||
);
|
||||
}
|
||||
|
||||
function horizontalTiltedCloserThanThd() {
|
||||
// horizontal road
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [22.42357, 0]});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [22.42367, 0]});
|
||||
var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: { highway: 'residential' }});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(w1)
|
||||
);
|
||||
|
||||
// tilted road to the west of w1 by 0.00001 logitude degree
|
||||
var n3 = iD.osmNode({id: 'n-3', loc: [22.423555, 0.001]});
|
||||
var n4 = iD.osmNode({id: 'n-4', loc: [22.423565, -0.001]});
|
||||
var w2 = iD.osmWay({id: 'w-2', nodes: ['n-3', 'n-4'], tags: { highway: 'residential' }});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n3),
|
||||
iD.actionAddEntity(n4),
|
||||
iD.actionAddEntity(w2)
|
||||
);
|
||||
}
|
||||
|
||||
function horizontalVertialFurtherThanThd() {
|
||||
// horizontal road
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [22.42357, 0]});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [22.42367, 0]});
|
||||
var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: { highway: 'residential' }});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(w1)
|
||||
);
|
||||
|
||||
// vertical road to the west of w1 by 0.00007 logitude degree
|
||||
var n3 = iD.osmNode({id: 'n-3', loc: [22.42350, 0.001]});
|
||||
var n4 = iD.osmNode({id: 'n-4', loc: [22.42350, -0.001]});
|
||||
var w2 = iD.osmWay({id: 'w-2', nodes: ['n-3', 'n-4'], tags: { highway: 'residential' }});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n3),
|
||||
iD.actionAddEntity(n4),
|
||||
iD.actionAddEntity(w2)
|
||||
);
|
||||
}
|
||||
|
||||
function twoHorizontalCloserThanThd() {
|
||||
// horizontal road
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [22.42357, 0]});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [22.42367, 0]});
|
||||
var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: { highway: 'residential' }});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(w1)
|
||||
);
|
||||
|
||||
// another horizontal road to the north of w1 by 0.0001 latitude degree
|
||||
var n3 = iD.osmNode({id: 'n-3', loc: [22.42357, 0.00001]});
|
||||
var n4 = iD.osmNode({id: 'n-4', loc: [22.42367, 0.00001]});
|
||||
var w2 = iD.osmWay({id: 'w-2', nodes: ['n-3', 'n-4'], tags: { highway: 'residential' }});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n3),
|
||||
iD.actionAddEntity(n4),
|
||||
iD.actionAddEntity(w2)
|
||||
);
|
||||
}
|
||||
|
||||
function horizontalVertialWithNoExit() {
|
||||
// horizontal road
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [22.42357, 0], tags: { noexit: 'yes' }});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [22.42367, 0]});
|
||||
var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: { highway: 'residential' }});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(w1)
|
||||
);
|
||||
|
||||
// vertical road to the west of w1 by 0.00001 logitude degree
|
||||
var n3 = iD.osmNode({id: 'n-3', loc: [22.42356, 0.001]});
|
||||
var n4 = iD.osmNode({id: 'n-4', loc: [22.42356, -0.001]});
|
||||
var w2 = iD.osmWay({id: 'w-2', nodes: ['n-3', 'n-4'], tags: { highway: 'residential' }});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n3),
|
||||
iD.actionAddEntity(n4),
|
||||
iD.actionAddEntity(w2)
|
||||
);
|
||||
}
|
||||
|
||||
function validate() {
|
||||
var validator = iD.validationAlmostJunction();
|
||||
var changes = context.history().changes();
|
||||
var entities = changes.modified.concat(changes.created);
|
||||
var issues = [];
|
||||
entities.forEach(function(entity) {
|
||||
issues = issues.concat(validator(entity, context));
|
||||
});
|
||||
return issues;
|
||||
}
|
||||
|
||||
it('has no errors on init', function() {
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('horizontal and vertical road, closer than threshold', function() {
|
||||
horizontalVertialCloserThanThd();
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(1);
|
||||
var issue = issues[0];
|
||||
expect(issue.type).to.eql('almost_junction');
|
||||
expect(issue.entities).to.have.lengthOf(3);
|
||||
expect(issue.entities[0].id).to.eql('w-1');
|
||||
expect(issue.entities[1].id).to.eql('n-1');
|
||||
expect(issue.entities[2].id).to.eql('w-2');
|
||||
|
||||
expect(issue.loc).to.have.lengthOf(2);
|
||||
expect(issue.loc[0]).to.eql(22.42357);
|
||||
expect(issue.loc[1]).to.eql(0);
|
||||
|
||||
expect(issue.info.edge).to.have.lengthOf(2);
|
||||
expect(issue.info.edge[0]).to.eql('n-3');
|
||||
expect(issue.info.edge[1]).to.eql('n-4');
|
||||
|
||||
expect(issue.info.cross_loc).to.have.lengthOf(2);
|
||||
expect(issue.info.cross_loc[0]).to.eql(22.42356);
|
||||
expect(issue.info.cross_loc[1]).to.eql(0);
|
||||
|
||||
expect(issue.fixes).to.have.lengthOf(2);
|
||||
issue.fixes[0].onClick();
|
||||
issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('horizontal and tilted road, closer than threshold', function() {
|
||||
horizontalTiltedCloserThanThd();
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(1);
|
||||
var issue = issues[0];
|
||||
expect(issue.type).to.eql('almost_junction');
|
||||
expect(issue.entities).to.have.lengthOf(3);
|
||||
expect(issue.entities[0].id).to.eql('w-1');
|
||||
expect(issue.entities[1].id).to.eql('n-1');
|
||||
expect(issue.entities[2].id).to.eql('w-2');
|
||||
|
||||
expect(issue.loc).to.have.lengthOf(2);
|
||||
expect(issue.loc[0]).to.eql(22.42357);
|
||||
expect(issue.loc[1]).to.eql(0);
|
||||
|
||||
expect(issue.info.edge).to.have.lengthOf(2);
|
||||
expect(issue.info.edge[0]).to.eql('n-3');
|
||||
expect(issue.info.edge[1]).to.eql('n-4');
|
||||
|
||||
expect(issue.info.cross_loc).to.have.lengthOf(2);
|
||||
expect(issue.info.cross_loc[0]).to.eql(22.42356);
|
||||
expect(issue.info.cross_loc[1]).to.eql(0);
|
||||
|
||||
expect(issue.fixes).to.have.lengthOf(2);
|
||||
issue.fixes[1].onClick();
|
||||
issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('horizontal and vertical road, further than threshold', function() {
|
||||
horizontalVertialFurtherThanThd();
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('horizontal and vertical road, closer than threshold but with noexit tag', function() {
|
||||
horizontalVertialWithNoExit();
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('two horizontal roads, closer than threshold', function() {
|
||||
twoHorizontalCloserThanThd();
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,250 @@
|
||||
describe('iD.validations.crossing_ways', function () {
|
||||
var context;
|
||||
|
||||
beforeEach(function() {
|
||||
context = iD.coreContext();
|
||||
});
|
||||
|
||||
function createWaysWithOneCrossingPoint(tags1, tags2) {
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [1,1]});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [2,2]});
|
||||
var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: tags1});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(w1)
|
||||
);
|
||||
|
||||
var n3 = iD.osmNode({id: 'n-3', loc: [1,2]});
|
||||
var n4 = iD.osmNode({id: 'n-4', loc: [2,1]});
|
||||
var w2 = iD.osmWay({id: 'w-2', nodes: ['n-3', 'n-4'], tags: tags2});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n3),
|
||||
iD.actionAddEntity(n4),
|
||||
iD.actionAddEntity(w2)
|
||||
);
|
||||
}
|
||||
|
||||
function createWaysWithTwoCrossingPoint() {
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [1,1]});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [3,3]});
|
||||
var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: { highway: 'residential' }});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(w1)
|
||||
);
|
||||
|
||||
var n3 = iD.osmNode({id: 'n-3', loc: [1,2]});
|
||||
var n4 = iD.osmNode({id: 'n-4', loc: [2,1]});
|
||||
var n5 = iD.osmNode({id: 'n-5', loc: [3,2]});
|
||||
var n6 = iD.osmNode({id: 'n-6', loc: [2,3]});
|
||||
var w2 = iD.osmWay({id: 'w-2', nodes: ['n-3', 'n-4', 'n-5', 'n-6'], tags: { highway: 'residential' }});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n3),
|
||||
iD.actionAddEntity(n4),
|
||||
iD.actionAddEntity(n5),
|
||||
iD.actionAddEntity(n6),
|
||||
iD.actionAddEntity(w2)
|
||||
);
|
||||
}
|
||||
|
||||
function validate() {
|
||||
var validator = iD.validationCrossingWays();
|
||||
var changes = context.history().changes();
|
||||
var entities = changes.modified.concat(changes.created);
|
||||
var issues = [];
|
||||
entities.forEach(function(entity) {
|
||||
issues = issues.concat(validator(entity, context));
|
||||
});
|
||||
return issues;
|
||||
}
|
||||
|
||||
function verifySingleCrossingIssue(issues) {
|
||||
var issue = issues[0];
|
||||
expect(issue.type).to.eql('crossing_ways');
|
||||
expect(issue.entities).to.have.lengthOf(2);
|
||||
|
||||
expect(issue.loc).to.have.lengthOf(2);
|
||||
expect(issue.loc[0]).to.eql(1.5);
|
||||
expect(issue.loc[1]).to.eql(1.5);
|
||||
}
|
||||
|
||||
it('has no errors on init', function() {
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
// legit crossing cases
|
||||
it('legit crossing between highway and highway', function() {
|
||||
createWaysWithOneCrossingPoint({ highway: 'residential', tunnel: 'yes', layer: '-1' }, { highway: 'residential' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('legit crossing between highway and railway', function() {
|
||||
createWaysWithOneCrossingPoint({ highway: 'residential' }, { railway: 'rail', bridge: 'yes' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('legit crossing between highway and waterway', function() {
|
||||
createWaysWithOneCrossingPoint({ highway: 'residential', bridge: 'yes' }, { waterway: 'river' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('legit crossing between highway and building', function() {
|
||||
createWaysWithOneCrossingPoint({ highway: 'residential', covered: 'yes' }, { building: 'yes' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('legit crossing between railway and railway', function() {
|
||||
createWaysWithOneCrossingPoint({ railway: 'rail', layer: '1' }, { railway: 'rail' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('legit crossing between railway and waterway', function() {
|
||||
createWaysWithOneCrossingPoint({ railway: 'rail' }, { waterway: 'river', tunnel: 'yes' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('legit crossing between railway and building', function() {
|
||||
createWaysWithOneCrossingPoint({ railway: 'rail', covered: 'yes' }, { building: 'yes' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('legit crossing between waterway and waterway', function() {
|
||||
createWaysWithOneCrossingPoint({ waterway: 'canal', tunnel: 'yes' }, { waterway: 'river' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('legit crossing between waterway and building', function() {
|
||||
createWaysWithOneCrossingPoint({ waterway: 'river', covered: 'yes' }, { building: 'yes' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('legit crossing between building and building', function() {
|
||||
createWaysWithOneCrossingPoint({ building: 'yes' }, { building: 'yes', covered: 'yes' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
// warning crossing cases between ways
|
||||
it('one cross point between highway and highway', function() {
|
||||
createWaysWithOneCrossingPoint({ highway: 'residential' }, { highway: 'residential' });
|
||||
verifySingleCrossingIssue(validate(), 'w-2');
|
||||
});
|
||||
|
||||
it('one cross point between highway and railway', function() {
|
||||
createWaysWithOneCrossingPoint({ highway: 'residential' }, { railway: 'rail' });
|
||||
verifySingleCrossingIssue(validate(), 'w-2');
|
||||
});
|
||||
|
||||
it('one cross point between highway and waterway', function() {
|
||||
createWaysWithOneCrossingPoint({ highway: 'residential' }, { waterway: 'river' });
|
||||
verifySingleCrossingIssue(validate(), 'w-2');
|
||||
});
|
||||
|
||||
it('one cross point between highway and building', function() {
|
||||
createWaysWithOneCrossingPoint({ highway: 'residential' }, { building: 'yes' });
|
||||
verifySingleCrossingIssue(validate(), 'w-2');
|
||||
});
|
||||
|
||||
it('one cross point between railway and railway', function() {
|
||||
createWaysWithOneCrossingPoint({ railway: 'rail' }, { railway: 'rail' });
|
||||
verifySingleCrossingIssue(validate(), 'w-2');
|
||||
});
|
||||
|
||||
it('one cross point between railway and waterway', function() {
|
||||
createWaysWithOneCrossingPoint({ railway: 'rail' }, { waterway: 'river' });
|
||||
verifySingleCrossingIssue(validate(), 'w-2');
|
||||
});
|
||||
|
||||
it('one cross point between railway and building', function() {
|
||||
createWaysWithOneCrossingPoint({ railway: 'rail' }, { building: 'yes' });
|
||||
verifySingleCrossingIssue(validate(), 'w-2');
|
||||
});
|
||||
|
||||
it('one cross point between waterway and waterway', function() {
|
||||
createWaysWithOneCrossingPoint({ waterway: 'canal' }, { waterway: 'river' });
|
||||
verifySingleCrossingIssue(validate(), 'w-2');
|
||||
});
|
||||
|
||||
it('one cross point between waterway and building', function() {
|
||||
createWaysWithOneCrossingPoint({ waterway: 'river' }, { building: 'yes' });
|
||||
verifySingleCrossingIssue(validate(), 'w-2');
|
||||
});
|
||||
|
||||
it('one cross point between building and building', function() {
|
||||
createWaysWithOneCrossingPoint({ building: 'yes' }, { building: 'yes' });
|
||||
verifySingleCrossingIssue(validate(), 'w-2');
|
||||
});
|
||||
|
||||
it('two cross points between two highways', function() {
|
||||
createWaysWithTwoCrossingPoint();
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(4);
|
||||
var issue = issues[0];
|
||||
expect(issue.type).to.eql('crossing_ways');
|
||||
expect(issue.entities).to.have.lengthOf(2);
|
||||
|
||||
expect(issue.loc).to.have.lengthOf(2);
|
||||
expect(issue.loc[0]).to.eql(1.5);
|
||||
expect(issue.loc[1]).to.eql(1.5);
|
||||
|
||||
issue = issues[1];
|
||||
expect(issue.type).to.eql('crossing_ways');
|
||||
expect(issue.entities).to.have.lengthOf(2);
|
||||
|
||||
expect(issue.loc).to.have.lengthOf(2);
|
||||
expect(issue.loc[0]).to.eql(2.5);
|
||||
expect(issue.loc[1]).to.eql(2.5);
|
||||
});
|
||||
|
||||
function createWayAndRelationWithOneCrossingPoint(wayTags, relTags) {
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [1,1]});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [2,2]});
|
||||
var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: wayTags});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(w1)
|
||||
);
|
||||
|
||||
var n3 = iD.osmNode({id: 'n-3', loc: [1,2]});
|
||||
var n4 = iD.osmNode({id: 'n-4', loc: [2,1]});
|
||||
var n5 = iD.osmNode({id: 'n-5', loc: [3,2]});
|
||||
var n6 = iD.osmNode({id: 'n-6', loc: [2,3]});
|
||||
var w2 = iD.osmWay({id: 'w-2', nodes: ['n-3', 'n-4', 'n-5'], tags: {}});
|
||||
var w3 = iD.osmWay({id: 'w-3', nodes: ['n-5', 'n-6', 'n-3'], tags: {}});
|
||||
var r1 = iD.osmRelation({id: 'r-1', members: [{id: 'w-2'}, {id: 'w-3'}], tags: relTags});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n3),
|
||||
iD.actionAddEntity(n4),
|
||||
iD.actionAddEntity(n5),
|
||||
iD.actionAddEntity(n6),
|
||||
iD.actionAddEntity(w2),
|
||||
iD.actionAddEntity(w3),
|
||||
iD.actionAddEntity(r1)
|
||||
);
|
||||
}
|
||||
|
||||
it('one cross point between highway and building relation', function() {
|
||||
createWayAndRelationWithOneCrossingPoint({ highway: 'residential' }, { building: 'yes' });
|
||||
verifySingleCrossingIssue(validate(), 'r-1');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
describe('iD.validations.deprecated_tag', function () {
|
||||
var context;
|
||||
|
||||
beforeEach(function() {
|
||||
context = iD.coreContext();
|
||||
});
|
||||
|
||||
function createWay(tags) {
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [4,4]});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [4,5]});
|
||||
var w = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: tags});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(w)
|
||||
);
|
||||
}
|
||||
|
||||
function validate() {
|
||||
var validator = iD.validationDeprecatedTag();
|
||||
var changes = context.history().changes();
|
||||
var entities = changes.modified.concat(changes.created);
|
||||
var issues = [];
|
||||
entities.forEach(function(entity) {
|
||||
issues = issues.concat(validator(entity, context));
|
||||
});
|
||||
return issues;
|
||||
}
|
||||
|
||||
it('has no errors on init', function() {
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('has no errors on good tags', function() {
|
||||
createWay({'highway': 'unclassified'});
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('finds deprecated tags', function() {
|
||||
createWay({'highway': 'ford'});
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(1);
|
||||
var issue = issues[0];
|
||||
expect(issue.type).to.eql('deprecated_tag');
|
||||
expect(issue.severity).to.eql('warning');
|
||||
expect(issue.entities).to.have.lengthOf(1);
|
||||
expect(issue.entities[0].id).to.eql('w-1');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
describe('iD.validations.disconnected_way', function () {
|
||||
var context;
|
||||
|
||||
beforeEach(function() {
|
||||
context = iD.coreContext();
|
||||
});
|
||||
|
||||
function createWay(tags) {
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [4,4]});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [4,5]});
|
||||
var w = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: tags});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(w)
|
||||
);
|
||||
}
|
||||
|
||||
function createConnectingWays() {
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [4,4]});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [4,5]});
|
||||
var n3 = iD.osmNode({id: 'n-3', loc: [5,5]});
|
||||
var w = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: {'highway': 'unclassified'}});
|
||||
var w2 = iD.osmWay({id: 'w-2', nodes: ['n-1', 'n-3'], tags: {'highway': 'unclassified'}});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(n3),
|
||||
iD.actionAddEntity(w),
|
||||
iD.actionAddEntity(w2)
|
||||
);
|
||||
}
|
||||
|
||||
function validate() {
|
||||
var validator = iD.validationDisconnectedWay();
|
||||
var changes = context.history().changes();
|
||||
var entities = changes.modified.concat(changes.created);
|
||||
var issues = [];
|
||||
entities.forEach(function(entity) {
|
||||
issues = issues.concat(validator(entity, context));
|
||||
});
|
||||
return issues;
|
||||
}
|
||||
|
||||
it('has no errors on init', function() {
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('finds disconnected highway', function() {
|
||||
createWay({'highway': 'unclassified'});
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(1);
|
||||
var issue = issues[0];
|
||||
expect(issue.type).to.eql('disconnected_way');
|
||||
expect(issue.severity).to.eql('warning');
|
||||
expect(issue.entities).to.have.lengthOf(1);
|
||||
expect(issue.entities[0].id).to.eql('w-1');
|
||||
});
|
||||
|
||||
it('ignores roads that are connected', function() {
|
||||
createConnectingWays();
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
describe('iD.validations.missing_tag', function () {
|
||||
var context;
|
||||
|
||||
beforeEach(function() {
|
||||
context = iD.coreContext();
|
||||
});
|
||||
|
||||
function createWay(tags) {
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [4,4]});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [4,5]});
|
||||
var w = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: tags});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(w)
|
||||
);
|
||||
}
|
||||
|
||||
function createRelation(tags) {
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [4,4]});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [4,5]});
|
||||
var n3 = iD.osmNode({id: 'n-3', loc: [5,5]});
|
||||
var w = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2', 'n-3', 'n-1']});
|
||||
var r = iD.osmRelation({id: 'r-1', members: [{id: 'w-1'}], tags: tags});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(n3),
|
||||
iD.actionAddEntity(w),
|
||||
iD.actionAddEntity(r)
|
||||
);
|
||||
}
|
||||
|
||||
function validate() {
|
||||
var validator = iD.validationMissingTag();
|
||||
var changes = context.history().changes();
|
||||
var entities = changes.modified.concat(changes.created);
|
||||
var issues = [];
|
||||
entities.forEach(function(entity) {
|
||||
issues = issues.concat(validator(entity, context));
|
||||
});
|
||||
return issues;
|
||||
}
|
||||
|
||||
it('has no errors on init', function() {
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('ignores way with descriptive tags', function() {
|
||||
createWay({ leisure: 'park' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('ignores multipolygon with descriptive tags', function() {
|
||||
createRelation({ leisure: 'park', type: 'multipolygon' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('finds no tags', function() {
|
||||
createWay({});
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(1);
|
||||
var issue = issues[0];
|
||||
expect(issue.type).to.eql('missing_tag');
|
||||
expect(issue.entities).to.have.lengthOf(1);
|
||||
expect(issue.entities[0].id).to.eql('w-1');
|
||||
});
|
||||
|
||||
it('finds no descriptive tags', function() {
|
||||
createWay({ name: 'Main Street', source: 'Bing' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(1);
|
||||
var issue = issues[0];
|
||||
expect(issue.type).to.eql('missing_tag');
|
||||
expect(issue.entities).to.have.lengthOf(1);
|
||||
expect(issue.entities[0].id).to.eql('w-1');
|
||||
});
|
||||
|
||||
it('finds no descriptive tags on multipolygon', function() {
|
||||
createRelation({ name: 'City Park', source: 'Bing', type: 'multipolygon' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(1);
|
||||
var issue = issues[0];
|
||||
expect(issue.type).to.eql('missing_tag');
|
||||
expect(issue.entities).to.have.lengthOf(1);
|
||||
expect(issue.entities[0].id).to.eql('r-1');
|
||||
});
|
||||
|
||||
it('finds no type tag on relation', function() {
|
||||
createRelation({ name: 'City Park', source: 'Bing', leisure: 'park' });
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(1);
|
||||
var issue = issues[0];
|
||||
expect(issue.type).to.eql('missing_tag');
|
||||
expect(issue.entities).to.have.lengthOf(1);
|
||||
expect(issue.entities[0].id).to.eql('r-1');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
describe('iD.validations.tag_suggests_area', function () {
|
||||
var context;
|
||||
|
||||
beforeEach(function() {
|
||||
context = iD.Context();
|
||||
});
|
||||
|
||||
function createWay(tags) {
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [4,4]});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [4,5]});
|
||||
var w = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: tags});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(w)
|
||||
);
|
||||
}
|
||||
|
||||
function createPoint(tags) {
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [4,4], tags: tags});
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1)
|
||||
);
|
||||
}
|
||||
|
||||
function validate() {
|
||||
var validator = iD.validationTagSuggestsArea();
|
||||
var changes = context.history().changes();
|
||||
var entities = changes.modified.concat(changes.created);
|
||||
var issues = [];
|
||||
entities.forEach(function(entity) {
|
||||
issues = issues.concat(validator(entity, context));
|
||||
});
|
||||
return issues;
|
||||
}
|
||||
|
||||
|
||||
it('has no errors on init', function() {
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('has no errors on good tags', function() {
|
||||
createWay({'highway': 'unclassified'});
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('ignores points', function() {
|
||||
createPoint({'building': 'yes'});
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('finds tags that suggest area', function() {
|
||||
createWay({'building': 'yes'});
|
||||
var issues = validate();
|
||||
expect(issues).to.have.lengthOf(1);
|
||||
var issue = issues[0];
|
||||
expect(issue.type).to.eql('tag_suggests_area');
|
||||
expect(issue.severity).to.eql('warning');
|
||||
expect(issue.entities).to.have.lengthOf(1);
|
||||
expect(issue.entities[0].id).to.eql('w-1');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
describe('iD.validations.validator', function () {
|
||||
var context;
|
||||
|
||||
beforeEach(function() {
|
||||
context = iD.coreContext();
|
||||
});
|
||||
|
||||
function createInvalidWay() {
|
||||
var n1 = iD.osmNode({id: 'n-1', loc: [4,4]});
|
||||
var n2 = iD.osmNode({id: 'n-2', loc: [4,5]});
|
||||
var w = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2']});
|
||||
|
||||
context.perform(
|
||||
iD.actionAddEntity(n1),
|
||||
iD.actionAddEntity(n2),
|
||||
iD.actionAddEntity(w)
|
||||
);
|
||||
}
|
||||
|
||||
it('has no issues on init', function() {
|
||||
var validator = new iD.coreValidator(context);
|
||||
var issues = validator.getIssues();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('populates issues on validate', function() {
|
||||
createInvalidWay();
|
||||
var validator = new iD.coreValidator(context);
|
||||
var issues = validator.getIssues();
|
||||
expect(issues).to.have.lengthOf(0);
|
||||
|
||||
validator.validate();
|
||||
issues = validator.getIssues();
|
||||
expect(issues).to.have.lengthOf(1);
|
||||
var issue = issues[0];
|
||||
expect(issue.type).to.eql('missing_tag');
|
||||
expect(issue.entities).to.have.lengthOf(1);
|
||||
expect(issue.entities[0].id).to.eql('w-1');
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user