Ticket #13307: improve_MultipolygonTest_v2.patch

File improve_MultipolygonTest_v2.patch, 20.0 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     */
     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    }
    117129    @Override
    118130    public String toString() {
    119131        return "WaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']';
    120132    }
     133
     134
    121135}
  • 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    /**
     
    284284            }
    285285        }
    286286    }
     287    private static class LatLonPolyData{
     288        final PolyData pd;
     289        final Path2D.Double latLonPath;
     290        public LatLonPolyData(PolyData polyData, Path2D.Double path) {
     291            this.pd = polyData;
     292            this.latLonPath = path;
     293        }
     294    }
    287295
    288296    /**
    289297     * Various geometry-related checks:<ul>
     
    293301     * </ul>
    294302     * @param r relation
    295303     * @param polygon multipolygon
     304     * @param rolesWereChecked might be used to skip most of the tests below
    296305     */
    297     private void checkGeometry(Relation r, Multipolygon polygon) {
     306    private void checkGeometry(Relation r, Multipolygon polygon, boolean rolesWereChecked) {
    298307        List<Node> openNodes = polygon.getOpenEnds();
    299308        if (!openNodes.isEmpty()) {
    300309            List<OsmPrimitive> primitives = new LinkedList<>();
     
    302311            primitives.addAll(openNodes);
    303312            addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon is not closed"), NON_CLOSED_WAY, primitives, openNodes));
    304313        }
     314        List<PolyData> innerPolygons = polygon.getInnerPolygons();
     315        List<PolyData> outerPolygons = polygon.getOuterPolygons();
    305316
     317        HashMap<PolyData, List<PolyData>> crossingPolyMap = checkCrossingWays(r, innerPolygons, outerPolygons);
     318
     319        //Polygons may intersect without crossing ways when one polygon lies completely inside the other
     320        List<LatLonPolyData> inner = calcLatLonPolygons(innerPolygons);
     321        List<LatLonPolyData> outer = calcLatLonPolygons(outerPolygons);
    306322        // 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);
     323        for (int i = 0; i+1 < outer.size(); i++) {
     324            // report outer polygons which lie inside another outer
     325            LatLonPolyData outer1 = outer.get(i);
     326            for (int j = 0; j < outer.size(); j++) {
     327                if (i != j && !crossing(crossingPolyMap, outer1.pd, outer.get(j).pd)){
     328                    LatLon c = outer.get(j).pd.getNodes().get(0).getCoor();
     329                    if (outer1.latLonPath.contains(c.lon(), c.lat())){
     330                        addError(r, new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
     331                                CROSSING_WAYS, Collections.singletonList(r), Arrays.asList(outer1.pd.getNodes(), outer.get(j).pd.getNodes())));
     332                    }
     333                }
    316334            }
    317335        }
    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);
     336        for (int i = 0; i < inner.size(); i++) {
     337            LatLonPolyData inner1 = inner.get(i);
     338            LatLon innerPoint = inner1.pd.getNodes().get(0).getCoor();
     339            for (int j = 0; j < inner.size(); j++) {
     340                LatLonPolyData inner2 = inner.get(j);
     341                if (i != j && !crossing(crossingPolyMap, inner1.pd, inner2.pd)){
     342                    if (inner.get(j).latLonPath.contains(innerPoint.lon(), innerPoint.lat())){
     343                        addError(r, new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
     344                                CROSSING_WAYS, Collections.singletonList(r), Arrays.asList(inner1.pd.getNodes(), inner2.pd.getNodes())));
     345                    }
     346                }
    323347            }
    324             // Check for intersection between inner and outer members
     348
     349            // Find inner polygons which are not inside any outer
    325350            boolean outside = true;
    326             for (int o = 0; o < outerPolygons.size(); o++) {
    327                 outside &= checkCrossingWays(r, outerPolygons, outerPolygonsPaths, pdInner, o) == Intersection.OUTSIDE;
     351            boolean crossingWithOuter = true;
     352
     353            for (int o = 0; o < outer.size(); o++) {
     354                if (crossing(crossingPolyMap, inner1.pd, outer.get(o).pd)){
     355                    crossingWithOuter = false;
     356                    break;
     357                }
     358                outside &= outer.get(o).latLonPath.contains(innerPoint.lon(), innerPoint.lat()) == false;
     359                if (!outside)
     360                    break;
    328361            }
    329             if (outside) {
     362            if (outside && !crossingWithOuter) {
    330363                addError(r, new TestError(this, Severity.WARNING, tr("Multipolygon inner way is outside"),
    331                         INNER_WAY_OUTSIDE, Collections.singletonList(r), Arrays.asList(pdInner.getNodes())));
     364                        INNER_WAY_OUTSIDE, Collections.singletonList(r), Arrays.asList(inner1.pd.getNodes())));
    332365            }
    333366        }
    334367    }
    335368
    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             }
     369    private boolean crossing(HashMap<PolyData, List<PolyData>> crossingPolyMap, PolyData pd1, PolyData pd2) {
     370        List<PolyData> crossingWithFirst = crossingPolyMap.get(pd1);
     371        if (crossingWithFirst != null){
     372            if (crossingWithFirst.contains(pd2))
     373                return true;
    344374        }
    345         return intersection;
     375        List<PolyData> crossingWith2nd= crossingPolyMap.get(pd2);
     376        if (crossingWith2nd != null){
     377            if (crossingWith2nd.contains(pd1))
     378                return true;
     379        }
     380        return false;
    346381    }
    347382
     383    private List<LatLonPolyData> calcLatLonPolygons(List<PolyData> polygons) {
     384        if (polygons == null || polygons.isEmpty())
     385            return Collections.emptyList();
     386        List<LatLonPolyData> latLonPolygons = new ArrayList<>();
     387        List<Path2D.Double> polygonsPaths = createPolygons(polygons);
     388        for (int i = 0; i < polygons.size(); i++) {
     389            latLonPolygons.add(new LatLonPolyData(polygons.get(i), polygonsPaths.get(i)));
     390        }
     391        return latLonPolygons;
     392    }
     393
    348394    /**
    349395     * Check for:<ul>
    350396     * <li>{@link #WRONG_MEMBER_ROLE}: No useful role for multipolygon member</li>
     
    389435        addRelationIfNeeded(error, r);
    390436        errors.add(error);
    391437    }
     438
     439    /**
     440     * Determine multipolygon ways which are intersecting. This is now allowed.
     441     * See {@link CrossingWays}
     442     * @param r the relation (for error reporting)
     443     * @param innerPolygons list of inner polygons
     444     * @param outerPolygons list of outer polygons
     445     * @return map of crossing polygons (including polygons touching outer)
     446     */
     447    private HashMap<PolyData, List<PolyData>> checkCrossingWays (Relation r, List<PolyData> innerPolygons, List<PolyData> outerPolygons){
     448        List<Way> innerWays = new ArrayList<>();
     449        List<Way> outerWays = new ArrayList<>();
     450        for (Way w : r.getMemberPrimitives(Way.class)){
     451            for (PolyData pd : innerPolygons){
     452                if (pd.getWayIds().contains(w.getUniqueId())){
     453                    innerWays.add(w);
     454                    break;
     455                }
     456            }
     457            for (PolyData pd : outerPolygons){
     458                if (pd.getWayIds().contains(w.getUniqueId())){
     459                    outerWays.add(w);
     460                    break;
     461                }
     462            }
     463        }
     464        for (Way w : innerWays){
     465            checkCrossingWay(w, r, true /* allow shared ways */);
     466        }
     467        for (Way w : outerWays){
     468            checkCrossingWay(w, r, false/* don't allow shared ways */);
     469        }
     470        HashMap<PolyData, List<PolyData>> crossingPolygonsMap = new HashMap<>();
     471        for (Entry<List<Way>, List<WaySegment>> entry : crossingWays.entrySet()){
     472            List<Way> ways = entry.getKey();
     473            PolyData[] crossingPolys = new PolyData[2];
     474            for (Way w: ways){
     475                for (int j = 0; j < crossingPolys.length; j++){
     476                    for (PolyData pd : innerPolygons){
     477                        if (pd.getWayIds().contains(w.getUniqueId())){
     478                            crossingPolys[j] = pd;
     479                            break;
     480                        }
     481                    }
     482                    if (crossingPolys[j] != null)
     483                        break;
     484                    for (PolyData pd : outerPolygons){
     485                        if (pd.getWayIds().contains(w.getUniqueId())){
     486                            crossingPolys[j] = pd;
     487                            break;
     488                        }
     489                    }
     490                }
     491            }
     492            if (crossingPolys[0] != null && crossingPolys[1] != null){
     493                List<PolyData> x = crossingPolygonsMap.get(crossingPolys[0]);
     494                if (x == null){
     495                    x = new ArrayList<>();
     496                    crossingPolygonsMap.put(crossingPolys[0], x);
     497                }
     498                x.add(crossingPolys[1]);
     499            }
     500        }
     501        return crossingPolygonsMap;
     502    }
     503
     504    /**
     505     *
     506     * @param w
     507     * @param r
     508     * @param allowSharedWaySegment
     509     */
     510    private void checkCrossingWay(Way w, Relation r, boolean allowSharedWaySegment) {
     511
     512        int nodesSize = w.getNodesCount();
     513        for (int i = 0; i < nodesSize - 1; i++) {
     514            final WaySegment es1 = new WaySegment(w, i);
     515            final EastNorth en1 = es1.getFirstNode().getEastNorth();
     516            final EastNorth en2 = es1.getSecondNode().getEastNorth();
     517            if (en1 == null || en2 == null) {
     518                Main.warn("Crossing ways test skipped "+es1);
     519                continue;
     520            }
     521            for (List<WaySegment> segments : getSegments(en1, en2)) {
     522                for (WaySegment es2 : segments) {
     523
     524                    List<WaySegment> highlight;
     525
     526                    if (!es1.intersects(es2)) {
     527                        if (allowSharedWaySegment || !es1.isSimilar(es2))
     528                            continue;
     529                    }
     530
     531                    List<Way> prims = Arrays.asList(es1.way, es2.way);
     532                    if ((highlight = crossingWays.get(prims)) == null) {
     533                        highlight = new ArrayList<>();
     534                        highlight.add(es1);
     535                        highlight.add(es2);
     536
     537                        addError(r, new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"),
     538                                CROSSING_WAYS, Arrays.asList(r,es1.way,es2.way), highlight ));
     539                        crossingWays.put(prims, highlight);
     540                    } else {
     541                        highlight.add(es1);
     542                        highlight.add(es2);
     543                    }
     544                }
     545                segments.add(es1);
     546            }
     547        }
     548    }
     549
     550    /**
     551     * Returns all the cells this segment crosses.  Each cell contains the list
     552     * of segments already processed
     553     *
     554     * @param n1 The first EastNorth
     555     * @param n2 The second EastNorth
     556     * @return A list with all the cells the segment crosses
     557     */
     558    private List<List<WaySegment>> getSegments(EastNorth n1, EastNorth n2) {
     559
     560        List<List<WaySegment>> cells = new ArrayList<>();
     561        for (Point2D cell : ValUtil.getSegmentCells(n1, n2, OsmValidator.griddetail)) {
     562            List<WaySegment> segments = cellSegments.get(cell);
     563            if (segments == null) {
     564                segments = new ArrayList<>();
     565                cellSegments.put(cell, segments);
     566            }
     567            cells.add(segments);
     568        }
     569        return cells;
     570    }
    392571}