Ticket #13307: improve_MultipolygonTest_v5.patch

File improve_MultipolygonTest_v5.patch, 20.1 KB (added by GerdP, 8 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     * @since r10819
     122     */
     123    public boolean isSimilar(WaySegment s2) {
     124        if (getFirstNode().equals(s2.getFirstNode()) && getSecondNode().equals(s2.getSecondNode()))
     125            return true;
     126        if (getFirstNode().equals(s2.getSecondNode()) && getSecondNode().equals(s2.getFirstNode()))
     127            return true;
     128        return false;
     129    }
     130
    117131    @Override
    118132    public String toString() {
    119133        return "WaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']';
  • src/org/openstreetmap/josm/data/validation/tests/MultipolygonTest.java

     
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55import static org.openstreetmap.josm.tools.I18n.trn;
    66
    7 import java.awt.geom.GeneralPath;
     7import java.awt.geom.Path2D;
     8import java.awt.geom.Point2D;
    89import java.text.MessageFormat;
    910import java.util.ArrayList;
    1011import java.util.Arrays;
    1112import java.util.Collection;
    1213import java.util.Collections;
     14import java.util.HashMap;
    1315import java.util.HashSet;
    1416import java.util.LinkedList;
    1517import java.util.List;
     18import java.util.Map;
     19import java.util.Map.Entry;
    1620import java.util.Set;
    1721
    1822import org.openstreetmap.josm.Main;
    1923import org.openstreetmap.josm.actions.CreateMultipolygonAction;
     24import org.openstreetmap.josm.data.coor.EastNorth;
     25import org.openstreetmap.josm.data.coor.LatLon;
    2026import org.openstreetmap.josm.data.osm.Node;
    2127import org.openstreetmap.josm.data.osm.OsmPrimitive;
    2228import org.openstreetmap.josm.data.osm.Relation;
    2329import org.openstreetmap.josm.data.osm.RelationMember;
    2430import org.openstreetmap.josm.data.osm.Way;
     31import org.openstreetmap.josm.data.osm.WaySegment;
    2532import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
    2633import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
    27 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
    2834import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
    2935import org.openstreetmap.josm.data.validation.OsmValidator;
    3036import org.openstreetmap.josm.data.validation.Severity;
    3137import org.openstreetmap.josm.data.validation.Test;
    3238import org.openstreetmap.josm.data.validation.TestError;
     39import org.openstreetmap.josm.data.validation.util.ValUtil;
    3340import org.openstreetmap.josm.gui.DefaultNameFormatter;
    3441import org.openstreetmap.josm.gui.mappaint.ElemStyles;
    3542import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
     
    7178    private static volatile ElemStyles styles;
    7279
    7380    private final Set<String> keysCheckedByAnotherTest = new HashSet<>();
     81    /** All way segments, grouped by cells */
     82    private final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000);
     83    /** The already detected ways in error */
     84    private final Map<List<Way>, List<WaySegment>> crossingWays = new HashMap<>(50);
    7485
    7586    /**
    7687     * Constructs a new {@code MultipolygonTest}.
     
    103114        super.endTest();
    104115    }
    105116
    106     private static GeneralPath createPath(List<Node> nodes) {
    107         GeneralPath result = new GeneralPath();
    108         result.moveTo((float) nodes.get(0).getCoor().lat(), (float) nodes.get(0).getCoor().lon());
     117    private static Path2D.Double createPath(List<Node> nodes) {
     118        Path2D.Double result = new Path2D.Double();
     119        result.moveTo(nodes.get(0).getCoor().lon(), nodes.get(0).getCoor().lat());
    109120        for (int i = 1; i < nodes.size(); i++) {
    110121            Node n = nodes.get(i);
    111             result.lineTo((float) n.getCoor().lat(), (float) n.getCoor().lon());
     122            result.lineTo(n.getCoor().lon(), n.getCoor().lat());
    112123        }
    113124        return result;
    114125    }
    115126
    116     private static List<GeneralPath> createPolygons(List<Multipolygon.PolyData> joinedWays) {
    117         List<GeneralPath> result = new ArrayList<>();
     127    private static List<Path2D.Double> createPolygons(List<Multipolygon.PolyData> joinedWays) {
     128        List<Path2D.Double> result = new ArrayList<>();
    118129        for (Multipolygon.PolyData way : joinedWays) {
    119130            result.add(createPath(way.getNodes()));
    120131        }
     
    121132        return result;
    122133    }
    123134
    124     private static Intersection getPolygonIntersection(GeneralPath outer, List<Node> inner) {
    125         boolean inside = false;
    126         boolean outside = false;
    127 
    128         for (Node n : inner) {
    129             boolean contains = outer.contains(n.getCoor().lat(), n.getCoor().lon());
    130             inside = inside | contains;
    131             outside = outside | !contains;
    132             if (inside & outside) {
    133                 return Intersection.CROSSING;
    134             }
    135         }
    136 
    137         return inside ? Intersection.INSIDE : Intersection.OUTSIDE;
    138     }
    139 
    140135    @Override
    141136    public void visit(Way w) {
    142137        if (!w.isArea() && ElemStyles.hasOnlyAreaElemStyle(w)) {
     
    155150    @Override
    156151    public void visit(Relation r) {
    157152        if (r.isMultipolygon()) {
     153            crossingWays.clear();
     154            cellSegments.clear();
     155
    158156            checkMembersAndRoles(r);
    159157            checkOuterWay(r);
    160158
     
    163161                Multipolygon polygon = MultipolygonCache.getInstance().get(Main.map.mapView, r);
    164162
    165163                // Create new multipolygon using the logics from CreateMultipolygonAction and see if roles match.
    166                 checkMemberRoleCorrectness(r);
     164                boolean rolesWereChecked = checkMemberRoleCorrectness(r);
    167165                checkStyleConsistency(r, polygon);
    168                 checkGeometry(r, polygon);
     166                checkGeometry(r, polygon, rolesWereChecked);
    169167            }
    170168        }
    171169    }
     
    194192     * <li>{@link #WRONG_MEMBER_ROLE}: Role for ''{0}'' should be ''{1}''</li>
    195193     * </ul>
    196194     * @param r relation
     195     * @return true if member roles were checked
    197196     */
    198     private void checkMemberRoleCorrectness(Relation r) {
     197    private boolean checkMemberRoleCorrectness(Relation r) {
    199198        final Pair<Relation, Relation> newMP = CreateMultipolygonAction.createMultipolygonRelation(r.getMemberPrimitives(Way.class), false);
    200199        if (newMP != null) {
    201200            for (RelationMember member : r.getMembers()) {
     
    216215                }
    217216            }
    218217        }
     218        return newMP != null;
    219219    }
    220220
    221221    /**
     
    285285        }
    286286    }
    287287
     288    private static class LatLonPolyData {
     289        final PolyData pd;
     290        final Path2D.Double latLonPath;
     291
     292        LatLonPolyData(PolyData polyData, Path2D.Double path) {
     293            this.pd = polyData;
     294            this.latLonPath = path;
     295        }
     296    }
     297
    288298    /**
    289299     * Various geometry-related checks:<ul>
    290300     * <li>{@link #NON_CLOSED_WAY}: Multipolygon is not closed</li>
     
    293303     * </ul>
    294304     * @param r relation
    295305     * @param polygon multipolygon
     306     * @param rolesWereChecked might be used to skip most of the tests below
    296307     */
    297     private void checkGeometry(Relation r, Multipolygon polygon) {
     308    private void checkGeometry(Relation r, Multipolygon polygon, boolean rolesWereChecked) {
    298309        List<Node> openNodes = polygon.getOpenEnds();
    299310        if (!openNodes.isEmpty()) {
    300311            List<OsmPrimitive> primitives = new LinkedList<>();
    301312            primitives.add(r);
    302313            primitives.addAll(openNodes);
    303             addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon is not closed"), NON_CLOSED_WAY, primitives, openNodes));
     314            addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon is not closed"), NON_CLOSED_WAY,
     315                    primitives, openNodes));
    304316        }
     317        List<PolyData> innerPolygons = polygon.getInnerPolygons();
     318        List<PolyData> outerPolygons = polygon.getOuterPolygons();
    305319
     320        HashMap<PolyData, List<PolyData>> crossingPolyMap = checkCrossingWays(r, innerPolygons, outerPolygons);
     321
     322        //Polygons may intersect without crossing ways when one polygon lies completely inside the other
     323        List<LatLonPolyData> inner = calcLatLonPolygons(innerPolygons);
     324        List<LatLonPolyData> outer = calcLatLonPolygons(outerPolygons);
    306325        // For painting is used Polygon class which works with ints only. For validation we need more precision
    307         List<PolyData> innerPolygons = polygon.getInnerPolygons();
    308         List<PolyData> outerPolygons = polygon.getOuterPolygons();
    309         List<GeneralPath> innerPolygonsPaths = innerPolygons.isEmpty() ? Collections.<GeneralPath>emptyList() : createPolygons(innerPolygons);
    310         List<GeneralPath> outerPolygonsPaths = createPolygons(outerPolygons);
    311         for (int i = 0; i < outerPolygons.size(); i++) {
    312             PolyData pdOuter = outerPolygons.get(i);
    313             // Check for intersection between outer members
    314             for (int j = i+1; j < outerPolygons.size(); j++) {
    315                 checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdOuter, j);
     326        for (int i = 0; i + 1 < outer.size(); i++) {
     327            // report outer polygons which lie inside another outer
     328            LatLonPolyData outer1 = outer.get(i);
     329            for (int j = 0; j < outer.size(); j++) {
     330                if (i != j && !crossing(crossingPolyMap, outer1.pd, outer.get(j).pd)) {
     331                    LatLon c = outer.get(j).pd.getNodes().get(0).getCoor();
     332                    if (outer1.latLonPath.contains(c.lon(), c.lat())) {
     333                        addError(r,
     334                                new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
     335                                        CROSSING_WAYS, Collections.singletonList(r),
     336                                        Arrays.asList(outer1.pd.getNodes(), outer.get(j).pd.getNodes())));
     337                    }
     338                }
    316339            }
    317340        }
    318         for (int i = 0; i < innerPolygons.size(); i++) {
    319             PolyData pdInner = innerPolygons.get(i);
    320             // Check for intersection between inner members
    321             for (int j = i+1; j < innerPolygons.size(); j++) {
    322                 checkCrossingWays(r, innerPolygons, innerPolygonsPaths, pdInner, j);
     341        for (int i = 0; i < inner.size(); i++) {
     342            LatLonPolyData inner1 = inner.get(i);
     343            LatLon innerPoint = inner1.pd.getNodes().get(0).getCoor();
     344            for (int j = 0; j < inner.size(); j++) {
     345                LatLonPolyData inner2 = inner.get(j);
     346                if (i != j && !crossing(crossingPolyMap, inner1.pd, inner2.pd)) {
     347                    if (inner.get(j).latLonPath.contains(innerPoint.lon(), innerPoint.lat())) {
     348                        addError(r,
     349                                new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
     350                                        CROSSING_WAYS, Collections.singletonList(r),
     351                                        Arrays.asList(inner1.pd.getNodes(), inner2.pd.getNodes())));
     352                    }
     353                }
    323354            }
    324             // Check for intersection between inner and outer members
     355
     356            // Find inner polygons which are not inside any outer
    325357            boolean outside = true;
    326             for (int o = 0; o < outerPolygons.size(); o++) {
    327                 outside &= checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdInner, o) == Intersection.OUTSIDE;
     358            boolean crossingWithOuter = false;
     359
     360            for (int o = 0; o < outer.size(); o++) {
     361                if (crossing(crossingPolyMap, inner1.pd, outer.get(o).pd)) {
     362                    crossingWithOuter = true;
     363                    break;
     364                }
     365                outside &= outer.get(o).latLonPath.contains(innerPoint.lon(), innerPoint.lat()) == false;
     366                if (!outside)
     367                    break;
    328368            }
    329             if (outside) {
     369            if (outside && !crossingWithOuter) {
    330370                addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon inner way is outside"),
    331                         INNER_WAY_OUTSIDE, Collections.singletonList(r), Arrays.asList(pdInner.getNodes())));
     371                        INNER_WAY_OUTSIDE, Collections.singletonList(r), Arrays.asList(inner1.pd.getNodes())));
    332372            }
    333373        }
    334374    }
    335375
    336     private Intersection checkCrossingWays(Relation r, List<PolyData> polygons, List<GeneralPath> polygonsPaths, PolyData pd, int idx) {
    337         Intersection intersection = getPolygonIntersection(polygonsPaths.get(idx), pd.getNodes());
    338         if (intersection == Intersection.CROSSING) {
    339             PolyData pdOther = polygons.get(idx);
    340             if (pdOther != null) {
    341                 addError(r, new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
    342                         CROSSING_WAYS, Collections.singletonList(r), Arrays.asList(pd.getNodes(), pdOther.getNodes())));
    343             }
     376    private boolean crossing(HashMap<PolyData, List<PolyData>> crossingPolyMap, PolyData pd1, PolyData pd2) {
     377        List<PolyData> crossingWithFirst = crossingPolyMap.get(pd1);
     378        if (crossingWithFirst != null) {
     379            if (crossingWithFirst.contains(pd2))
     380                return true;
    344381        }
    345         return intersection;
     382        List<PolyData> crossingWith2nd = crossingPolyMap.get(pd2);
     383        if (crossingWith2nd != null) {
     384            if (crossingWith2nd.contains(pd1))
     385                return true;
     386        }
     387        return false;
    346388    }
    347389
     390    private List<LatLonPolyData> calcLatLonPolygons(List<PolyData> polygons) {
     391        if (polygons == null || polygons.isEmpty())
     392            return Collections.emptyList();
     393        List<LatLonPolyData> latLonPolygons = new ArrayList<>();
     394        List<Path2D.Double> polygonsPaths = createPolygons(polygons);
     395        for (int i = 0; i < polygons.size(); i++) {
     396            latLonPolygons.add(new LatLonPolyData(polygons.get(i), polygonsPaths.get(i)));
     397        }
     398        return latLonPolygons;
     399    }
     400
    348401    /**
    349402     * Check for:<ul>
    350403     * <li>{@link #WRONG_MEMBER_ROLE}: No useful role for multipolygon member</li>
     
    389442        addRelationIfNeeded(error, r);
    390443        errors.add(error);
    391444    }
     445
     446    /**
     447     * Determine multipolygon ways which are intersecting. This is now allowed.
     448     * See {@link CrossingWays}
     449     * @param r the relation (for error reporting)
     450     * @param innerPolygons list of inner polygons
     451     * @param outerPolygons list of outer polygons
     452     * @return map of crossing polygons (including polygons touching outer)
     453     */
     454    private HashMap<PolyData, List<PolyData>> checkCrossingWays(Relation r, List<PolyData> innerPolygons,
     455            List<PolyData> outerPolygons) {
     456        List<Way> innerWays = new ArrayList<>();
     457        List<Way> outerWays = new ArrayList<>();
     458        for (Way w : r.getMemberPrimitives(Way.class)) {
     459            for (PolyData pd : innerPolygons) {
     460                if (pd.getWayIds().contains(w.getUniqueId())) {
     461                    innerWays.add(w);
     462                    break;
     463                }
     464            }
     465            for (PolyData pd : outerPolygons) {
     466                if (pd.getWayIds().contains(w.getUniqueId())) {
     467                    outerWays.add(w);
     468                    break;
     469                }
     470            }
     471        }
     472        for (Way w : innerWays) {
     473            checkCrossingWay(w, r, true /* allow shared ways */);
     474        }
     475        for (Way w : outerWays) {
     476            checkCrossingWay(w, r, false/* don't allow shared ways */);
     477        }
     478        HashMap<PolyData, List<PolyData>> crossingPolygonsMap = new HashMap<>();
     479        for (Entry<List<Way>, List<WaySegment>> entry : crossingWays.entrySet()) {
     480            List<Way> ways = entry.getKey();
     481            PolyData[] crossingPolys = new PolyData[2];
     482            for (Way w : ways) {
     483                for (int j = 0; j < crossingPolys.length; j++) {
     484                    for (PolyData pd : innerPolygons) {
     485                        if (pd.getWayIds().contains(w.getUniqueId())) {
     486                            crossingPolys[j] = pd;
     487                            break;
     488                        }
     489                    }
     490                    if (crossingPolys[j] != null)
     491                        break;
     492                    for (PolyData pd : outerPolygons) {
     493                        if (pd.getWayIds().contains(w.getUniqueId())) {
     494                            crossingPolys[j] = pd;
     495                            break;
     496                        }
     497                    }
     498                }
     499            }
     500            if (crossingPolys[0] != null && crossingPolys[1] != null) {
     501                List<PolyData> x = crossingPolygonsMap.get(crossingPolys[0]);
     502                if (x == null) {
     503                    x = new ArrayList<>();
     504                    crossingPolygonsMap.put(crossingPolys[0], x);
     505                }
     506                x.add(crossingPolys[1]);
     507            }
     508        }
     509        return crossingPolygonsMap;
     510    }
     511
     512    /**
     513     *
     514     * @param w way that is member of the relation
     515     * @param r the relation (used for error messages)
     516     * @param allowSharedWaySegment false: treat similar way segment as crossing
     517     */
     518    private void checkCrossingWay(Way w, Relation r, boolean allowSharedWaySegment) {
     519
     520        int nodesSize = w.getNodesCount();
     521        for (int i = 0; i < nodesSize - 1; i++) {
     522            final WaySegment es1 = new WaySegment(w, i);
     523            final EastNorth en1 = es1.getFirstNode().getEastNorth();
     524            final EastNorth en2 = es1.getSecondNode().getEastNorth();
     525            if (en1 == null || en2 == null) {
     526                Main.warn("Crossing ways test skipped " + es1);
     527                continue;
     528            }
     529            for (List<WaySegment> segments : getSegments(en1, en2)) {
     530                for (WaySegment es2 : segments) {
     531
     532                    List<WaySegment> highlight;
     533
     534                    if (!es1.intersects(es2)) {
     535                        if (allowSharedWaySegment || !es1.isSimilar(es2))
     536                            continue;
     537                    }
     538
     539                    List<Way> prims = Arrays.asList(es1.way, es2.way);
     540                    if ((highlight = crossingWays.get(prims)) == null) {
     541                        highlight = new ArrayList<>();
     542                        highlight.add(es1);
     543                        highlight.add(es2);
     544
     545                        addError(r, new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
     546                                CROSSING_WAYS, Arrays.asList(r, es1.way, es2.way), highlight));
     547                        crossingWays.put(prims, highlight);
     548                    } else {
     549                        highlight.add(es1);
     550                        highlight.add(es2);
     551                    }
     552                }
     553                segments.add(es1);
     554            }
     555        }
     556    }
     557
     558    /**
     559     * Returns all the cells this segment crosses.  Each cell contains the list
     560     * of segments already processed
     561     *
     562     * @param n1 The first EastNorth
     563     * @param n2 The second EastNorth
     564     * @return A list with all the cells the segment crosses
     565     */
     566    private List<List<WaySegment>> getSegments(EastNorth n1, EastNorth n2) {
     567
     568        List<List<WaySegment>> cells = new ArrayList<>();
     569        for (Point2D cell : ValUtil.getSegmentCells(n1, n2, OsmValidator.griddetail)) {
     570            List<WaySegment> segments = cellSegments.get(cell);
     571            if (segments == null) {
     572                segments = new ArrayList<>();
     573                cellSegments.put(cell, segments);
     574            }
     575            cells.add(segments);
     576        }
     577        return cells;
     578    }
    392579}