From f244e636610f3560c0e2b65c6a5706bc0594d033 Mon Sep 17 00:00:00 2001 From: Kyle Hensel Date: Wed, 5 Feb 2025 14:41:56 +0100 Subject: [PATCH] validate crossing aeroways (#9315) --- CHANGELOG.md | 1 + data/core.yaml | 10 +++ modules/osm/tags.js | 4 ++ modules/validations/crossing_ways.js | 31 ++++++++- test/spec/validations/crossing_ways.js | 88 ++++++++++++++++++++++++++ 5 files changed, 131 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c709a75e9..582818254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ _Breaking developer changes, which may affect downstream projects or sites that #### :scissors: Operations #### :camera: Street-Level #### :white_check_mark: Validation +* Add warning if aeroways cross each other, buildings or highways ([#9315], thanks [@k-yle]) #### :bug: Bugfixes * Prevent degenerate ways caused by deleting a corner of a triangle ([#10003], thanks [@k-yle]) #### :earth_asia: Localization diff --git a/data/core.yaml b/data/core.yaml index 87baf0059..da821d6b0 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -1778,6 +1778,16 @@ en: title: Crossing Ways message: "{feature} crosses {feature2}" tip: "Find features that incorrectly cross over one another" + aeroway-aeroway: + reference: "Crossing aeroways should be connected." + aeroway-building: + reference: "Aeroways crossing buildings should use bridges or different layers." + aeroway-highway: + reference: "Aeroways crossing railways should be connected." + aeroway-railway: + reference: "Aeroways crossing highways should be connected." + aeroway-waterway: + reference: "Aeroways crossing waterways should use tunnels." building-building: reference: "Buildings should not intersect except on different layers." building-highway: diff --git a/modules/osm/tags.js b/modules/osm/tags.js index e105ce556..e31a1332c 100644 --- a/modules/osm/tags.js +++ b/modules/osm/tags.js @@ -233,6 +233,10 @@ export var osmRoutableHighwayTagValues = { unclassified: true, road: true, service: true, track: true, living_street: true, bus_guideway: true, busway: true, path: true, footway: true, cycleway: true, bridleway: true, pedestrian: true, corridor: true, steps: true, ladder: true }; +/** aeroway tags that are treated as routable for aircraft */ +export const osmRoutableAerowayTags = { + runway: true, taxiway: true +}; // "highway" tag values that generally do not allow motor vehicles export var osmPathHighwayTagValues = { path: true, footway: true, cycleway: true, bridleway: true, pedestrian: true, corridor: true, steps: true, ladder: true diff --git a/modules/validations/crossing_ways.js b/modules/validations/crossing_ways.js index 7b03c5fa0..a7d4aa80f 100644 --- a/modules/validations/crossing_ways.js +++ b/modules/validations/crossing_ways.js @@ -8,7 +8,7 @@ import { modeSelect } from '../modes/select'; import { geoAngle, geoExtent, geoLatToMeters, geoLonToMeters, geoLineIntersection, geoSphericalClosestNode, geoSphericalDistance, geoVecAngle, geoVecLength, geoMetersToLat, geoMetersToLon } from '../geo'; import { osmNode } from '../osm/node'; -import { osmFlowingWaterwayTagValues, osmPathHighwayTagValues, osmRailwayTrackTagValues, osmRoutableHighwayTagValues } from '../osm/tags'; +import { osmFlowingWaterwayTagValues, osmPathHighwayTagValues, osmRailwayTrackTagValues, osmRoutableAerowayTags, osmRoutableHighwayTagValues } from '../osm/tags'; import { t } from '../core/localizer'; import { utilDisplayLabel } from '../util/utilDisplayLabel'; import { validationIssue, validationIssueFix } from '../core/validation'; @@ -44,7 +44,7 @@ export function validationCrossingWays(context) { } function allowsBridge(featureType) { - return featureType === 'highway' || featureType === 'railway' || featureType === 'waterway'; + return featureType === 'highway' || featureType === 'railway' || featureType === 'waterway' || featureType === 'aeroway'; } function allowsTunnel(featureType) { return featureType === 'highway' || featureType === 'railway' || featureType === 'waterway'; @@ -63,6 +63,8 @@ export function validationCrossingWays(context) { var tags = entity.tags; + if (tags.aeroway in osmRoutableAerowayTags) return 'aeroway'; + if (hasTag(tags, 'building') && !ignoredBuildings[tags.building]) return 'building'; if (hasTag(tags, 'highway') && osmRoutableHighwayTagValues[tags.highway]) return 'highway'; @@ -126,6 +128,9 @@ export function validationCrossingWays(context) { primary: true, primary_link: true, secondary: true, secondary_link: true }; + /** + * @returns {object | null} the tags for the connecting node, or null if the entities should not be joined + */ function tagsForConnectionNodeIfAllowed(entity1, entity2, graph, lessLikelyTags) { var featureType1 = getFeatureType(entity1, graph); var featureType2 = getFeatureType(entity2, graph); @@ -134,6 +139,27 @@ export function validationCrossingWays(context) { var geometry2 = entity2.geometry(graph); var bothLines = geometry1 === 'line' && geometry2 === 'line'; + /** + * @typedef {NonNullable>} FeatureType + * @type {`${FeatureType}-${FeatureType}`} + */ + const featureTypes = [featureType1, featureType2].sort().join('-'); + + if (featureTypes === 'aeroway-aeroway') return {}; + + if (featureTypes === 'aeroway-highway') { + const isServiceRoad = entity1.tags.highway === 'service' || entity2.tags.highway === 'service'; + const isPath = entity1.tags.highway in osmPathHighwayTagValues || entity2.tags.highway in osmPathHighwayTagValues; + // only significant roads get the aeroway=aircraft_crossing tag + return isServiceRoad || isPath ? {} : { aeroway: 'aircraft_crossing' }; + } + + if (featureTypes === 'aeroway-railway') { + return { aeroway: 'aircraft_crossing', railway: 'level_crossing' }; + } + + if (featureTypes === 'aeroway-waterway') return null; + if (featureType1 === featureType2) { if (featureType1 === 'highway') { var entity1IsPath = osmPathHighwayTagValues[entity1.tags.highway]; @@ -167,7 +193,6 @@ export function validationCrossingWays(context) { if (featureType1 === 'railway') return {}; } else { - var featureTypes = [featureType1, featureType2]; if (featureTypes.indexOf('highway') !== -1) { if (featureTypes.indexOf('railway') !== -1) { if (!bothLines) return {}; diff --git a/test/spec/validations/crossing_ways.js b/test/spec/validations/crossing_ways.js index 735b22b88..58bcbe905 100644 --- a/test/spec/validations/crossing_ways.js +++ b/test/spec/validations/crossing_ways.js @@ -202,6 +202,60 @@ describe('iD.validations.crossing_ways', function () { expect(issues).to.have.lengthOf(0); }); + it('ignores a routable aeroway crossing a non-routable aeroway', function() { + createWaysWithOneCrossingPoint({ aeroway: 'taxiway' }, { aeroway: 'aerodrome' }); + const issues = validate(); + expect(issues).to.have.lengthOf(0); + }); + + it('ignores an aeroway crossing a road tunnel', function() { + createWaysWithOneCrossingPoint({ aeroway: 'runway' }, { highway: 'trunk', tunnel: 'yes', layer: '-1' }); + const issues = validate(); + expect(issues).to.have.lengthOf(0); + }); + + it('ignores an aeroway crossing a road bridge', function() { + createWaysWithOneCrossingPoint({ aeroway: 'runway' }, { highway: 'trunk', bridge: 'yes', layer: '1' }); + const issues = validate(); + expect(issues).to.have.lengthOf(0); + }); + + it('ignores an aeroway crossing a rail tunnel', function() { + createWaysWithOneCrossingPoint({ aeroway: 'runway' }, { railway: 'track', tunnel: 'yes', layer: '-1' }); + const issues = validate(); + expect(issues).to.have.lengthOf(0); + }); + + it('ignores an aeroway crossing a rail bridge', function() { + createWaysWithOneCrossingPoint({ aeroway: 'runway' }, { railway: 'track', bridge: 'yes', layer: '1' }); + const issues = validate(); + expect(issues).to.have.lengthOf(0); + }); + + it('ignores an aeroway bridge crossing a road', function() { + createWaysWithOneCrossingPoint({ aeroway: 'runway', bridge: 'yes', layer: '2' }, { highway: 'trunk', layer: '1' }); + const issues = validate(); + expect(issues).to.have.lengthOf(0); + }); + + it('ignores an aeroway bridge crossing a railway', function() { + createWaysWithOneCrossingPoint({ aeroway: 'runway', bridge: 'yes', layer: '1' }, { railway: 'track' }); + const issues = validate(); + expect(issues).to.have.lengthOf(0); + }); + + it('ignores an aeroway crossing a culvert', function() { + createWaysWithOneCrossingPoint({ aeroway: 'taxiway' }, { waterway: 'ditch', tunnel: 'culvert', layer: -1 }); + const issues = validate(); + expect(issues).to.have.lengthOf(0); + }); + + it('ignores an aeroway crossing a building on a different layer', function() { + createWaysWithOneCrossingPoint({ aeroway: 'runway' }, { building: 'yes', layer: '0.5' }); + const issues = validate(); + expect(issues).to.have.lengthOf(0); + }); + // warning crossing cases between ways it('flags road crossing road', function() { createWaysWithOneCrossingPoint({ highway: 'residential' }, { highway: 'residential' }); @@ -453,4 +507,38 @@ describe('iD.validations.crossing_ways', function () { verifySingleCrossingIssue(validate(), {}); }); + it('flags an aeroway crosing another aeroway', function() { + createWaysWithOneCrossingPoint({ aeroway: 'runway' }, { aeroway: 'taxiway' }); + verifySingleCrossingIssue(validate(), {}); + }); + + it('flags an aeroway crosing a major road', function() { + createWaysWithOneCrossingPoint({ aeroway: 'runway' }, { highway: 'motorway' }); + verifySingleCrossingIssue(validate(), { aeroway: 'aircraft_crossing' }); + }); + + it('flags an aeroway crosing a service road', function() { + createWaysWithOneCrossingPoint({ aeroway: 'runway' }, { highway: 'service' }); + verifySingleCrossingIssue(validate(), {}); + }); + + it('flags an aeroway crosing a path', function() { + createWaysWithOneCrossingPoint({ aeroway: 'runway' }, { highway: 'corridor' }); + verifySingleCrossingIssue(validate(), {}); + }); + + it('flags an aeroway crosing a railway', function() { + createWaysWithOneCrossingPoint({ aeroway: 'taxiway' }, { railway: 'disused' }); + verifySingleCrossingIssue(validate(), { aeroway: 'aircraft_crossing', railway: 'level_crossing' }); + }); + + it('flags an aeroway crosing a waterway', function() { + createWaysWithOneCrossingPoint({ aeroway: 'runway' }, { waterway: 'canal' }); + verifySingleCrossingIssue(validate(), null); + }); + + it('flags an aeroway crosing a building', function() { + createWaysWithOneCrossingPoint({ aeroway: 'runway' }, { building: 'hangar' }); + verifySingleCrossingIssue(validate(), null); + }); });