From a4e1012c91fce51b5b67bec61712cf7c957b2ea7 Mon Sep 17 00:00:00 2001 From: Xiaoming Gao Date: Wed, 19 Dec 2018 17:16:52 -0500 Subject: [PATCH] add crossing_way validation Tests are also added and passed. One thing to note: I had to add the tree() function to history so that I can use the tree in the test; hope that's fine. --- data/core.yaml | 2 + dist/locales/en.json | 66 ++++++++++++-- modules/core/history.js | 7 +- modules/validations/crossing_ways.js | 112 ++++++++++++++++++++++++ modules/validations/index.js | 1 + modules/validations/validation_issue.js | 3 +- test/index.html | 1 + test/spec/validations/crossing_ways.js | 106 ++++++++++++++++++++++ 8 files changed, 289 insertions(+), 9 deletions(-) create mode 100644 modules/validations/crossing_ways.js create mode 100644 test/spec/validations/crossing_ways.js diff --git a/data/core.yaml b/data/core.yaml index d3478c6b7..311849e44 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -638,6 +638,8 @@ en: 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}" + crossing_ways: Corssing ways without connection + crossing_ways_tooltip: "Roads are crossing other roads, buildings, railroads, or waterways without connection nodes or a bridge tag." zoom: in: Zoom in out: Zoom out diff --git a/dist/locales/en.json b/dist/locales/en.json index 4b6362c75..69a228e50 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -774,7 +774,9 @@ "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}" + "deprecated_tags": "Deprecated tags: {tags}", + "crossing_ways": "Corssing ways without connection", + "crossing_ways_tooltip": "Roads are crossing other roads, buildings, railroads, or waterways without connection nodes or a bridge tag." }, "zoom": { "in": "Zoom in", @@ -7590,9 +7592,6 @@ "SPW_PICC": { "name": "SPW(allonie) PICC numerical imagery" }, - "US-TIGER-Roads-2012": { - "name": "TIGER Roads 2012" - }, "US-TIGER-Roads-2014": { "description": "At zoom level 16+, public domain map data from the US Census. At lower zooms, only changes since 2006 minus changes already incorporated into OpenStreetMap", "name": "TIGER Roads 2014" @@ -7601,6 +7600,10 @@ "description": "Yellow = Public domain map data from the US Census. Red = Data not found in OpenStreetMap", "name": "TIGER Roads 2017" }, + "US-TIGER-Roads-2018": { + "description": "Yellow = Public domain map data from the US Census. Red = Data not found in OpenStreetMap", + "name": "TIGER Roads 2018" + }, "US_Forest_Service_roads_overlay": { "description": "Highway: Green casing = unclassified. Brown casing = track. Surface: gravel = light brown fill, Asphalt = black, paved = gray, ground =white, concrete = blue, grass = green. Seasonal = white bars", "name": "U.S. Forest Roads Overlay" @@ -7617,6 +7620,12 @@ }, "name": "UrbIS-Ortho 2017" }, + "UrbISOrtho2018": { + "attribution": { + "text": "Realized by means of Brussels UrbIS®© - Distribution & Copyright CIRB" + }, + "name": "UrbIS-Ortho 2018" + }, "UrbisAdmFR": { "attribution": { "text": "Realized by means of Brussels UrbIS®© - Distribution & Copyright CIRB" @@ -7701,11 +7710,33 @@ "description": "Japan GSI Standard Map. Widely covered.", "name": "Japan GSI Standard Map" }, - "hike_n_bike": { + "helsingborg-orto": { "attribution": { - "text": "© OpenStreetMap contributors" + "text": "© Helsingborg municipality" }, - "name": "Hike & Bike" + "description": "Orthophotos from the municipality of Helsingborg 2016, public domain", + "name": "Helsingborg Orthophoto" + }, + "kalmar-orto-2014": { + "attribution": { + "text": "© Kalmar municipality" + }, + "description": "Orthophotos for the north coast of the municipality of Kalmar 2014", + "name": "Kalmar North Orthophoto 2014" + }, + "kalmar-orto-2016": { + "attribution": { + "text": "© Kalmar municipality" + }, + "description": "Orthophotos for the south coast of the municipality of Kalmar 2016", + "name": "Kalmar South Orthophoto 2016" + }, + "kalmar-orto-2018": { + "attribution": { + "text": "© Kalmar municipality" + }, + "description": "Orthophotos for urban areas of the municipality of Kalmar 2018", + "name": "Kalmar Urban Orthophoto 2018" }, "kelkkareitit": { "attribution": { @@ -7728,6 +7759,20 @@ "description": "Mosaic of Swedish orthophotos from the period 1970–1980. Is under construction.", "name": "Lantmäteriet Historic Orthophoto 1975" }, + "lantmateriet-topowebb": { + "attribution": { + "text": "© Lantmäteriet, CC0" + }, + "description": "Topographic map of Sweden 1:50 000", + "name": "Lantmäteriet Topographic Map" + }, + "linkoping-orto": { + "attribution": { + "text": "© Linköping municipality" + }, + "description": "Orthophotos from the municipality of Linköping 2010, open data", + "name": "Linköping Orthophoto" + }, "mapbox_locator_overlay": { "attribution": { "text": "Terms & Feedback" @@ -7792,6 +7837,13 @@ }, "name": "Stamen Terrain" }, + "stockholm-orto": { + "attribution": { + "text": "© Stockholm municipality, CC0" + }, + "description": "Orthophotos from the municipality of Stockholm 2015, CC0 license", + "name": "Stockholm Orthophoto" + }, "tf-cycle": { "attribution": { "text": "Maps © Thunderforest, Data © OpenStreetMap contributors" diff --git a/modules/core/history.js b/modules/core/history.js index 239034733..9df59db4b 100644 --- a/modules/core/history.js +++ b/modules/core/history.js @@ -122,6 +122,11 @@ export function coreHistory(context) { }, + tree: function() { + return _tree; + }, + + base: function() { return _stack[0].graph; }, @@ -285,7 +290,7 @@ export function coreHistory(context) { return _flatten(_map( validations, function(fn) { - return fn(context)(changes, _stack[_index].graph); + return fn(context)(changes, _stack[_index].graph, _tree); } )); }, diff --git a/modules/validations/crossing_ways.js b/modules/validations/crossing_ways.js new file mode 100644 index 000000000..c991b58bb --- /dev/null +++ b/modules/validations/crossing_ways.js @@ -0,0 +1,112 @@ +import { geoExtent, geoLineIntersection } from '../geo'; +import { set as d3_set } from 'd3-collection'; +import { t } from '../util/locale'; +import { + ValidationIssueType, + ValidationIssueSeverity, + validationIssue, +} from './validation_issue'; + + +export function validationHighwayCrossingOtherWays() { + // Check if the edge going from n1 to n2 crosses (without a connection node) + // any edge on way. Return the corss point if so. + function findEdgeToWayCrossCoords(n1, n2, way, graph, edgePairsVisited) { + var crossCoords = []; + for (var j = 0; j < way.nodes.length - 1; j++) { + var nidA = way.nodes[j], + nidB = way.nodes[j + 1]; + if (nidA === n1.id || nidA === n2.id || + nidB === n1.id || nidB === n2.id) { + // n1 or n2 is a connection node; skip + continue; + } + + var edgePair = edgePairString(n1.id, n2.id, nidA, nidB); + if (edgePairsVisited.has(edgePair)) continue; + edgePairsVisited.add(edgePair); + + var nA = graph.entity(nidA), + nB = graph.entity(nidB), + point = geoLineIntersection([n1.loc, n2.loc], [nA.loc, nB.loc]); + if (point) crossCoords.push(point); + } + return crossCoords; + } + + // n1 and n2 from one edge, nA and nB from the other edge + function edgePairString(n1, n2, nA, nB) { + return n1 > nA ? n1 + n2 + nA + nB : nA + nB + n1 + n2; + } + + function findCrossingsByWay(entity, graph, tree, edgePairsVisited) { + var edgeCrossInfos = []; + if (entity.type !== 'way' || !entity.tags.highway) return edgeCrossInfos; + if (entity.geometry(graph) !== 'line') return edgeCrossInfos; + + for (var i = 0; i < entity.nodes.length - 1; i++) { + var nid1 = entity.nodes[i], + nid2 = entity.nodes[i + 1], + n1 = graph.entity(nid1), + n2 = graph.entity(nid2), + 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]) + ] + ]), + 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]; + if (!(way.tags.highway || way.tags.building || + way.tags.railway || way.tags.waterway)) { + continue; + } + if (way.tags.waterway && entity.tags.bridge === 'yes') { + continue; + } + var crossCoords = findEdgeToWayCrossCoords(n1, n2, way, graph, edgePairsVisited); + for (var k = 0; k < crossCoords.length; k++) { + edgeCrossInfos.push({ + ways: [entity, way], + cross_point: crossCoords[k], + }); + } + } + } + return edgeCrossInfos; + } + + + var validation = function(changes, graph, tree) { + // create one issue per crossing point + var edited = changes.created.concat(changes.modified), + edgePairsVisited = d3_set(), + issues = []; + for (var i = 0; i < edited.length; i++) { + var crosses = findCrossingsByWay(edited[i], graph, tree, edgePairsVisited); + for (var j = 0; j < crosses.length; j++) { + issues.push(new validationIssue({ + type: ValidationIssueType.crossing_ways, + severity: ValidationIssueSeverity.error, + message: t('validations.crossing_ways'), + tooltip: t('validations.crossing_ways_tooltip'), + entities: crosses[j].ways, + coordinates: crosses[j].cross_point, + })); + } + } + + return issues; + }; + + + return validation; +} diff --git a/modules/validations/index.js b/modules/validations/index.js index ead99f117..51b3300c3 100644 --- a/modules/validations/index.js +++ b/modules/validations/index.js @@ -1,5 +1,6 @@ export { validationDeprecatedTag } from './deprecated_tag'; export { validationDisconnectedHighway } from './disconnected_highway'; +export { validationHighwayCrossingOtherWays } from './crossing_ways'; export { ValidationIssueType, ValidationIssueSeverity } from './validation_issue'; export { validationManyDeletions } from './many_deletions'; export { validationMapCSSChecks } from './mapcss_checks'; diff --git a/modules/validations/validation_issue.js b/modules/validations/validation_issue.js index a3aad341b..dd265309c 100644 --- a/modules/validations/validation_issue.js +++ b/modules/validations/validation_issue.js @@ -9,6 +9,7 @@ var ValidationIssueType = Object.freeze({ old_multipolygon: 'old_multipolygon', tag_suggests_area: 'tag_suggests_area', map_rule_issue: 'map_rule_issue', + crossing_ways: 'crossing_ways' }); @@ -41,6 +42,6 @@ export function validationIssue(attrs) { this.message = attrs.message; this.tooltip = attrs.tooltip; this.entities = attrs.entities; // expect an array of entities - this.coordinates = attrs.coordinates; // expect an array of [lon, lat] + this.coordinates = attrs.coordinates; // expect a [lon, lat] array this.fixes = attrs.fixes; // expect an array of functions for possible fixes } diff --git a/test/index.html b/test/index.html index 30f39cf6c..21a6ea7c5 100644 --- a/test/index.html +++ b/test/index.html @@ -144,6 +144,7 @@ +