diff --git a/data/core.yaml b/data/core.yaml index f98a773c50..b310bfa1d4 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -1748,6 +1748,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 0978e8fb49..d680e0262c 100644 --- a/modules/osm/tags.js +++ b/modules/osm/tags.js @@ -213,6 +213,10 @@ export var osmRoutableHighwayTagValues = { unclassified: true, road: true, service: true, track: true, living_street: true, bus_guideway: true, path: true, footway: true, cycleway: true, bridleway: true, pedestrian: true, corridor: true, steps: 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 diff --git a/modules/validations/crossing_ways.js b/modules/validations/crossing_ways.js index bec80921ff..be6d2e084b 100644 --- a/modules/validations/crossing_ways.js +++ b/modules/validations/crossing_ways.js @@ -6,7 +6,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'; import { validationIssue, validationIssueFix } from '../core/validation'; @@ -42,7 +42,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'; @@ -61,6 +61,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'; @@ -125,6 +127,9 @@ export function validationCrossingWays(context) { }; var nonCrossingHighways = { track: true }; + /** + * @returns {object | null} the tags for the connecting node, or null if the entities should not be joined + */ function tagsForConnectionNodeIfAllowed(entity1, entity2, graph) { var featureType1 = getFeatureType(entity1, graph); var featureType2 = getFeatureType(entity2, graph); @@ -133,6 +138,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]; @@ -159,7 +185,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 308af22727..131a7998f3 100644 --- a/test/spec/validations/crossing_ways.js +++ b/test/spec/validations/crossing_ways.js @@ -197,6 +197,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' }); @@ -432,4 +486,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); + }); });