Ticket #13307: improve_MultipolygonTest_v14.patch

File improve_MultipolygonTest_v14.patch, 23.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;
     
    2121import org.openstreetmap.josm.actions.CreateMultipolygonAction;
    2222import org.openstreetmap.josm.command.ChangeCommand;
    2323import org.openstreetmap.josm.command.Command;
     24import org.openstreetmap.josm.data.coor.EastNorth;
    2425import org.openstreetmap.josm.data.osm.Node;
    2526import org.openstreetmap.josm.data.osm.OsmPrimitive;
    2627import org.openstreetmap.josm.data.osm.Relation;
    2728import org.openstreetmap.josm.data.osm.RelationMember;
    2829import org.openstreetmap.josm.data.osm.Way;
     30import org.openstreetmap.josm.data.osm.WaySegment;
    2931import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
    3032import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
    31 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
    32 import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
    3333import org.openstreetmap.josm.data.validation.OsmValidator;
    3434import org.openstreetmap.josm.data.validation.Severity;
    3535import org.openstreetmap.josm.data.validation.Test;
     
    7676    /** Multipolygon member repeated (same primitive, different role) */
    7777    public static final int REPEATED_MEMBER_DIFF_ROLE = 1615;
    7878
    79     private static volatile ElemStyles styles;
    80 
    8179    private final Set<String> keysCheckedByAnotherTest = new HashSet<>();
    8280
    8381    /**
     
    9088
    9189    @Override
    9290    public void initialize() {
    93         styles = MapPaintStyles.getStyles();
    9491    }
    9592
    9693    @Override
     
    111108        super.endTest();
    112109    }
    113110
    114     private static GeneralPath createPath(List<Node> nodes) {
    115         GeneralPath result = new GeneralPath();
    116         result.moveTo((float) nodes.get(0).getCoor().lat(), (float) nodes.get(0).getCoor().lon());
    117         for (int i = 1; i < nodes.size(); i++) {
    118             Node n = nodes.get(i);
    119             result.lineTo((float) n.getCoor().lat(), (float) n.getCoor().lon());
    120         }
    121         return result;
    122     }
    123 
    124     private static List<GeneralPath> createPolygons(List<Multipolygon.PolyData> joinedWays) {
    125         List<GeneralPath> result = new ArrayList<>();
    126         for (Multipolygon.PolyData way : joinedWays) {
    127             result.add(createPath(way.getNodes()));
    128         }
    129         return result;
    130     }
    131 
    132     private static Intersection getPolygonIntersection(GeneralPath outer, List<Node> inner) {
    133         boolean inside = false;
    134         boolean outside = false;
    135 
    136         for (Node n : inner) {
    137             boolean contains = outer.contains(n.getCoor().lat(), n.getCoor().lon());
    138             inside = inside | contains;
    139             outside = outside | !contains;
    140             if (inside & outside) {
    141                 return Intersection.CROSSING;
    142             }
    143         }
    144 
    145         return inside ? Intersection.INSIDE : Intersection.OUTSIDE;
    146     }
    147 
    148111    @Override
    149112    public void visit(Way w) {
    150113        if (!w.isArea() && ElemStyles.hasOnlyAreaElemStyle(w)) {
     
    168131        if (r.isMultipolygon()) {
    169132            checkMembersAndRoles(r);
    170133            checkOuterWay(r);
    171             checkRepeatedWayMembers(r);
    172 
    173             // Rest of checks is only for complete multipolygons
    174             if (!r.hasIncompleteMembers()) {
    175                 Multipolygon polygon = new Multipolygon(r);
    176 
    177                 // Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match.
    178                 checkMemberRoleCorrectness(r);
    179                 checkStyleConsistency(r, polygon);
    180                 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                }
    181143            }
    182144        }
    183145    }
     
    205167    }
    206168
    207169    /**
    208      * 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>
    209172     * <li>{@link #WRONG_MEMBER_ROLE}: Role for ''{0}'' should be ''{1}''</li>
    210173     * </ul>
    211174     * @param r relation
     175     * @return true if member roles were checked
    212176     */
    213     private void checkMemberRoleCorrectness(Relation r) {
     177    private boolean checkMemberRoleCorrectness(Relation r) {
    214178        final Pair<Relation, Relation> newMP = CreateMultipolygonAction.createMultipolygonRelation(r.getMemberPrimitives(Way.class), false);
     179
    215180        if (newMP != null) {
    216181            for (RelationMember member : r.getMembers()) {
    217182                final Collection<RelationMember> memberInNewMP = newMP.b.getMembersFor(Collections.singleton(member.getMember()));
     
    229194                }
    230195            }
    231196        }
     197        return newMP != null;
    232198    }
    233199
    234200    /**
     
    242208     * @param polygon multipolygon
    243209     */
    244210    private void checkStyleConsistency(Relation r, Multipolygon polygon) {
     211        ElemStyles styles = MapPaintStyles.getStyles();
    245212        if (styles != null && !"boundary".equals(r.get("type"))) {
    246213            AreaElement area = ElemStyles.getAreaElemStyle(r, false);
    247214            boolean areaStyle = area != null;
     
    313280     * </ul>
    314281     * @param r relation
    315282     * @param polygon multipolygon
     283     * @param rolesWereChecked might be used to skip most of the tests below
    316284     */
    317     private void checkGeometry(Relation r, Multipolygon polygon) {
     285    private void checkGeometry(Relation r, Multipolygon polygon, boolean rolesWereChecked) {
    318286        List<Node> openNodes = polygon.getOpenEnds();
    319287        if (!openNodes.isEmpty()) {
    320288            errors.add(TestError.builder(this, Severity.WARNING, NON_CLOSED_WAY)
     
    323291                    .highlight(openNodes)
    324292                    .build());
    325293        }
    326 
    327         // For painting is used Polygon class which works with ints only. For validation we need more precision
     294        List<Node> intersectionNodes = calcIntersectionAtNodes(r);
    328295        List<PolyData> innerPolygons = polygon.getInnerPolygons();
    329296        List<PolyData> outerPolygons = polygon.getOuterPolygons();
    330         List<GeneralPath> innerPolygonsPaths = innerPolygons.isEmpty() ? Collections.<GeneralPath>emptyList() : createPolygons(innerPolygons);
    331         List<GeneralPath> outerPolygonsPaths = createPolygons(outerPolygons);
    332         for (int i = 0; i < outerPolygons.size(); i++) {
    333             PolyData pdOuter = outerPolygons.get(i);
    334             // Check for intersection between outer members
    335             for (int j = i+1; j < outerPolygons.size(); j++) {
    336                 checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdOuter, j);
     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
     302
     303        for (PolyData outer1: outerPolygons) {
     304            for (PolyData outer2: outerPolygons) {
     305                if (outer1 != outer2 && !checkProblemMap(crossingPolyMap, outer1, outer2)) {
     306                    int foundIntersections = checkSharedNodes(r, outer1, outer2, intersectionNodes, "oo");
     307                    if (!rolesWereChecked && foundIntersections == 0)
     308                        checkIfInside(r, outer1, outer2);
     309                }
    337310            }
    338311        }
    339         for (int i = 0; i < innerPolygons.size(); i++) {
    340             PolyData pdInner = innerPolygons.get(i);
    341             // Check for intersection between inner members
    342             for (int j = i+1; j < innerPolygons.size(); j++) {
    343                 checkCrossingWays(r, innerPolygons, innerPolygonsPaths, pdInner, j);
     312
     313        for (PolyData inner1 : innerPolygons) {
     314            if (!intersectionNodes.isEmpty()) {
     315                for (PolyData outer: outerPolygons) {
     316                    checkSharedNodes(r, outer, inner1, intersectionNodes, "oi");
     317                }
    344318            }
    345             // Check for intersection between inner and outer members
    346             boolean outside = true;
    347             for (int o = 0; o < outerPolygons.size(); o++) {
    348                 outside &= checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdInner, o) == Intersection.OUTSIDE;
     319
     320            for (PolyData inner2 : innerPolygons) {
     321                if (inner1 != inner2 && !checkProblemMap(crossingPolyMap, inner1, inner2)) {
     322                    int foundIntersections = checkSharedNodes(r, inner1, inner2, intersectionNodes, "ii");
     323                    if (!rolesWereChecked && foundIntersections == 0)
     324                        checkIfInside(r, inner1, inner2);
     325
     326                }
    349327            }
    350             if (outside) {
    351                 errors.add(TestError.builder(this, Severity.WARNING, INNER_WAY_OUTSIDE)
    352                         .message(tr("Multipolygon inner way is outside"))
    353                         .primitives(r)
    354                         .highlightNodePairs(Collections.singletonList(pdInner.getNodes()))
    355                         .build());
     328            if (!rolesWereChecked) {
     329                // Find inner polygons which are not inside any outer
     330                boolean outside = true;
     331                boolean crossingWithOuter = false;
     332                EastNorth innerPoint = inner1.getNodes().get(0).getEastNorth();
     333                for (PolyData outer : outerPolygons) {
     334                    if (checkProblemMap(crossingPolyMap, inner1, outer)) {
     335                        crossingWithOuter = true;
     336                        break;
     337                    }
     338                    outside &= !outer.get().contains(innerPoint.getX(), innerPoint.getY());
     339                    if (!outside)
     340                        break;
     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                }
    356349            }
    357350        }
    358351    }
    359352
    360     private Intersection checkCrossingWays(Relation r, List<PolyData> polygons, List<GeneralPath> polygonsPaths, PolyData pd, int idx) {
    361         Intersection intersection = getPolygonIntersection(polygonsPaths.get(idx), pd.getNodes());
    362         if (intersection == Intersection.CROSSING) {
    363             PolyData pdOther = polygons.get(idx);
    364             if (pdOther != null) {
     353    /**
     354     * Check two polygons for intersections at shared nodes.
     355     * @param r relation
     356     * @param p1 1st polygon
     357     * @param p2 2nd polygon
     358     * @param intersectionNodes list of all intersection nodes in the multipolygon relation
     359     * @param roles String with two chars ('o' or 'i') two pass info about roles of the two polygons
     360     * @return number of intersections between these two polygons
     361     */
     362    private int checkSharedNodes(Relation r, PolyData p1, PolyData p2, List<Node> intersectionNodes, String roles) {
     363        if (intersectionNodes.isEmpty())
     364            return 0;
     365
     366        int numIntersections = 0;
     367        int inside = 0;
     368        int outside = 0;
     369        for (Node n : p2.getNodes()) {
     370            if (intersectionNodes.contains(n) && p1.getNodes().contains(n)) {
     371                ++numIntersections;
     372            } else {
     373                EastNorth en = n.getEastNorth();
     374                if (en == null || en.isValid() == false)
     375                    continue;
     376                if (p1.get().contains(en.getX(), en.getY()))
     377                    ++inside;
     378                else
     379                    ++outside;
     380            }
     381        }
     382        if (numIntersections > 0) {
     383            if (inside > 0 && (outside > 0 || "oo".equals(roles))) {
    365384                errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS)
    366385                        .message(tr("Intersection between multipolygon ways"))
    367386                        .primitives(r)
    368                         .highlightNodePairs(Arrays.asList(pd.getNodes(), pdOther.getNodes()))
     387                        .highlight(intersectionNodes)
    369388                        .build());
    370389            }
    371390        }
    372         return intersection;
     391        return numIntersections;
    373392    }
    374393
     394    private void checkIfInside(Relation r, PolyData p1, PolyData p2) {
     395        Node n = p2.getNodes().get(0);
     396        EastNorth en = n.getEastNorth();
     397        if (en != null && en.isValid() && p1.get().contains(en.getX(), en.getY())) {
     398            errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS)
     399                    .message(tr("Multipolygon way inside other multipolygon way with same role"))
     400                    .primitives(r)
     401                    .highlight(p2.getNodes())
     402                    .build());
     403
     404        }
     405    }
     406
    375407    /**
     408     * Determine multipolygon ways which are intersecting (crossing without a common node) or sharing one or more way segments.
     409     * See also {@link CrossingWays}
     410     * @param r the relation (for error reporting)
     411     * @param innerPolygons list of inner polygons
     412     * @param outerPolygons list of outer polygons
     413     * @return map with crossing polygons
     414     */
     415    private Map<PolyData, List<PolyData>> findIntersectingWays(Relation r, List<PolyData> innerPolygons,
     416            List<PolyData> outerPolygons) {
     417        HashMap<PolyData, List<PolyData>> crossingPolygonsMap = new HashMap<>();
     418        HashMap<PolyData, List<PolyData>> sharedWaySegmentsPolygonsMap = new HashMap<>();
     419
     420        for (int loop = 0; loop < 2; loop++) {
     421            /** All way segments, grouped by cells */
     422            final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000);
     423            /** The already detected ways in error */
     424            final Map<List<Way>, List<WaySegment>> problemWays = new HashMap<>(50);
     425
     426            Map<PolyData, List<PolyData>> problemPolygonMap = (loop == 0) ? crossingPolygonsMap : sharedWaySegmentsPolygonsMap;
     427
     428            for (Way w : r.getMemberPrimitives(Way.class)) {
     429                findIntersectingWay(w, r, cellSegments, problemWays, loop == 1);
     430            }
     431
     432            if (!problemWays.isEmpty()) {
     433                List<PolyData> allPolygons = new ArrayList<>(innerPolygons.size() + outerPolygons.size());
     434                allPolygons.addAll(innerPolygons);
     435                allPolygons.addAll(outerPolygons);
     436
     437                for (Entry<List<Way>, List<WaySegment>> entry : problemWays.entrySet()) {
     438                    List<Way> ways = entry.getKey();
     439                    if (ways.size() != 2)
     440                        continue;
     441                    PolyData[] crossingPolys = new PolyData[2];
     442                    boolean allInner = true;
     443                    for (int i = 0; i < 2; i++) {
     444                        Way w = ways.get(i);
     445                        for (int j = 0; j < allPolygons.size(); j++) {
     446                            PolyData pd = allPolygons.get(j);
     447                            if (pd.getWayIds().contains(w.getUniqueId())) {
     448                                crossingPolys[i] = pd;
     449                                if (j >= innerPolygons.size())
     450                                    allInner = false;
     451                                break;
     452                            }
     453                        }
     454                    }
     455                    if (loop == 0 || (loop == 1 && !allInner)) {
     456                        String msg = (loop == 0) ? tr("Intersection between multipolygon ways")
     457                                : tr("Multipolygon ways share segments");
     458                        errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS)
     459                                .message(msg)
     460                                .primitives(Arrays.asList(r, ways.get(0), ways.get(1)))
     461                                .highlightWaySegments(entry.getValue())
     462                                .build());
     463                    }
     464                    if (crossingPolys[0] != null && crossingPolys[1] != null) {
     465                        List<PolyData> crossingPolygons = problemPolygonMap.get(crossingPolys[0]);
     466                        if (crossingPolygons == null) {
     467                            crossingPolygons = new ArrayList<>();
     468                            problemPolygonMap.put(crossingPolys[0], crossingPolygons);
     469                        }
     470                        crossingPolygons.add(crossingPolys[1]);
     471                    }
     472                }
     473            }
     474        }
     475        return crossingPolygonsMap;
     476    }
     477
     478    /**
     479     * Find ways which are crossing without sharing a node.
     480     * @param w way that is member of the relation
     481     * @param r the relation (used for error messages)
     482     * @param cellSegments map with already collected way segments
     483     * @param crossingWays list to collect crossing ways
     484     * @param findSharedWaySegments true: find shared way segments instead of crossings
     485     */
     486    private void findIntersectingWay(Way w, Relation r, Map<Point2D, List<WaySegment>> cellSegments,
     487            Map<List<Way>, List<WaySegment>> crossingWays, boolean findSharedWaySegments) {
     488        int nodesSize = w.getNodesCount();
     489        for (int i = 0; i < nodesSize - 1; i++) {
     490            final WaySegment es1 = new WaySegment(w, i);
     491            final EastNorth en1 = es1.getFirstNode().getEastNorth();
     492            final EastNorth en2 = es1.getSecondNode().getEastNorth();
     493            if (en1 == null || en2 == null) {
     494                Main.warn("Crossing ways test skipped " + es1);
     495                continue;
     496            }
     497            for (List<WaySegment> segments : CrossingWays.getSegments(cellSegments, en1, en2)) {
     498                for (WaySegment es2 : segments) {
     499
     500                    List<WaySegment> highlight;
     501                    if (es2.way == w)
     502                        continue; // reported by CrossingWays.SelfIntersection
     503                    if (findSharedWaySegments && !es1.isSimilar(es2))
     504                        continue;
     505                    if (!findSharedWaySegments && !es1.intersects(es2))
     506                        continue;
     507
     508                    List<Way> prims = Arrays.asList(es1.way, es2.way);
     509                    if ((highlight = crossingWays.get(prims)) == null) {
     510                        highlight = new ArrayList<>();
     511                        highlight.add(es1);
     512                        highlight.add(es2);
     513                        crossingWays.put(prims, highlight);
     514                    } else {
     515                        highlight.add(es1);
     516                        highlight.add(es2);
     517                    }
     518                }
     519                segments.add(es1);
     520            }
     521        }
     522    }
     523
     524    /**
     525     * Detect intersections of multipolygon ways at nodes. If any way node is used by more than two ways
     526     * or two times in one way and at least once in another way we found an intersection.
     527     * @param r the relation
     528     * @return List of nodes were ways intersect
     529     */
     530    private List<Node> calcIntersectionAtNodes(Relation r) {
     531        List<Node> intersectionNodes = new ArrayList<>();
     532        Map<Node, List<Way>> nodeMap = new HashMap<>();
     533        for (RelationMember rm : r.getMembers()) {
     534            if (!rm.isWay())
     535                continue;
     536            int numNodes = rm.getWay().getNodesCount();
     537            for (int i = 0; i < numNodes; i++) {
     538                Node n = rm.getWay().getNode(i);
     539                if (n.getReferrers().size() <= 1) {
     540                    continue; // cannot be a problem node
     541                }
     542                List<Way> ways = nodeMap.get(n);
     543                if (ways == null) {
     544                    ways = new ArrayList<>();
     545                    nodeMap.put(n, ways);
     546                }
     547                ways.add(rm.getWay());
     548                if (ways.size() > 2 || (ways.size() == 2 && i != 0 && i + 1 != numNodes)) {
     549                    intersectionNodes.add(n);
     550                }
     551            }
     552        }
     553        return intersectionNodes;
     554    }
     555
     556    /**
     557     * Check if map contains combination of two given polygons.
     558     * @param problemPolyMap the map
     559     * @param pd1 1st polygon
     560     * @param pd2 2nd polygon
     561     * @return true if the combination of polygons is found in the map
     562     */
     563    private boolean checkProblemMap(Map<PolyData, List<PolyData>> problemPolyMap, PolyData pd1, PolyData pd2) {
     564        List<PolyData> crossingWithFirst = problemPolyMap.get(pd1);
     565        if (crossingWithFirst != null) {
     566            if (crossingWithFirst.contains(pd2))
     567                return true;
     568        }
     569        List<PolyData> crossingWith2nd = problemPolyMap.get(pd2);
     570        if (crossingWith2nd != null) {
     571            if (crossingWith2nd.contains(pd1))
     572                return true;
     573        }
     574        return false;
     575    }
     576
     577    /**
    376578     * Check for:<ul>
    377579     * <li>{@link #WRONG_MEMBER_ROLE}: No useful role for multipolygon member</li>
    378580     * <li>{@link #WRONG_MEMBER_TYPE}: Non-Way in multipolygon</li>
     
    511713                        }
    512714                    }
    513715                    newRel.setMembers(newMembers);
    514                     return new ChangeCommand (oldRel, newRel);
     716                    return new ChangeCommand(oldRel, newRel);
    515717                }
    516718            }
    517719        }