diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index cefb8557e..1cbe3bd9b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -459,6 +459,7 @@ A feature's tags indicate it should have a different geometry than it currently * `area_as_line`: an unclosed way has tags implying it should be a closed area (e.g. `area=yes` or `building=yes`) * `vertex_as_point`: a detached node has tags implying it should be part of a way (e.g. `highway=stop`) * `point_as_vertex`: a vertex node has tags implying it should be detached from ways (e.g. `amenity=cafe`) +* `unclosed_multipolygon_part`: a relation is tagged as a multipolygon but not all of its member ways form closed rings ##### `missing_role` diff --git a/data/core.yaml b/data/core.yaml index e48d5e1f8..c7280aff9 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -1513,6 +1513,9 @@ en: end: message: "{feature} has no outlet" reference: "One-way roads must lead to other roads." + unclosed_multipolygon_part: + message: "{feature} has an unclosed part" + reference: "All inner and outer parts of multipolygons should have connected endpoints." unsquare_way: title: "Unsquare Corners (up to {val}°)" message: "{feature} has unsquare corners" diff --git a/dist/locales/en.json b/dist/locales/en.json index 5ecd34e13..cdd71b0e9 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -1883,6 +1883,10 @@ } } }, + "unclosed_multipolygon_part": { + "message": "{feature} has an unclosed part", + "reference": "All inner and outer parts of multipolygons should have connected endpoints." + }, "unsquare_way": { "title": "Unsquare Corners (up to {val}°)", "message": "{feature} has unsquare corners", diff --git a/modules/validations/mismatched_geometry.js b/modules/validations/mismatched_geometry.js index a3ea1ce0a..869f1002f 100644 --- a/modules/validations/mismatched_geometry.js +++ b/modules/validations/mismatched_geometry.js @@ -3,6 +3,7 @@ import { actionChangeTags } from '../actions/change_tags'; import { actionMergeNodes } from '../actions/merge_nodes'; import { actionExtract } from '../actions/extract'; import { modeSelect } from '../modes/select'; +import { osmJoinWays } from '../osm/multipolygon'; import { osmNodeGeometriesForTags } from '../osm/tags'; import { geoHasSelfIntersections, geoSphericalDistance } from '../geo'; import { t } from '../util/locale'; @@ -224,11 +225,63 @@ export function validationMismatchedGeometry(context) { return null; } + function unclosedMultipolygonPartIssues(entity, graph) { + + if (entity.type !== 'relation' || !entity.isMultipolygon() || entity.isDegenerate()) return null; + + var sequences = osmJoinWays(entity.members, graph); + + var issues = []; + + for (var i in sequences) { + var sequence = sequences[i]; + + if (!sequence.nodes) continue; + + var firstNode = sequence.nodes[0]; + var lastNode = sequence.nodes[sequence.nodes.length - 1]; + + // part is closed if the first and last nodes are the same + if (firstNode === lastNode) continue; + + var issue = new validationIssue({ + type: type, + subtype: 'unclosed_multipolygon_part', + severity: 'warning', + message: function(context) { + var entity = context.hasEntity(this.entityIds[0]); + return entity ? t('issues.unclosed_multipolygon_part.message', { + feature: utilDisplayLabel(entity, context) + }) : ''; + }, + reference: showReference, + loc: sequence.nodes[0].loc, + entityIds: [entity.id], + hash: sequence.map(function(way) { + return way.id; + }).join() + }); + issues.push(issue); + } + + return issues; + + function showReference(selection) { + selection.selectAll('.issue-reference') + .data([0]) + .enter() + .append('div') + .attr('class', 'issue-reference') + .text(t('issues.unclosed_multipolygon_part.reference')); + } + } + var validation = function checkMismatchedGeometry(entity, graph) { var issues = [ vertexTaggedAsPointIssue(entity, graph), lineTaggedAsAreaIssue(entity) ]; + issues = issues.concat(unclosedMultipolygonPartIssues(entity, graph)); return issues.filter(Boolean); };