Ticket #13307: improve_MultipolygonTest_v13.patch

File improve_MultipolygonTest_v13.patch, 15.2 KB (added by GerdP, 10 years ago)
  • 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;
     
    5555    public static final int NON_CLOSED_WAY = 1603;
    5656    /** No outer way for multipolygon */
    5757    public static final int MISSING_OUTER_WAY = 1604;
    58     /** Multipolygon inner way is outside */
    59     public static final int INNER_WAY_OUTSIDE = 1605;
    6058    /** Intersection between multipolygon ways */
    6159    public static final int CROSSING_WAYS = 1606;
    6260    /** Style for outer way mismatches / With the currently used mappaint style(s) the style for outer way mismatches the area style */
     
    111109        super.endTest();
    112110    }
    113111
    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 
    148112    @Override
    149113    public void visit(Way w) {
    150114        if (!w.isArea() && ElemStyles.hasOnlyAreaElemStyle(w)) {
     
    169133            checkMembersAndRoles(r);
    170134            checkOuterWay(r);
    171135            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);
     136            boolean hasRepeatedMembers = checkRepeatedWayMembers(r);
     137            if (!hasRepeatedMembers) {
     138                List<Node> intersectionNodes = checkIntersectionAtNodes(r);
     139                // Rest of checks is only for complete multipolygons
     140                if (!r.hasIncompleteMembers()) {
     141                    checkMemberRoleCorrectness(r);
     142                    Multipolygon polygon = new Multipolygon(r);
     143                    checkStyleConsistency(r, polygon);
     144                    checkGeometry(r, polygon, intersectionNodes);
     145                }
    181146            }
    182147        }
    183148    }
     
    205170    }
    206171
    207172    /**
    208      * Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match:<ul>
     173     * If simple joining of ways doesn't work, create new multipolygon using the logics from
     174     * CreateMultipolygonAction and see if roles match:<ul>
    209175     * <li>{@link #WRONG_MEMBER_ROLE}: Role for ''{0}'' should be ''{1}''</li>
    210176     * </ul>
    211177     * @param r relation
     178     * @return true if member roles were checked
    212179     */
    213     private void checkMemberRoleCorrectness(Relation r) {
     180    private boolean checkMemberRoleCorrectness(Relation r) {
    214181        final Pair<Relation, Relation> newMP = CreateMultipolygonAction.createMultipolygonRelation(r.getMemberPrimitives(Way.class), false);
     182
    215183        if (newMP != null) {
    216184            for (RelationMember member : r.getMembers()) {
    217185                final Collection<RelationMember> memberInNewMP = newMP.b.getMembersFor(Collections.singleton(member.getMember()));
     
    229197                }
    230198            }
    231199        }
     200        return newMP != null;
    232201    }
    233202
    234203    /**
     
    308277    /**
    309278     * Various geometry-related checks:<ul>
    310279     * <li>{@link #NON_CLOSED_WAY}: Multipolygon is not closed</li>
    311      * <li>{@link #INNER_WAY_OUTSIDE}: Multipolygon inner way is outside</li>
    312280     * <li>{@link #CROSSING_WAYS}: Intersection between multipolygon ways</li>
    313281     * </ul>
    314282     * @param r relation
    315283     * @param polygon multipolygon
     284     * @param intersectionNodes known nodes where ways of this multipolygon intersect or touch
    316285     */
    317     private void checkGeometry(Relation r, Multipolygon polygon) {
     286    private void checkGeometry(Relation r, Multipolygon polygon, List<Node> intersectionNodes) {
    318287        List<Node> openNodes = polygon.getOpenEnds();
    319288        if (!openNodes.isEmpty()) {
    320289            errors.add(TestError.builder(this, Severity.WARNING, NON_CLOSED_WAY)
     
    324293                    .build());
    325294        }
    326295
    327         // For painting is used Polygon class which works with ints only. For validation we need more precision
    328296        List<PolyData> innerPolygons = polygon.getInnerPolygons();
    329297        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);
    337             }
     298        checkCrossingWays(r, innerPolygons, outerPolygons);
     299
     300    }
     301
     302    /**
     303     * Determine multipolygon ways which are intersecting (crossing without a common node). This is not allowed.
     304     * See {@link CrossingWays}
     305     * @param r the relation (for error reporting)
     306     * @param innerPolygons list of inner polygons
     307     * @param outerPolygons list of outer polygons
     308     */
     309    private void checkCrossingWays(Relation r, List<PolyData> innerPolygons,
     310            List<PolyData> outerPolygons) {
     311        /** All way segments, grouped by cells */
     312        final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000);
     313        /** The already detected ways in error */
     314        final Map<List<Way>, List<WaySegment>> crossingWays = new HashMap<>(50);
     315
     316        for (Way w : r.getMemberPrimitives(Way.class)) {
     317            checkCrossingWay(w, r, cellSegments, crossingWays);
    338318        }
    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);
     319    }
     320
     321    /**
     322     * Find ways which are crossing without sharing a node.
     323     * @param w way that is member of the relation
     324     * @param r the relation (used for error messages)
     325     * @param crossingWays
     326     * @param cellSegments
     327     */
     328    private void checkCrossingWay(Way w, Relation r, Map<Point2D, List<WaySegment>> cellSegments, Map<List<Way>, List<WaySegment>> crossingWays) {
     329        int nodesSize = w.getNodesCount();
     330        for (int i = 0; i < nodesSize - 1; i++) {
     331            final WaySegment es1 = new WaySegment(w, i);
     332            final EastNorth en1 = es1.getFirstNode().getEastNorth();
     333            final EastNorth en2 = es1.getSecondNode().getEastNorth();
     334            if (en1 == null || en2 == null) {
     335                Main.warn("Crossing ways test skipped " + es1);
     336                continue;
    344337            }
    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;
     338            for (List<WaySegment> segments : CrossingWays.getSegments(cellSegments, en1, en2)) {
     339                for (WaySegment es2 : segments) {
     340
     341                    List<WaySegment> highlight;
     342                    if (es2.way == w)
     343                        continue; // reported by CrossingWays.SelfIntersection
     344                    if (!es1.intersects(es2))
     345                        continue;
     346
     347                    List<Way> prims = Arrays.asList(es1.way, es2.way);
     348                    if ((highlight = crossingWays.get(prims)) == null) {
     349                        highlight = new ArrayList<>();
     350                        highlight.add(es1);
     351                        highlight.add(es2);
     352                        errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS)
     353                                .message(tr("Intersection between multipolygon ways"))
     354                                .primitives(Arrays.asList(r, es1.way, es2.way))
     355                                .highlightWaySegments(highlight)
     356                                .build());
     357                        crossingWays.put(prims, highlight);
     358                    } else {
     359                        highlight.add(es1);
     360                        highlight.add(es2);
     361                    }
     362                }
     363                segments.add(es1);
    349364            }
    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());
    356             }
    357365        }
    358366    }
    359367
    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) {
     368    /**
     369     * Detect intersections of multipolygon ways at nodes. If any way node is used by more than two ways
     370     * or two times in one way and at least once in another way we found an intersection.
     371     * @param r the relation
     372     * @return List of nodes were ways intersect
     373     */
     374    private List<Node> checkIntersectionAtNodes(Relation r) {
     375        List<Node> intersectionNodes = new ArrayList<>();
     376        List<Pair<Set<Way>,List<Node>>> intersectionErrors = new ArrayList<>();
     377        HashMap<Way, String> wayRoleMap = new HashMap<>();
     378        for (RelationMember rm : r.getMembers()) {
     379            if (rm.isWay())
     380                wayRoleMap.put(rm.getWay(), rm.getRole());
     381        }
     382        Map<Node, List<Way>> nodeMap = new HashMap<>();
     383        for (RelationMember rm : r.getMembers()) {
     384            if (!rm.isWay())
     385                continue;
     386            int numNodes = rm.getWay().getNodesCount();
     387            for (int i = 0; i < numNodes; i++) {
     388                Node n = rm.getWay().getNode(i);
     389                if (n.getReferrers().size() <= 1) {
     390                    continue; // cannot be a problem node
     391                }
     392                List<Way> ways = nodeMap.get(n);
     393                if (ways == null) {
     394                    ways = new ArrayList<>();
     395                    nodeMap.put(n, ways);
     396                }
     397                ways.add(rm.getWay());
     398                if (ways.size() > 2 || (ways.size() == 2 && i != 0 && i + 1 != numNodes)) {
     399                    intersectionNodes.add(n);
     400                    boolean allInner = true;
     401                    for (Way w : ways) {
     402                        String role = wayRoleMap.get(w);
     403                        if (!"inner".equals(role))
     404                            allInner = false;
     405                    }
     406                    if (!allInner) {
     407                        Set<Way> errorWays = new HashSet<>(ways);
     408                        boolean addNew = true;
     409                        for (Pair<Set<Way>, List<Node>> pair : intersectionErrors) {
     410                            if (pair.a.size() == errorWays.size() && pair.a.containsAll(errorWays)) {
     411                                pair.b.add(n);
     412                                addNew = false;
     413                                break;
     414                            }
     415                        }
     416                        if (addNew) {
     417                            List<Node> errNodes = new ArrayList<>();
     418                            errNodes.add(n);
     419                            intersectionErrors.add(new Pair<>(errorWays, errNodes));
     420                        }
     421                    }
     422                }
     423            }
     424        }
     425        for (Pair<Set<Way>,List<Node>> pair : intersectionErrors) {
     426            // A single shared node between two ways ("touching ways") is considered okay.
     427            if (pair.b.size() > 1) {
    365428                errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS)
    366                         .message(tr("Intersection between multipolygon ways"))
    367                         .primitives(r)
    368                         .highlightNodePairs(Arrays.asList(pd.getNodes(), pdOther.getNodes()))
     429                        .message(tr("Multipolygon ways share multiple nodes"))
     430                        .primitives(pair.a)
     431                        .highlight(pair.b)
    369432                        .build());
    370433            }
    371434        }
    372         return intersection;
     435        return intersectionNodes;
    373436    }
    374437
    375438    /**