// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.validation.tests; import static org.openstreetmap.josm.tools.I18n.marktr; import static org.openstreetmap.josm.tools.I18n.tr; import static org.openstreetmap.josm.tools.I18n.trn; import java.awt.geom.Area; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.command.ChangeCommand; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.RelationMember; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.data.osm.WaySegment; import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon; import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData; import org.openstreetmap.josm.data.validation.OsmValidator; import org.openstreetmap.josm.data.validation.Severity; import org.openstreetmap.josm.data.validation.Test; import org.openstreetmap.josm.data.validation.TestError; import org.openstreetmap.josm.gui.DefaultNameFormatter; import org.openstreetmap.josm.gui.mappaint.ElemStyles; import org.openstreetmap.josm.gui.mappaint.MapPaintStyles; import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement; import org.openstreetmap.josm.gui.progress.ProgressMonitor; import org.openstreetmap.josm.tools.Geometry; import org.openstreetmap.josm.tools.Geometry.PolygonIntersection; /** * Checks if multipolygons are valid * @since 3669 */ public class MultipolygonTest extends Test { /** Non-Way in multipolygon */ public static final int WRONG_MEMBER_TYPE = 1601; /** No useful role for multipolygon member */ public static final int WRONG_MEMBER_ROLE = 1602; /** Multipolygon is not closed */ public static final int NON_CLOSED_WAY = 1603; /** No outer way for multipolygon */ public static final int MISSING_OUTER_WAY = 1604; /** Multipolygon inner way is outside */ public static final int INNER_WAY_OUTSIDE = 1605; /** Intersection between multipolygon ways */ public static final int CROSSING_WAYS = 1606; /** Style for outer way mismatches / With the currently used mappaint style(s) the style for outer way mismatches the area style */ public static final int OUTER_STYLE_MISMATCH = 1607; /** With the currently used mappaint style the style for inner way equals the multipolygon style */ public static final int INNER_STYLE_MISMATCH = 1608; /** Area style way is not closed */ public static final int NOT_CLOSED = 1609; /** No area style for multipolygon */ public static final int NO_STYLE = 1610; /** Multipolygon relation should be tagged with area tags and not the outer way(s) */ public static final int NO_STYLE_POLYGON = 1611; /** Area style on outer way */ public static final int OUTER_STYLE = 1613; /** Multipolygon member repeated (same primitive, same role */ public static final int REPEATED_MEMBER_SAME_ROLE = 1614; /** Multipolygon member repeated (same primitive, different role) */ public static final int REPEATED_MEMBER_DIFF_ROLE = 1615; /** Multipolygon ring is equal to another ring */ public static final int EQUAL_RINGS = 1616; /** Multipolygon rings share nodes */ public static final int RINGS_SHARE_NODES = 1617; private static final int FOUND_INSIDE = 1; private static final int FOUND_OUTSIDE = 2; private final Set<String> keysCheckedByAnotherTest = new HashSet<>(); /** * Constructs a new {@code MultipolygonTest}. */ public MultipolygonTest() { super(tr("Multipolygon"), tr("This test checks if multipolygons are valid.")); } @Override public void startTest(ProgressMonitor progressMonitor) { super.startTest(progressMonitor); keysCheckedByAnotherTest.clear(); for (Test t : OsmValidator.getEnabledTests(false)) { if (t instanceof UnclosedWays) { keysCheckedByAnotherTest.addAll(((UnclosedWays) t).getCheckedKeys()); break; } } } @Override public void endTest() { keysCheckedByAnotherTest.clear(); super.endTest(); } @Override public void visit(Way w) { if (!w.isArea() && ElemStyles.hasOnlyAreaOrTextStyleElements(w)) { List<Node> nodes = w.getNodes(); if (nodes.isEmpty()) return; // fix zero nodes bug for (String key : keysCheckedByAnotherTest) { if (w.hasKey(key)) { return; } } errors.add(TestError.builder(this, Severity.WARNING, NOT_CLOSED) .message(tr("Area style way is not closed")) .primitives(w) .highlight(Arrays.asList(nodes.get(0), nodes.get(nodes.size() - 1))) .build()); } } @Override public void visit(Relation r) { if (r.isMultipolygon() && r.getMembersCount() > 0) { checkMembersAndRoles(r); checkOuterWay(r); boolean hasRepeatedMembers = checkRepeatedWayMembers(r); // Rest of checks is only for complete multipolygon if (!hasRepeatedMembers && !r.hasIncompleteMembers()) { Multipolygon polygon = new Multipolygon(r); checkStyleConsistency(r, polygon); checkGeometryAndRoles(r, polygon); } } } /** * Checks that multipolygon has at least an outer way:<ul> * <li>{@link #MISSING_OUTER_WAY}: No outer way for multipolygon</li> * </ul> * @param r relation */ private void checkOuterWay(Relation r) { for (RelationMember m : r.getMembers()) { if (m.isWay() && "outer".equals(m.getRole())) { return; } } errors.add(TestError.builder(this, Severity.WARNING, MISSING_OUTER_WAY) .message(r.isBoundary() ? tr("No outer way for boundary") : tr("No outer way for multipolygon")) .primitives(r) .build()); } /** * Various style-related checks:<ul> * <li>{@link #NO_STYLE_POLYGON}: Multipolygon relation should be tagged with area tags and not the outer way</li> * <li>{@link #INNER_STYLE_MISMATCH}: With the currently used mappaint style the style for inner way equals the multipolygon style</li> * <li>{@link #OUTER_STYLE_MISMATCH}: Style for outer way mismatches</li> * <li>{@link #OUTER_STYLE}: Area style on outer way</li> * </ul> * @param r relation * @param polygon multipolygon */ private void checkStyleConsistency(Relation r, Multipolygon polygon) { ElemStyles styles = MapPaintStyles.getStyles(); if (styles != null && !r.isBoundary()) { AreaElement area = ElemStyles.getAreaElemStyle(r, false); boolean areaStyle = area != null; // If area style was not found for relation then use style of ways if (area == null) { for (Way w : polygon.getOuterWays()) { area = ElemStyles.getAreaElemStyle(w, true); if (area != null) { break; } } if (area == null) { errors.add(TestError.builder(this, Severity.OTHER, NO_STYLE) .message(tr("No area style for multipolygon")) .primitives(r) .build()); } else { /* old style multipolygon - solve: copy tags from outer way to multipolygon */ errors.add(TestError.builder(this, Severity.ERROR, NO_STYLE_POLYGON) .message(trn("Multipolygon relation should be tagged with area tags and not the outer way", "Multipolygon relation should be tagged with area tags and not the outer ways", polygon.getOuterWays().size())) .primitives(r) .build()); } } if (area != null) { for (Way wInner : polygon.getInnerWays()) { if (area.equals(ElemStyles.getAreaElemStyle(wInner, false))) { errors.add(TestError.builder(this, Severity.OTHER, INNER_STYLE_MISMATCH) .message(tr("With the currently used mappaint style the style for inner way equals the multipolygon style")) .primitives(Arrays.asList(r, wInner)) .highlight(wInner) .build()); } } for (Way wOuter : polygon.getOuterWays()) { AreaElement areaOuter = ElemStyles.getAreaElemStyle(wOuter, false); if (areaOuter != null) { if (!area.equals(areaOuter)) { String message = !areaStyle ? tr("Style for outer way mismatches") : tr("With the currently used mappaint style(s) the style for outer way mismatches the area style"); errors.add(TestError.builder(this, Severity.OTHER, OUTER_STYLE_MISMATCH) .message(message) .primitives(Arrays.asList(r, wOuter)) .highlight(wOuter) .build()); } else if (areaStyle) { /* style on outer way of multipolygon, but equal to polygon */ errors.add(TestError.builder(this, Severity.ERROR, OUTER_STYLE) .message(tr("Area style on outer way")) .primitives(Arrays.asList(r, wOuter)) .highlight(wOuter) .build()); } } } } } } /** * Various geometry-related checks:<ul> * <li>{@link #NON_CLOSED_WAY}: Multipolygon is not closed</li> * <li>{@link #INNER_WAY_OUTSIDE}: Multipolygon inner way is outside</li> * <li>{@link #CROSSING_WAYS}: Intersection between multipolygon ways</li> * </ul> * @param r relation * @param polygon multipolygon */ private void checkGeometryAndRoles(Relation r, Multipolygon polygon) { int oldErrorsSize = errors.size(); List<Node> openNodes = polygon.getOpenEnds(); if (!openNodes.isEmpty()) { errors.add(TestError.builder(this, Severity.ERROR, NON_CLOSED_WAY) .message(tr("Multipolygon is not closed")) .primitives(combineRelAndPrimitives(r, openNodes)) .highlight(openNodes) .build()); } Map<Long, RelationMember> wayMap = new HashMap<>(); for (int i = 0; i < r.getMembersCount(); i++) { RelationMember mem = r.getMember(i); if (!mem.isWay()) continue; wayMap.put(mem.getWay().getUniqueId(), mem); // duplicate members were checked before } if (wayMap.isEmpty()) return; Set<Node> sharedNodes = findIntersectionNodes(r); List<PolyData> innerPolygons = polygon.getInnerPolygons(); List<PolyData> outerPolygons = polygon.getOuterPolygons(); List<PolyData> allPolygons = new ArrayList<>(); allPolygons.addAll(outerPolygons); allPolygons.addAll(innerPolygons); Map<PolyData, List<PolyData>> crossingPolyMap = findIntersectingWays(r, innerPolygons, outerPolygons); if (!sharedNodes.isEmpty()) { for (int i = 0; i < allPolygons.size(); i++) { PolyData pd1 = allPolygons.get(i); for (int j = i + 1; j < allPolygons.size(); j++) { PolyData pd2 = allPolygons.get(j); if (!checkProblemMap(crossingPolyMap, pd1, pd2)) { checkPolygonsForSharedNodes(r, pd1, pd2, sharedNodes); } } } } boolean checkRoles = true; for (int i = oldErrorsSize; i < errors.size(); i++) { if (errors.get(i).getSeverity() != Severity.OTHER) { checkRoles = false; break; } } if (checkRoles) { // we found no intersection or crossing between the polygons and they are closed // now we can calculate the nesting level to verify the roles with some simple node checks checkRoles(r, allPolygons, wayMap, sharedNodes); } } /** * Detect intersections of multipolygon ways at nodes. If any way node is used by more than two ways * or two times in one way and at least once in another way we found an intersection. * @param r the relation * @return List of nodes were ways intersect */ private static Set<Node> findIntersectionNodes(Relation r) { Set<Node> intersectionNodes = new HashSet<>(); Map<Node, List<Way>> nodeMap = new HashMap<>(); for (RelationMember rm : r.getMembers()) { if (!rm.isWay()) continue; int numNodes = rm.getWay().getNodesCount(); for (int i = 0; i < numNodes; i++) { Node n = rm.getWay().getNode(i); if (n.getReferrers().size() <= 1) { continue; // cannot be a problem node } List<Way> ways = nodeMap.get(n); if (ways == null) { ways = new ArrayList<>(); nodeMap.put(n, ways); } ways.add(rm.getWay()); if (ways.size() > 2 || (ways.size() == 2 && i != 0 && i + 1 != numNodes)) { intersectionNodes.add(n); } } } return intersectionNodes; } private enum ExtPolygonIntersection { EQUAL, FIRST_INSIDE_SECOND, SECOND_INSIDE_FIRST, OUTSIDE, CROSSING } private void checkPolygonsForSharedNodes(Relation r, PolyData pd1, PolyData pd2, Set<Node> allSharedNodes) { Set<Node> sharedByPolygons = new HashSet<>(allSharedNodes); sharedByPolygons.retainAll(pd1.getNodes()); sharedByPolygons.retainAll(pd2.getNodes()); if (sharedByPolygons.isEmpty()) return; // the two polygons share one or more nodes // 1st might be equal to 2nd (same nodes, same or different direction) --> error shared way segments // they overlap --> error // 1st and 2nd share segments // 1st fully inside 2nd --> okay // 2nd fully inside 1st --> okay int errorCode = RINGS_SHARE_NODES; ExtPolygonIntersection res = checkOverlapAtSharedNodes(sharedByPolygons, pd1, pd2); if (res == ExtPolygonIntersection.CROSSING) { errorCode = CROSSING_WAYS; } else if (res == ExtPolygonIntersection.EQUAL) { errorCode = EQUAL_RINGS; } if (errorCode != 0) { Set<OsmPrimitive> prims = new HashSet<>(); prims.add(r); for (Node n : sharedByPolygons) { for (OsmPrimitive p : n.getReferrers()) { if (p instanceof Way && (pd1.getWayIds().contains(p.getUniqueId()) || pd2.getWayIds().contains(p.getUniqueId()))) { prims.add(p); } } } if (errorCode == RINGS_SHARE_NODES) { errors.add(TestError.builder(this, Severity.OTHER, errorCode) .message(tr("Multipolygon rings share node(s)")) .primitives(prims) .highlight(sharedByPolygons) .build()); } else { errors.add(TestError.builder(this, Severity.WARNING, errorCode) .message(errorCode == CROSSING_WAYS ? tr("Intersection between multipolygon ways") : tr("Multipolygon rings are equal")) .primitives(prims) .highlight(sharedByPolygons) .build()); } } } private static ExtPolygonIntersection checkOverlapAtSharedNodes(Set<Node> shared, PolyData pd1, PolyData pd2) { // Idea: if two polygons share one or more nodes they can either just touch or share segments or intersect. // The insideness test is complex, so we try to reduce the number of these tests. // There is no need to check all nodes, we only have to check the node following a shared node. int[] flags = new int[2]; for (int loop = 0; loop < flags.length; loop++) { List<Node> nodes2Test = loop == 0 ? pd1.getNodes() : pd2.getNodes(); int num = nodes2Test.size() - 1; // ignore closing duplicate node int lenShared = 0; for (int i = 0; i < num; i++) { Node n = nodes2Test.get(i); if (shared.contains(n)) { ++lenShared; } else { if (i == 0 || lenShared > 0) { // do we have to treat lenShared > 1 special ? lenShared = 0; boolean inside = checkIfNodeIsInsidePolygon(n, loop == 0 ? pd2 : pd1); flags[loop] |= inside ? FOUND_INSIDE : FOUND_OUTSIDE; if (flags[loop] == (FOUND_INSIDE | FOUND_OUTSIDE)) { return ExtPolygonIntersection.CROSSING; } } } } } if ((flags[0] & FOUND_INSIDE) != 0) return ExtPolygonIntersection.FIRST_INSIDE_SECOND; if ((flags[1] & FOUND_INSIDE) != 0) return ExtPolygonIntersection.SECOND_INSIDE_FIRST; if ((flags[0] & FOUND_OUTSIDE) != (flags[1] & FOUND_OUTSIDE)) { return (flags[0] & FOUND_OUTSIDE) != 0 ? ExtPolygonIntersection.SECOND_INSIDE_FIRST : ExtPolygonIntersection.FIRST_INSIDE_SECOND; } if ((flags[0] & FOUND_OUTSIDE) != 0 && (flags[1] & FOUND_OUTSIDE) != 0) { // the two polygons may only share one or more segments but they may also intersect Area a1 = new Area(pd1.get()); Area a2 = new Area(pd2.get()); PolygonIntersection areaRes = Geometry.polygonIntersection(a1, a2, 1e-6); if (areaRes == PolygonIntersection.OUTSIDE) return ExtPolygonIntersection.OUTSIDE; return ExtPolygonIntersection.CROSSING; } return ExtPolygonIntersection.EQUAL; } /** * Helper class for calculation of nesting levels */ private static class PolygonLevel { final int level; // nesting level, even for outer, odd for inner polygons. final PolyData outerWay; PolygonLevel(PolyData pd, int level) { this.outerWay = pd; this.level = level; } } /** * Calculate the nesting levels of the polygon rings and check if calculated role matches * @param r relation (for error reporting) * @param allPolygons list of polygon rings * @param wayMap maps way ids to relation members * @param sharedNodes all nodes shared by multiple ways of this multipolygon */ private void checkRoles(Relation r, List<PolyData> allPolygons, Map<Long, RelationMember> wayMap, Set<Node> sharedNodes) { PolygonLevelFinder levelFinder = new PolygonLevelFinder(sharedNodes); List<PolygonLevel> list = levelFinder.findOuterWays(allPolygons); if (list == null || list.isEmpty()) { return; } for (PolygonLevel pol : list) { String calculatedRole = (pol.level % 2 == 0) ? "outer" : "inner"; for (long wayId : pol.outerWay.getWayIds()) { RelationMember member = wayMap.get(wayId); if (!member.getRole().equals(calculatedRole)) { errors.add(TestError.builder(this, Severity.ERROR, WRONG_MEMBER_ROLE) .message(RelationChecker.ROLE_VERIF_PROBLEM_MSG, marktr("Role for ''{0}'' should be ''{1}''"), member.getMember().getDisplayName(DefaultNameFormatter.getInstance()), calculatedRole) .primitives(Arrays.asList(r, member.getMember())) .highlight(member.getMember()) .build()); if (pol.level == 0 && "inner".equals(member.getRole())) { // maybe only add this error if we found an outer ring with correct role(s) ? errors.add(TestError.builder(this, Severity.ERROR, INNER_WAY_OUTSIDE) .message(tr("Multipolygon inner way is outside")) .primitives(Arrays.asList(r, member.getMember())) .highlight(member.getMember()) .build()); } } } } } /** * Check if a node is inside the polygon according to the insideness rules of Shape. * @param n the node * @param p the polygon * @return true if the node is inside the polygon */ private static boolean checkIfNodeIsInsidePolygon(Node n, PolyData p) { EastNorth en = n.getEastNorth(); return en != null && p.get().contains(en.getX(), en.getY()); } /** * Determine multipolygon ways which are intersecting (crossing without a common node) or sharing one or more way segments. * See also {@link CrossingWays} * @param r the relation (for error reporting) * @param innerPolygons list of inner polygons * @param outerPolygons list of outer polygons * @return map with crossing polygons */ private Map<PolyData, List<PolyData>> findIntersectingWays(Relation r, List<PolyData> innerPolygons, List<PolyData> outerPolygons) { HashMap<PolyData, List<PolyData>> crossingPolygonsMap = new HashMap<>(); HashMap<PolyData, List<PolyData>> sharedWaySegmentsPolygonsMap = new HashMap<>(); for (int loop = 0; loop < 2; loop++) { /** All way segments, grouped by cells */ final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000); /** The already detected ways in error */ final Map<List<Way>, List<WaySegment>> problemWays = new HashMap<>(50); Map<PolyData, List<PolyData>> problemPolygonMap = (loop == 0) ? crossingPolygonsMap : sharedWaySegmentsPolygonsMap; for (Way w : r.getMemberPrimitives(Way.class)) { findIntersectingWay(w, cellSegments, problemWays, loop == 1); } if (!problemWays.isEmpty()) { List<PolyData> allPolygons = new ArrayList<>(innerPolygons.size() + outerPolygons.size()); allPolygons.addAll(innerPolygons); allPolygons.addAll(outerPolygons); for (Entry<List<Way>, List<WaySegment>> entry : problemWays.entrySet()) { List<Way> ways = entry.getKey(); if (ways.size() != 2) continue; PolyData[] crossingPolys = new PolyData[2]; boolean allInner = true; for (int i = 0; i < 2; i++) { Way w = ways.get(i); for (int j = 0; j < allPolygons.size(); j++) { PolyData pd = allPolygons.get(j); if (pd.getWayIds().contains(w.getUniqueId())) { crossingPolys[i] = pd; if (j >= innerPolygons.size()) allInner = false; break; } } } boolean samePoly = false; if (crossingPolys[0] != null && crossingPolys[1] != null) { List<PolyData> crossingPolygons = problemPolygonMap.get(crossingPolys[0]); if (crossingPolygons == null) { crossingPolygons = new ArrayList<>(); problemPolygonMap.put(crossingPolys[0], crossingPolygons); } crossingPolygons.add(crossingPolys[1]); if (crossingPolys[0] == crossingPolys[1]) { samePoly = true; } } if (loop == 0 || samePoly || (loop == 1 && !allInner)) { String msg = loop == 0 ? tr("Intersection between multipolygon ways") : samePoly ? tr("Multipolygon ring contains segments twice") : tr("Multipolygon outer way shares segment(s) with other ring"); errors.add(TestError.builder(this, Severity.ERROR, CROSSING_WAYS) .message(msg) .primitives(Arrays.asList(r, ways.get(0), ways.get(1))) .highlightWaySegments(entry.getValue()) .build()); } } } } return crossingPolygonsMap; } /** * Find ways which are crossing without sharing a node. * @param w way that is member of the relation * @param cellSegments map with already collected way segments * @param crossingWays list to collect crossing ways * @param findSharedWaySegments true: find shared way segments instead of crossings */ private static void findIntersectingWay(Way w, Map<Point2D, List<WaySegment>> cellSegments, Map<List<Way>, List<WaySegment>> crossingWays, boolean findSharedWaySegments) { int nodesSize = w.getNodesCount(); for (int i = 0; i < nodesSize - 1; i++) { final WaySegment es1 = new WaySegment(w, i); final EastNorth en1 = es1.getFirstNode().getEastNorth(); final EastNorth en2 = es1.getSecondNode().getEastNorth(); if (en1 == null || en2 == null) { Main.warn("Crossing ways test (MP) skipped " + es1); continue; } for (List<WaySegment> segments : CrossingWays.getSegments(cellSegments, en1, en2)) { for (WaySegment es2 : segments) { List<WaySegment> highlight; if (es2.way == w) continue; // reported by CrossingWays.SelfIntersection if (findSharedWaySegments && !es1.isSimilar(es2)) continue; if (!findSharedWaySegments && !es1.intersects(es2)) continue; List<Way> prims = Arrays.asList(es1.way, es2.way); if ((highlight = crossingWays.get(prims)) == null) { highlight = new ArrayList<>(); highlight.add(es1); highlight.add(es2); crossingWays.put(prims, highlight); } else { highlight.add(es1); highlight.add(es2); } } segments.add(es1); } } } /** * Check if map contains combination of two given polygons. * @param problemPolyMap the map * @param pd1 1st polygon * @param pd2 2nd polygon * @return true if the combination of polygons is found in the map */ private static boolean checkProblemMap(Map<PolyData, List<PolyData>> problemPolyMap, PolyData pd1, PolyData pd2) { List<PolyData> crossingWithFirst = problemPolyMap.get(pd1); if (crossingWithFirst != null && crossingWithFirst.contains(pd2)) { return true; } List<PolyData> crossingWith2nd = problemPolyMap.get(pd2); return crossingWith2nd != null && crossingWith2nd.contains(pd1); } /** * Check for:<ul> * <li>{@link #WRONG_MEMBER_ROLE}: No useful role for multipolygon member</li> * <li>{@link #WRONG_MEMBER_TYPE}: Non-Way in multipolygon</li> * </ul> * @param r relation */ private void checkMembersAndRoles(Relation r) { for (RelationMember rm : r.getMembers()) { if (rm.isWay()) { if (!(rm.hasRole("inner", "outer") || !rm.hasRole())) { errors.add(TestError.builder(this, Severity.WARNING, WRONG_MEMBER_ROLE) .message(tr("No useful role for multipolygon member")) .primitives(Arrays.asList(r, rm.getMember())) .build()); } } else { if (!r.isBoundary() || !rm.hasRole("admin_centre", "label", "subarea", "land_area")) { errors.add(TestError.builder(this, Severity.WARNING, WRONG_MEMBER_TYPE) .message(r.isBoundary() ? tr("Non-Way in boundary") : tr("Non-Way in multipolygon")) .primitives(Arrays.asList(r, rm.getMember())) .build()); } } } } private static Collection<? extends OsmPrimitive> combineRelAndPrimitives(Relation r, Collection<? extends OsmPrimitive> primitives) { // add multipolygon in order to let user select something and fix the error if (!primitives.contains(r)) { List<OsmPrimitive> newPrimitives = new ArrayList<>(primitives); newPrimitives.add(0, r); return newPrimitives; } else { return primitives; } } /** * Check for:<ul> * <li>{@link #REPEATED_MEMBER_DIFF_ROLE}: Multipolygon member(s) repeated with different role</li> * <li>{@link #REPEATED_MEMBER_SAME_ROLE}: Multipolygon member(s) repeated with same role</li> * </ul> * @param r relation * @return true if repeated members have been detected, false otherwise */ private boolean checkRepeatedWayMembers(Relation r) { boolean hasDups = false; Map<OsmPrimitive, List<RelationMember>> seenMemberPrimitives = new HashMap<>(); for (RelationMember rm : r.getMembers()) { List<RelationMember> list = seenMemberPrimitives.get(rm.getMember()); if (list == null) { list = new ArrayList<>(2); seenMemberPrimitives.put(rm.getMember(), list); } else { hasDups = true; } list.add(rm); } if (hasDups) { List<OsmPrimitive> repeatedSameRole = new ArrayList<>(); List<OsmPrimitive> repeatedDiffRole = new ArrayList<>(); for (Entry<OsmPrimitive, List<RelationMember>> e : seenMemberPrimitives.entrySet()) { List<RelationMember> visited = e.getValue(); if (e.getValue().size() == 1) continue; // we found a duplicate member, check if the roles differ boolean rolesDiffer = false; RelationMember rm = visited.get(0); List<OsmPrimitive> primitives = new ArrayList<>(); for (int i = 1; i < visited.size(); i++) { RelationMember v = visited.get(i); primitives.add(rm.getMember()); if (!v.getRole().equals(rm.getRole())) { rolesDiffer = true; } } if (rolesDiffer) { repeatedDiffRole.addAll(primitives); } else { repeatedSameRole.addAll(primitives); } } addRepeatedMemberError(r, repeatedDiffRole, REPEATED_MEMBER_DIFF_ROLE, tr("Multipolygon member(s) repeated with different role")); addRepeatedMemberError(r, repeatedSameRole, REPEATED_MEMBER_SAME_ROLE, tr("Multipolygon member(s) repeated with same role")); } return hasDups; } private void addRepeatedMemberError(Relation r, List<OsmPrimitive> repeatedMembers, int errorCode, String msg) { if (!repeatedMembers.isEmpty()) { errors.add(TestError.builder(this, Severity.ERROR, errorCode) .message(msg) .primitives(combineRelAndPrimitives(r, repeatedMembers)) .highlight(repeatedMembers) .build()); } } @Override public Command fixError(TestError testError) { if (testError.getCode() == REPEATED_MEMBER_SAME_ROLE) { ArrayList<OsmPrimitive> primitives = new ArrayList<>(testError.getPrimitives()); if (primitives.size() >= 2 && primitives.get(0) instanceof Relation) { Relation oldRel = (Relation) primitives.get(0); Relation newRel = new Relation(oldRel); List<OsmPrimitive> repeatedPrims = primitives.subList(1, primitives.size()); List<RelationMember> oldMembers = oldRel.getMembers(); List<RelationMember> newMembers = new ArrayList<>(); HashSet<OsmPrimitive> toRemove = new HashSet<>(repeatedPrims); HashSet<OsmPrimitive> found = new HashSet<>(repeatedPrims.size()); for (RelationMember rm : oldMembers) { if (toRemove.contains(rm.getMember())) { if (!found.contains(rm.getMember())) { found.add(rm.getMember()); newMembers.add(rm); } } else { newMembers.add(rm); } } newRel.setMembers(newMembers); return new ChangeCommand(oldRel, newRel); } } return null; } @Override public boolean isFixable(TestError testError) { if (testError.getCode() == REPEATED_MEMBER_SAME_ROLE) return true; return false; } /** * Find nesting levels of polygons. Logic taken from class MultipolygonBuilder, uses different structures. */ private static class PolygonLevelFinder { private final Set<Node> sharedNodes; PolygonLevelFinder(Set<Node> sharedNodes) { this.sharedNodes = sharedNodes; } List<PolygonLevel> findOuterWays(List<PolyData> allPolygons) { return findOuterWaysRecursive(0, allPolygons); } private List<PolygonLevel> findOuterWaysRecursive(int level, List<PolyData> polygons) { final List<PolygonLevel> result = new ArrayList<>(); for (PolyData pd : polygons) { if (processOuterWay(level, polygons, result, pd) == null) { return null; } } return result; } private Object processOuterWay(int level, List<PolyData> polygons, List<PolygonLevel> result, PolyData pd) { List<PolyData> inners = findInnerWaysCandidates(pd, polygons); if (inners != null) { //add new outer polygon PolygonLevel pol = new PolygonLevel(pd, level); //process inner ways if (!inners.isEmpty()) { List<PolygonLevel> innerList = findOuterWaysRecursive(level + 1, inners); result.addAll(innerList); } result.add(pol); } return result; } /** * Check if polygon is an out-most ring, if so, collect the inners * @param outerCandidate polygon which is checked * @param polygons all polygons * @return null if outerCandidate is inside any other polygon, else a list of inner polygons (which might be empty) */ private List<PolyData> findInnerWaysCandidates(PolyData outerCandidate, List<PolyData> polygons) { List<PolyData> innerCandidates = new ArrayList<>(); for (PolyData inner : polygons) { if (inner == outerCandidate) { continue; } if (!outerCandidate.getBounds().intersects(inner.getBounds())) { continue; } boolean useIntersectionTest = false; Node unsharedOuterNode = null; Node unsharedInnerNode = getNonIntersectingNode(outerCandidate, inner); if (unsharedInnerNode != null) { if (checkIfNodeIsInsidePolygon(unsharedInnerNode, outerCandidate)) { innerCandidates.add(inner); } else { // inner is not inside outerCandidate, check if it contains outerCandidate unsharedOuterNode = getNonIntersectingNode(inner, outerCandidate); if (unsharedOuterNode != null) { if (checkIfNodeIsInsidePolygon(unsharedOuterNode, inner)) { return null; // outer is inside inner } } else { useIntersectionTest = true; } } } else { // all nodes of inner are also nodes of outerCandidate unsharedOuterNode = getNonIntersectingNode(inner, outerCandidate); if (unsharedOuterNode == null) { return null; // all nodes shared -> same ways, maybe different direction } else { if (checkIfNodeIsInsidePolygon(unsharedOuterNode, inner)) { return null; // outer is inside inner } else { useIntersectionTest = true; } } } if (useIntersectionTest) { PolygonIntersection res = Geometry.polygonIntersection(inner.getNodes(), outerCandidate.getNodes()); if (res == PolygonIntersection.FIRST_INSIDE_SECOND) innerCandidates.add(inner); else if (res == PolygonIntersection.SECOND_INSIDE_FIRST) return null; } } return innerCandidates; } /** * Find node of pd2 which is not an intersection node with pd1. * @param pd1 1st polygon * @param pd2 2nd polygon * @return node of pd2 which is not an intersection node with pd1 or null if none is found */ private Node getNonIntersectingNode(PolyData pd1, PolyData pd2) { for (Node n : pd2.getNodes()) { if (!sharedNodes.contains(n) || !pd1.getNodes().contains(n)) return n; } return null; } } }