Ticket #13307: improve_MultipolygonTest_v4.patch

File improve_MultipolygonTest_v4.patch, 19.6 KB (added by Don-vip, 10 years ago)

v4 = v3 + checkstyle

  • 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 10813
     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<>();
     
    302313            primitives.addAll(openNodes);
    303314            addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon is not closed"), NON_CLOSED_WAY, primitives, openNodes));
    304315        }
    305 
    306         // For painting is used Polygon class which works with ints only. For validation we need more precision
    307316        List<PolyData> innerPolygons = polygon.getInnerPolygons();
    308317        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);
     318
     319        HashMap<PolyData, List<PolyData>> crossingPolyMap = checkCrossingWays(r, innerPolygons, outerPolygons);
     320
     321        // Polygons may intersect without crossing ways when one polygon lies completely inside the other
     322        List<LatLonPolyData> inner = calcLatLonPolygons(innerPolygons);
     323        List<LatLonPolyData> outer = calcLatLonPolygons(outerPolygons);
     324        // For painting is used Polygon class which works with ints only. For validation we need more precision
     325        for (int i = 0; i+1 < outer.size(); i++) {
     326            // report outer polygons which lie inside another outer
     327            LatLonPolyData outer1 = outer.get(i);
     328            for (int j = 0; j < outer.size(); j++) {
     329                if (i != j && !crossing(crossingPolyMap, outer1.pd, outer.get(j).pd)) {
     330                    LatLon c = outer.get(j).pd.getNodes().get(0).getCoor();
     331                    if (outer1.latLonPath.contains(c.lon(), c.lat())) {
     332                        addError(r, new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
     333                                CROSSING_WAYS, Collections.singletonList(r), Arrays.asList(outer1.pd.getNodes(), outer.get(j).pd.getNodes())));
     334                    }
     335                }
    316336            }
    317337        }
    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);
     338        for (int i = 0; i < inner.size(); i++) {
     339            LatLonPolyData inner1 = inner.get(i);
     340            LatLon innerPoint = inner1.pd.getNodes().get(0).getCoor();
     341            for (int j = 0; j < inner.size(); j++) {
     342                LatLonPolyData inner2 = inner.get(j);
     343                if (i != j && !crossing(crossingPolyMap, inner1.pd, inner2.pd)) {
     344                    if (inner.get(j).latLonPath.contains(innerPoint.lon(), innerPoint.lat())) {
     345                        addError(r, new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
     346                                CROSSING_WAYS, Collections.singletonList(r), Arrays.asList(inner1.pd.getNodes(), inner2.pd.getNodes())));
     347                    }
     348                }
    323349            }
    324             // Check for intersection between inner and outer members
     350
     351            // Find inner polygons which are not inside any outer
    325352            boolean outside = true;
    326             for (int o = 0; o < outerPolygons.size(); o++) {
    327                 outside &= checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdInner, o) == Intersection.OUTSIDE;
     353            boolean crossingWithOuter = false;
     354
     355            for (int o = 0; o < outer.size(); o++) {
     356                if (crossing(crossingPolyMap, inner1.pd, outer.get(o).pd)) {
     357                    crossingWithOuter = true;
     358                    break;
     359                }
     360                outside &= outer.get(o).latLonPath.contains(innerPoint.lon(), innerPoint.lat()) == false;
     361                if (!outside)
     362                    break;
    328363            }
    329             if (outside) {
     364            if (outside && !crossingWithOuter) {
    330365                addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon inner way is outside"),
    331                         INNER_WAY_OUTSIDE, Collections.singletonList(r), Arrays.asList(pdInner.getNodes())));
     366                        INNER_WAY_OUTSIDE, Collections.singletonList(r), Arrays.asList(inner1.pd.getNodes())));
    332367            }
    333368        }
    334369    }
    335370
    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             }
     371    private boolean crossing(HashMap<PolyData, List<PolyData>> crossingPolyMap, PolyData pd1, PolyData pd2) {
     372        List<PolyData> crossingWithFirst = crossingPolyMap.get(pd1);
     373        if (crossingWithFirst != null) {
     374            if (crossingWithFirst.contains(pd2))
     375                return true;
     376        }
     377        List<PolyData> crossingWith2nd = crossingPolyMap.get(pd2);
     378        if (crossingWith2nd != null) {
     379            if (crossingWith2nd.contains(pd1))
     380                return true;
    344381        }
    345         return intersection;
     382        return false;
     383    }
     384
     385    private List<LatLonPolyData> calcLatLonPolygons(List<PolyData> polygons) {
     386        if (polygons == null || polygons.isEmpty())
     387            return Collections.emptyList();
     388        List<LatLonPolyData> latLonPolygons = new ArrayList<>();
     389        List<Path2D.Double> polygonsPaths = createPolygons(polygons);
     390        for (int i = 0; i < polygons.size(); i++) {
     391            latLonPolygons.add(new LatLonPolyData(polygons.get(i), polygonsPaths.get(i)));
     392        }
     393        return latLonPolygons;
    346394    }
    347395
    348396    /**
     
    389437        addRelationIfNeeded(error, r);
    390438        errors.add(error);
    391439    }
     440
     441    /**
     442     * Determine multipolygon ways which are intersecting. This is now allowed.
     443     * See {@link CrossingWays}
     444     * @param r the relation (for error reporting)
     445     * @param innerPolygons list of inner polygons
     446     * @param outerPolygons list of outer polygons
     447     * @return map of crossing polygons (including polygons touching outer)
     448     */
     449    private HashMap<PolyData, List<PolyData>> checkCrossingWays(Relation r, List<PolyData> innerPolygons, List<PolyData> outerPolygons) {
     450        List<Way> innerWays = new ArrayList<>();
     451        List<Way> outerWays = new ArrayList<>();
     452        for (Way w : r.getMemberPrimitives(Way.class)) {
     453            for (PolyData pd : innerPolygons) {
     454                if (pd.getWayIds().contains(w.getUniqueId())) {
     455                    innerWays.add(w);
     456                    break;
     457                }
     458            }
     459            for (PolyData pd : outerPolygons) {
     460                if (pd.getWayIds().contains(w.getUniqueId())) {
     461                    outerWays.add(w);
     462                    break;
     463                }
     464            }
     465        }
     466        for (Way w : innerWays) {
     467            checkCrossingWay(w, r, true /* allow shared ways */);
     468        }
     469        for (Way w : outerWays) {
     470            checkCrossingWay(w, r, false/* don't allow shared ways */);
     471        }
     472        HashMap<PolyData, List<PolyData>> crossingPolygonsMap = new HashMap<>();
     473        for (Entry<List<Way>, List<WaySegment>> entry : crossingWays.entrySet()) {
     474            List<Way> ways = entry.getKey();
     475            PolyData[] crossingPolys = new PolyData[2];
     476            for (Way w: ways) {
     477                for (int j = 0; j < crossingPolys.length; j++) {
     478                    for (PolyData pd : innerPolygons) {
     479                        if (pd.getWayIds().contains(w.getUniqueId())) {
     480                            crossingPolys[j] = pd;
     481                            break;
     482                        }
     483                    }
     484                    if (crossingPolys[j] != null)
     485                        break;
     486                    for (PolyData pd : outerPolygons) {
     487                        if (pd.getWayIds().contains(w.getUniqueId())) {
     488                            crossingPolys[j] = pd;
     489                            break;
     490                        }
     491                    }
     492                }
     493            }
     494            if (crossingPolys[0] != null && crossingPolys[1] != null) {
     495                List<PolyData> x = crossingPolygonsMap.get(crossingPolys[0]);
     496                if (x == null) {
     497                    x = new ArrayList<>();
     498                    crossingPolygonsMap.put(crossingPolys[0], x);
     499                }
     500                x.add(crossingPolys[1]);
     501            }
     502        }
     503        return crossingPolygonsMap;
     504    }
     505
     506    private void checkCrossingWay(Way w, Relation r, boolean allowSharedWaySegment) {
     507
     508        int nodesSize = w.getNodesCount();
     509        for (int i = 0; i < nodesSize - 1; i++) {
     510            final WaySegment es1 = new WaySegment(w, i);
     511            final EastNorth en1 = es1.getFirstNode().getEastNorth();
     512            final EastNorth en2 = es1.getSecondNode().getEastNorth();
     513            if (en1 == null || en2 == null) {
     514                Main.warn("Crossing ways test skipped "+es1);
     515                continue;
     516            }
     517            for (List<WaySegment> segments : getSegments(en1, en2)) {
     518                for (WaySegment es2 : segments) {
     519
     520                    List<WaySegment> highlight;
     521
     522                    if (!es1.intersects(es2)) {
     523                        if (allowSharedWaySegment || !es1.isSimilar(es2))
     524                            continue;
     525                    }
     526
     527                    List<Way> prims = Arrays.asList(es1.way, es2.way);
     528                    if ((highlight = crossingWays.get(prims)) == null) {
     529                        highlight = new ArrayList<>();
     530                        highlight.add(es1);
     531                        highlight.add(es2);
     532
     533                        addError(r, new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
     534                                CROSSING_WAYS, Arrays.asList(r, es1.way, es2.way), highlight));
     535                        crossingWays.put(prims, highlight);
     536                    } else {
     537                        highlight.add(es1);
     538                        highlight.add(es2);
     539                    }
     540                }
     541                segments.add(es1);
     542            }
     543        }
     544    }
     545
     546    /**
     547     * Returns all the cells this segment crosses.  Each cell contains the list
     548     * of segments already processed
     549     *
     550     * @param n1 The first EastNorth
     551     * @param n2 The second EastNorth
     552     * @return A list with all the cells the segment crosses
     553     */
     554    private List<List<WaySegment>> getSegments(EastNorth n1, EastNorth n2) {
     555
     556        List<List<WaySegment>> cells = new ArrayList<>();
     557        for (Point2D cell : ValUtil.getSegmentCells(n1, n2, OsmValidator.griddetail)) {
     558            List<WaySegment> segments = cellSegments.get(cell);
     559            if (segments == null) {
     560                segments = new ArrayList<>();
     561                cellSegments.put(cell, segments);
     562            }
     563            cells.add(segments);
     564        }
     565        return cells;
     566    }
    392567}