Ticket #13307: improve_MultipolygonTest_v15.patch

File improve_MultipolygonTest_v15.patch, 24.0 KB (added by GerdP, 10 years ago)
  • src/org/openstreetmap/josm/data/osm/WaySegment.java

     
    114114                s2.getSecondNode().getEastNorth().east(), s2.getSecondNode().getEastNorth().north());
    115115    }
    116116
     117    /**
     118     * Checks whether this segment and another way segment share the same points
     119     * @param s2 The other segment
     120     * @return true if other way segment is the same or reverse
     121     */
     122    public boolean isSimilar(WaySegment s2) {
     123        if (getFirstNode().equals(s2.getFirstNode()) && getSecondNode().equals(s2.getSecondNode()))
     124            return true;
     125        if (getFirstNode().equals(s2.getSecondNode()) && getSecondNode().equals(s2.getFirstNode()))
     126            return true;
     127        return false;
     128    }
     129
    117130    @Override
    118131    public String toString() {
    119132        return "WaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']';
  • src/org/openstreetmap/josm/data/validation/tests/MultipolygonTest.java

     
    55import static org.openstreetmap.josm.tools.I18n.tr;
    66import static org.openstreetmap.josm.tools.I18n.trn;
    77
    8 import java.awt.geom.GeneralPath;
     8import java.awt.geom.Point2D;
    99import java.util.ArrayList;
    1010import java.util.Arrays;
    1111import java.util.Collection;
     
    1717import java.util.Map.Entry;
    1818import java.util.Set;
    1919
     20import org.openstreetmap.josm.Main;
    2021import org.openstreetmap.josm.actions.CreateMultipolygonAction;
    2122import org.openstreetmap.josm.command.ChangeCommand;
    2223import org.openstreetmap.josm.command.Command;
     24import org.openstreetmap.josm.data.coor.EastNorth;
    2325import org.openstreetmap.josm.data.osm.Node;
    2426import org.openstreetmap.josm.data.osm.OsmPrimitive;
    2527import org.openstreetmap.josm.data.osm.Relation;
    2628import org.openstreetmap.josm.data.osm.RelationMember;
    2729import org.openstreetmap.josm.data.osm.Way;
     30import org.openstreetmap.josm.data.osm.WaySegment;
    2831import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
    2932import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
    30 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
    3133import org.openstreetmap.josm.data.validation.OsmValidator;
    3234import org.openstreetmap.josm.data.validation.Severity;
    3335import org.openstreetmap.josm.data.validation.Test;
     
    7476    /** Multipolygon member repeated (same primitive, different role) */
    7577    public static final int REPEATED_MEMBER_DIFF_ROLE = 1615;
    7678
    77     private static volatile ElemStyles styles;
    78 
    7979    private final Set<String> keysCheckedByAnotherTest = new HashSet<>();
    8080
    8181    /**
     
    8888
    8989    @Override
    9090    public void initialize() {
    91         styles = MapPaintStyles.getStyles();
    9291    }
    9392
    9493    @Override
     
    109108        super.endTest();
    110109    }
    111110
    112     private static GeneralPath createPath(List<Node> nodes) {
    113         GeneralPath result = new GeneralPath();
    114         result.moveTo((float) nodes.get(0).getCoor().lat(), (float) nodes.get(0).getCoor().lon());
    115         for (int i = 1; i < nodes.size(); i++) {
    116             Node n = nodes.get(i);
    117             result.lineTo((float) n.getCoor().lat(), (float) n.getCoor().lon());
    118         }
    119         return result;
    120     }
    121 
    122     private static List<GeneralPath> createPolygons(List<Multipolygon.PolyData> joinedWays) {
    123         List<GeneralPath> result = new ArrayList<>();
    124         for (Multipolygon.PolyData way : joinedWays) {
    125             result.add(createPath(way.getNodes()));
    126         }
    127         return result;
    128     }
    129 
    130     private static Intersection getPolygonIntersection(GeneralPath outer, List<Node> inner) {
    131         boolean inside = false;
    132         boolean outside = false;
    133 
    134         for (Node n : inner) {
    135             boolean contains = outer.contains(n.getCoor().lat(), n.getCoor().lon());
    136             inside = inside | contains;
    137             outside = outside | !contains;
    138             if (inside & outside) {
    139                 return Intersection.CROSSING;
    140             }
    141         }
    142 
    143         return inside ? Intersection.INSIDE : Intersection.OUTSIDE;
    144     }
    145 
    146111    @Override
    147112    public void visit(Way w) {
    148113        if (!w.isArea() && ElemStyles.hasOnlyAreaElemStyle(w)) {
     
    166131        if (r.isMultipolygon()) {
    167132            checkMembersAndRoles(r);
    168133            checkOuterWay(r);
    169             checkRepeatedWayMembers(r);
    170 
    171             // Rest of checks is only for complete multipolygons
    172             if (!r.hasIncompleteMembers()) {
    173                 Multipolygon polygon = new Multipolygon(r);
    174 
    175                 // Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match.
    176                 checkMemberRoleCorrectness(r);
    177                 checkStyleConsistency(r, polygon);
    178                 checkGeometry(r, polygon);
     134            boolean hasRepeatedMembers = checkRepeatedWayMembers(r);
     135            if (!hasRepeatedMembers) {
     136                // Rest of checks is only for complete multipolygons
     137                if (!r.hasIncompleteMembers()) {
     138                    boolean rolesWereChecked = checkMemberRoleCorrectness(r);
     139                    Multipolygon polygon = new Multipolygon(r);
     140                    checkStyleConsistency(r, polygon);
     141                    checkGeometry(r, polygon, rolesWereChecked);
     142                }
    179143            }
    180144        }
    181145    }
     
    203167    }
    204168
    205169    /**
    206      * Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match:<ul>
     170     * If simple joining of ways doesn't work, create new multipolygon using the logics from
     171     * CreateMultipolygonAction and see if roles match:<ul>
    207172     * <li>{@link #WRONG_MEMBER_ROLE}: Role for ''{0}'' should be ''{1}''</li>
    208173     * </ul>
    209174     * @param r relation
     175     * @return true if member roles were checked
    210176     */
    211     private void checkMemberRoleCorrectness(Relation r) {
     177    private boolean checkMemberRoleCorrectness(Relation r) {
    212178        final Pair<Relation, Relation> newMP = CreateMultipolygonAction.createMultipolygonRelation(r.getMemberPrimitives(Way.class), false);
     179
    213180        if (newMP != null) {
    214181            for (RelationMember member : r.getMembers()) {
    215182                final Collection<RelationMember> memberInNewMP = newMP.b.getMembersFor(Collections.singleton(member.getMember()));
     
    227194                }
    228195            }
    229196        }
     197        return newMP != null;
    230198    }
    231199
    232200    /**
     
    240208     * @param polygon multipolygon
    241209     */
    242210    private void checkStyleConsistency(Relation r, Multipolygon polygon) {
     211        ElemStyles styles = MapPaintStyles.getStyles();
    243212        if (styles != null && !"boundary".equals(r.get("type"))) {
    244213            AreaElement area = ElemStyles.getAreaElemStyle(r, false);
    245214            boolean areaStyle = area != null;
     
    311280     * </ul>
    312281     * @param r relation
    313282     * @param polygon multipolygon
     283     * @param rolesWereChecked is used to skip some tests
    314284     */
    315     private void checkGeometry(Relation r, Multipolygon polygon) {
     285    private void checkGeometry(Relation r, Multipolygon polygon, boolean rolesWereChecked) {
    316286        List<Node> openNodes = polygon.getOpenEnds();
    317287        if (!openNodes.isEmpty()) {
    318288            errors.add(TestError.builder(this, Severity.WARNING, NON_CLOSED_WAY)
     
    321291                    .highlight(openNodes)
    322292                    .build());
    323293        }
    324 
    325         // For painting is used Polygon class which works with ints only. For validation we need more precision
     294        Collection<Node> intersectionNodes = calcIntersectionAtNodes(r);
    326295        List<PolyData> innerPolygons = polygon.getInnerPolygons();
    327296        List<PolyData> outerPolygons = polygon.getOuterPolygons();
    328         List<GeneralPath> innerPolygonsPaths = innerPolygons.isEmpty() ? Collections.<GeneralPath>emptyList() : createPolygons(innerPolygons);
    329         List<GeneralPath> outerPolygonsPaths = createPolygons(outerPolygons);
     297        Map<PolyData, List<PolyData>> crossingPolyMap = findIntersectingWays(r, innerPolygons, outerPolygons);
     298
     299
     300        // Polygons may intersect without crossing ways when one polygon lies completely inside the other
     301        // or when they cross at shared nodes
    330302        for (int i = 0; i < outerPolygons.size(); i++) {
    331             PolyData pdOuter = outerPolygons.get(i);
    332             // Check for intersection between outer members
    333             for (int j = i+1; j < outerPolygons.size(); j++) {
    334                 checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdOuter, j);
     303            PolyData outer1 = outerPolygons.get(i);
     304            for (int j = i + 1; j < outerPolygons.size(); j++) {
     305                PolyData outer2 = outerPolygons.get(j);
     306                if (!checkProblemMap(crossingPolyMap, outer1, outer2)) {
     307                    checkSharedNodes(r, outer1, outer2, intersectionNodes);
     308                }
    335309            }
    336310        }
     311
     312
    337313        for (int i = 0; i < innerPolygons.size(); i++) {
    338             PolyData pdInner = innerPolygons.get(i);
    339             // Check for intersection between inner members
     314            PolyData inner1 = innerPolygons.get(i);
     315            if (!intersectionNodes.isEmpty()) {
     316                for (PolyData outer: outerPolygons) {
     317                    checkSharedNodes(r, outer, inner1, intersectionNodes);
     318                }
     319            }
     320
    340321            for (int j = i+1; j < innerPolygons.size(); j++) {
    341                 checkCrossingWays(r, innerPolygons, innerPolygonsPaths, pdInner, j);
     322                PolyData inner2 = innerPolygons.get(j);
     323                if (!checkProblemMap(crossingPolyMap, inner1, inner2)) {
     324                    checkSharedNodes(r, inner1, inner2, intersectionNodes);
     325                }
    342326            }
    343             // Check for intersection between inner and outer members
    344             boolean outside = true;
    345             for (int o = 0; o < outerPolygons.size(); o++) {
    346                 outside &= checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdInner, o) == Intersection.OUTSIDE;
     327            if (!rolesWereChecked) {
     328                // Find inner polygons which are not inside any outer
     329                boolean outside = true;
     330                boolean crossingWithOuter = false;
     331                EastNorth innerPoint = inner1.getNodes().get(0).getEastNorth();
     332                for (PolyData outer : outerPolygons) {
     333                    if (checkProblemMap(crossingPolyMap, inner1, outer)) {
     334                        crossingWithOuter = true;
     335                        break;
     336                    }
     337                    outside &= !outer.get().contains(innerPoint.getX(), innerPoint.getY());
     338                    if (!outside) {
     339                        break;
     340                    }
     341                }
     342                if (outside && !crossingWithOuter) {
     343                    errors.add(TestError.builder(this, Severity.WARNING, INNER_WAY_OUTSIDE)
     344                            .message(tr("Multipolygon inner way is outside"))
     345                            .primitives(r)
     346                            .highlightNodePairs(Collections.singletonList(inner1.getNodes()))
     347                            .build());
     348                }
    347349            }
    348             if (outside) {
    349                 errors.add(TestError.builder(this, Severity.WARNING, INNER_WAY_OUTSIDE)
    350                         .message(tr("Multipolygon inner way is outside"))
    351                         .primitives(r)
    352                         .highlightNodePairs(Collections.singletonList(pdInner.getNodes()))
    353                         .build());
    354             }
    355350        }
    356351    }
    357352
    358     private Intersection checkCrossingWays(Relation r, List<PolyData> polygons, List<GeneralPath> polygonsPaths, PolyData pd, int idx) {
    359         Intersection intersection = getPolygonIntersection(polygonsPaths.get(idx), pd.getNodes());
    360         if (intersection == Intersection.CROSSING) {
    361             PolyData pdOther = polygons.get(idx);
    362             if (pdOther != null) {
     353    /**
     354     * Check two polygons for intersections at shared nodes. Only create an error if the intersection of the two
     355     * polygons is neither equal to p1 nor to p2.
     356     * @param r relation
     357     * @param p1 1st polygon
     358     * @param p2 2nd polygon
     359     * @param intersectionNodes collection of all intersection nodes in the multipolygon relation
     360     */
     361    private void checkSharedNodes(Relation r, PolyData p1, PolyData p2, Collection<Node> intersectionNodes) {
     362        if (intersectionNodes.isEmpty())
     363            return;
     364
     365        boolean p1IsInsideP2 = false;
     366        boolean p1IsOutsideP2 = false;
     367        Set<Node> sharedNodes = new HashSet<>();
     368        boolean foundInOutIntersection = false;
     369        for (Node n : p1.getNodes()) {
     370            if (intersectionNodes.contains(n) && p2.getNodes().contains(n)) {
     371                sharedNodes.add(n);
     372            } else {
     373                if (checkIfNodeIsInsidePolygon(n, p2)) {
     374                    p1IsInsideP2 = true;
     375                } else {
     376                    p1IsOutsideP2 = true;
     377                }
     378            }
     379        }
     380
     381        if (!sharedNodes.isEmpty()) {
     382            if (p1IsOutsideP2) {
     383                if (p1IsInsideP2) {
     384                    foundInOutIntersection = true;
     385                } else {
     386                    // find out if p2 is fully contained in p1 or fully outside (only sharing nodes)
     387                    boolean isInsideP1 = false;
     388                    boolean isOutsideP1 = false;
     389                    for (Node n : p2.getNodes()) {
     390                        if (sharedNodes.contains(n))
     391                            continue;
     392                        if (checkIfNodeIsInsidePolygon(n, p1)) {
     393                            isInsideP1 = true;
     394                        } else {
     395                            isOutsideP1 = true;
     396                        }
     397                        if (isInsideP1 && isOutsideP1) {
     398                            foundInOutIntersection = true;
     399                            break;
     400                        }
     401                    }
     402                }
     403            }
     404            if (foundInOutIntersection) {
    363405                errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS)
    364406                        .message(tr("Intersection between multipolygon ways"))
    365407                        .primitives(r)
    366                         .highlightNodePairs(Arrays.asList(pd.getNodes(), pdOther.getNodes()))
     408                        .highlight(sharedNodes)
    367409                        .build());
    368410            }
    369411        }
    370         return intersection;
    371412    }
    372413
    373414    /**
     415     * Check if the node is inside the polygon according to the insideness rules of Shape.
     416     * @param n the node
     417     * @param p the polygon
     418     * @return true if the node is inside the polygon
     419     */
     420    private boolean checkIfNodeIsInsidePolygon(Node n, PolyData p) {
     421        EastNorth en = n.getEastNorth();
     422        return (en != null && en.isValid() && p.get().contains(en.getX(), en.getY()));
     423    }
     424
     425    /**
     426     * Determine multipolygon ways which are intersecting (crossing without a common node) or sharing one or more way segments.
     427     * See also {@link CrossingWays}
     428     * @param r the relation (for error reporting)
     429     * @param innerPolygons list of inner polygons
     430     * @param outerPolygons list of outer polygons
     431     * @return map with crossing polygons
     432     */
     433    private Map<PolyData, List<PolyData>> findIntersectingWays(Relation r, List<PolyData> innerPolygons,
     434            List<PolyData> outerPolygons) {
     435        HashMap<PolyData, List<PolyData>> crossingPolygonsMap = new HashMap<>();
     436        HashMap<PolyData, List<PolyData>> sharedWaySegmentsPolygonsMap = new HashMap<>();
     437
     438        for (int loop = 0; loop < 2; loop++) {
     439            /** All way segments, grouped by cells */
     440            final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000);
     441            /** The already detected ways in error */
     442            final Map<List<Way>, List<WaySegment>> problemWays = new HashMap<>(50);
     443
     444            Map<PolyData, List<PolyData>> problemPolygonMap = (loop == 0) ? crossingPolygonsMap : sharedWaySegmentsPolygonsMap;
     445
     446            for (Way w : r.getMemberPrimitives(Way.class)) {
     447                findIntersectingWay(w, r, cellSegments, problemWays, loop == 1);
     448            }
     449
     450            if (!problemWays.isEmpty()) {
     451                List<PolyData> allPolygons = new ArrayList<>(innerPolygons.size() + outerPolygons.size());
     452                allPolygons.addAll(innerPolygons);
     453                allPolygons.addAll(outerPolygons);
     454
     455                for (Entry<List<Way>, List<WaySegment>> entry : problemWays.entrySet()) {
     456                    List<Way> ways = entry.getKey();
     457                    if (ways.size() != 2)
     458                        continue;
     459                    PolyData[] crossingPolys = new PolyData[2];
     460                    boolean allInner = true;
     461                    for (int i = 0; i < 2; i++) {
     462                        Way w = ways.get(i);
     463                        for (int j = 0; j < allPolygons.size(); j++) {
     464                            PolyData pd = allPolygons.get(j);
     465                            if (pd.getWayIds().contains(w.getUniqueId())) {
     466                                crossingPolys[i] = pd;
     467                                if (j >= innerPolygons.size())
     468                                    allInner = false;
     469                                break;
     470                            }
     471                        }
     472                    }
     473                    if (loop == 0 || (loop == 1 && !allInner)) {
     474                        String msg = (loop == 0) ? tr("Intersection between multipolygon ways")
     475                                : tr("Multipolygon ways share segments");
     476                        errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS)
     477                                .message(msg)
     478                                .primitives(Arrays.asList(r, ways.get(0), ways.get(1)))
     479                                .highlightWaySegments(entry.getValue())
     480                                .build());
     481                    }
     482                    if (crossingPolys[0] != null && crossingPolys[1] != null) {
     483                        List<PolyData> crossingPolygons = problemPolygonMap.get(crossingPolys[0]);
     484                        if (crossingPolygons == null) {
     485                            crossingPolygons = new ArrayList<>();
     486                            problemPolygonMap.put(crossingPolys[0], crossingPolygons);
     487                        }
     488                        crossingPolygons.add(crossingPolys[1]);
     489                    }
     490                }
     491            }
     492        }
     493        return crossingPolygonsMap;
     494    }
     495
     496    /**
     497     * Find ways which are crossing without sharing a node.
     498     * @param w way that is member of the relation
     499     * @param r the relation (used for error messages)
     500     * @param cellSegments map with already collected way segments
     501     * @param crossingWays list to collect crossing ways
     502     * @param findSharedWaySegments true: find shared way segments instead of crossings
     503     */
     504    private void findIntersectingWay(Way w, Relation r, Map<Point2D, List<WaySegment>> cellSegments,
     505            Map<List<Way>, List<WaySegment>> crossingWays, boolean findSharedWaySegments) {
     506        int nodesSize = w.getNodesCount();
     507        for (int i = 0; i < nodesSize - 1; i++) {
     508            final WaySegment es1 = new WaySegment(w, i);
     509            final EastNorth en1 = es1.getFirstNode().getEastNorth();
     510            final EastNorth en2 = es1.getSecondNode().getEastNorth();
     511            if (en1 == null || en2 == null) {
     512                Main.warn("Crossing ways test skipped " + es1);
     513                continue;
     514            }
     515            for (List<WaySegment> segments : CrossingWays.getSegments(cellSegments, en1, en2)) {
     516                for (WaySegment es2 : segments) {
     517
     518                    List<WaySegment> highlight;
     519                    if (es2.way == w)
     520                        continue; // reported by CrossingWays.SelfIntersection
     521                    if (findSharedWaySegments && !es1.isSimilar(es2))
     522                        continue;
     523                    if (!findSharedWaySegments && !es1.intersects(es2))
     524                        continue;
     525
     526                    List<Way> prims = Arrays.asList(es1.way, es2.way);
     527                    if ((highlight = crossingWays.get(prims)) == null) {
     528                        highlight = new ArrayList<>();
     529                        highlight.add(es1);
     530                        highlight.add(es2);
     531                        crossingWays.put(prims, highlight);
     532                    } else {
     533                        highlight.add(es1);
     534                        highlight.add(es2);
     535                    }
     536                }
     537                segments.add(es1);
     538            }
     539        }
     540    }
     541
     542    /**
     543     * Detect intersections of multipolygon ways at nodes. If any way node is used by more than two ways
     544     * or two times in one way and at least once in another way we found an intersection.
     545     * @param r the relation
     546     * @return List of nodes were ways intersect
     547     */
     548    private Set<Node> calcIntersectionAtNodes(Relation r) {
     549        Set<Node> intersectionNodes = new HashSet<>();
     550        Map<Node, List<Way>> nodeMap = new HashMap<>();
     551        for (RelationMember rm : r.getMembers()) {
     552            if (!rm.isWay())
     553                continue;
     554            int numNodes = rm.getWay().getNodesCount();
     555            for (int i = 0; i < numNodes; i++) {
     556                Node n = rm.getWay().getNode(i);
     557                if (n.getReferrers().size() <= 1) {
     558                    continue; // cannot be a problem node
     559                }
     560                List<Way> ways = nodeMap.get(n);
     561                if (ways == null) {
     562                    ways = new ArrayList<>();
     563                    nodeMap.put(n, ways);
     564                }
     565                ways.add(rm.getWay());
     566                if (ways.size() > 2 || (ways.size() == 2 && i != 0 && i + 1 != numNodes)) {
     567                    intersectionNodes.add(n);
     568                }
     569            }
     570        }
     571        return intersectionNodes;
     572    }
     573
     574    /**
     575     * Check if map contains combination of two given polygons.
     576     * @param problemPolyMap the map
     577     * @param pd1 1st polygon
     578     * @param pd2 2nd polygon
     579     * @return true if the combination of polygons is found in the map
     580     */
     581    private boolean checkProblemMap(Map<PolyData, List<PolyData>> problemPolyMap, PolyData pd1, PolyData pd2) {
     582        List<PolyData> crossingWithFirst = problemPolyMap.get(pd1);
     583        if (crossingWithFirst != null) {
     584            if (crossingWithFirst.contains(pd2))
     585                return true;
     586        }
     587        List<PolyData> crossingWith2nd = problemPolyMap.get(pd2);
     588        if (crossingWith2nd != null) {
     589            if (crossingWith2nd.contains(pd1))
     590                return true;
     591        }
     592        return false;
     593    }
     594
     595    /**
    374596     * Check for:<ul>
    375597     * <li>{@link #WRONG_MEMBER_ROLE}: No useful role for multipolygon member</li>
    376598     * <li>{@link #WRONG_MEMBER_TYPE}: Non-Way in multipolygon</li>
  • test/unit/org/openstreetmap/josm/data/validation/tests/MultipolygonTestTest.java

     
    8383    public void testMultipolygonFile() throws Exception {
    8484        ValidatorTestUtils.testSampleFile("data_nodist/multipolygon.osm",
    8585                ds -> ds.getRelations().stream().filter(Relation::isMultipolygon).collect(Collectors.toList()),
    86                 name -> name.startsWith("06") || name.startsWith("07"), MULTIPOLYGON_TEST, RELATION_TEST);
     86                name -> name.startsWith("06") || name.startsWith("07") || name.startsWith("08"), MULTIPOLYGON_TEST, RELATION_TEST);
    8787    }
    8888}