Merge pull request #5830 from openstreetmap/validation

Issue manager and advanced validation
This commit is contained in:
Quincy Morgan
2019-02-15 14:25:07 -05:00
committed by GitHub
68 changed files with 3861 additions and 450 deletions
+187 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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"}
}
]
}
+170 -19
View File
@@ -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",
+8 -3
View File
@@ -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);
+2 -2
View File
@@ -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);
+25 -24
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+1
View File
@@ -3,3 +3,4 @@ export { coreDifference } from './difference';
export { coreGraph } from './graph';
export { coreHistory } from './history';
export { coreTree } from './tree';
export { coreValidator } from './validator';
+230
View File
@@ -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
}
+20
View File
@@ -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;
}
}
+1
View File
@@ -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';
+1
View File
@@ -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';
+1 -2
View File
@@ -220,8 +220,7 @@ export function modeDragNode(context) {
}
context.replace(
actionMoveNode(entity.id, loc),
moveAnnotation(entity)
actionMoveNode(entity.id, loc)
);
// Below here: validations
+6 -2
View File
@@ -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();
}
+6 -2
View File
@@ -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
View File
@@ -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;
+3 -2
View File
@@ -17,8 +17,9 @@ export {
} from './lanes';
export {
osmIsSimpleMultipolygonOuterMember,
osmSimpleMultipolygonOuterMember,
osmOldMultipolygonOuterMemberOfRelation,
osmIsOldMultipolygonOuterMember,
osmOldMultipolygonOuterMember,
osmJoinWays
} from './multipolygon';
+37 -2
View File
@@ -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
View File
@@ -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;
},
+13 -7
View File
@@ -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],
}));
}
}
};
+2 -2
View File
@@ -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
View File
@@ -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')
+2 -2
View File
@@ -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;
+8 -7
View File
@@ -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
View File
@@ -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');
}
}
+24 -20
View File
@@ -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; })
));
}
}
});
+12 -2
View File
@@ -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)
+180
View File
@@ -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;
}
+2
View File
@@ -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')
+6
View File
@@ -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
View File
@@ -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;
+394
View File
@@ -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;
}
+2 -1
View File
@@ -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
+6 -11
View File
@@ -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
+4 -4
View File
@@ -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 -8
View File
@@ -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')
+6 -3
View File
@@ -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() {};
+3 -1
View File
@@ -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
View File
@@ -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);
}
+202
View File
@@ -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;
}
+420
View File
@@ -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;
}
+76 -16
View File
@@ -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;
}
+100
View File
@@ -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;
}
+39 -15
View File
@@ -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;
}
+5 -3
View File
@@ -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';
+44 -17
View File
@@ -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;
}
-27
View File
@@ -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;
}
+29
View File
@@ -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;
}
+67 -19
View File
@@ -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;
}
+54 -15
View File
@@ -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;
}
+87 -31
View File
@@ -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;
}
+4
View File
@@ -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

+4
View File
@@ -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

+8
View File
@@ -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();
+5 -2
View File
@@ -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' }
}]);
});
});
+21 -21
View File
@@ -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;
});
});
+9 -10
View File
@@ -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);
});
});
});
});
});
+221
View File
@@ -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);
});
});
+250
View File
@@ -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');
});
});
+53
View File
@@ -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');
});
});
+69
View File
@@ -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);
});
});
+104
View File
@@ -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');
});
});
+41
View File
@@ -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');
});
});