Ticket #17011: 17011-v5.patch

File 17011-v5.patch, 24.5 KB (added by GerdP, 7 years ago)
  • src/org/openstreetmap/josm/data/validation/OsmValidator.java

     
    5555import org.openstreetmap.josm.data.validation.tests.MultipolygonTest;
    5656import org.openstreetmap.josm.data.validation.tests.NameMismatch;
    5757import org.openstreetmap.josm.data.validation.tests.OpeningHourTest;
     58import org.openstreetmap.josm.data.validation.tests.OverlappingAreas;
    5859import org.openstreetmap.josm.data.validation.tests.OverlappingWays;
    5960import org.openstreetmap.josm.data.validation.tests.PowerLines;
    6061import org.openstreetmap.josm.data.validation.tests.PublicTransportRouteTest;
     
    148149        LongSegment.class, // 3500 .. 3599
    149150        PublicTransportRouteTest.class, // 3600 .. 3699
    150151        RightAngleBuildingTest.class, // 3700 .. 3799
     152        OverlappingAreas.class, // 3800 .. 3899
    151153    };
    152154
    153155    /**
  • src/org/openstreetmap/josm/data/validation/Test.java

     
    1818import org.openstreetmap.josm.command.DeleteCommand;
    1919import org.openstreetmap.josm.data.osm.Node;
    2020import org.openstreetmap.josm.data.osm.OsmPrimitive;
     21import org.openstreetmap.josm.data.osm.OsmUtils;
    2122import org.openstreetmap.josm.data.osm.Relation;
    2223import org.openstreetmap.josm.data.osm.Way;
    2324import org.openstreetmap.josm.data.osm.search.SearchCompiler.InDataSourceArea;
     
    364365        return p.hasTag("landuse", "residential");
    365366    }
    366367
     368    /**
     369     * Determines if the specified primitives are in the same layer.
     370     * @param p1 first primitive
     371     * @param p2 second primitive
     372     * @return True if the objects are in the same layer
     373     */
     374    protected static final boolean inSameLayer(OsmPrimitive p1, OsmPrimitive p2) {
     375        return Objects.equals(OsmUtils.getLayer(p1), OsmUtils.getLayer(p2));
     376    }
     377
    367378    @Override
    368379    public int hashCode() {
    369380        return Objects.hash(name, description);
  • src/org/openstreetmap/josm/data/validation/tests/CrossingWays.java

     
    1313
    1414import org.openstreetmap.josm.data.coor.EastNorth;
    1515import org.openstreetmap.josm.data.osm.OsmPrimitive;
    16 import org.openstreetmap.josm.data.osm.OsmUtils;
    1716import org.openstreetmap.josm.data.osm.Relation;
    1817import org.openstreetmap.josm.data.osm.Way;
    1918import org.openstreetmap.josm.data.osm.WaySegment;
     
    3534    static final String HIGHWAY = "highway";
    3635    static final String RAILWAY = "railway";
    3736    static final String WATERWAY = "waterway";
    38     static final String LANDUSE = "landuse";
    3937
    4038    static final class MessageHelper {
    4139        final String message;
     
    106104        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
    107105            if (w1 == w2)
    108106                return false;
    109             if (!Objects.equals(OsmUtils.getLayer(w1), OsmUtils.getLayer(w2))) {
     107            if (!inSameLayer(w1, w2)) {
    110108                return true;
    111109            }
    112110            if (w1.hasKey(HIGHWAY) && w2.hasKey(HIGHWAY) && !Objects.equals(w1.get("level"), w2.get("level"))) {
     
    251249
    252250        @Override
    253251        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
    254             return !Objects.equals(OsmUtils.getLayer(w1), OsmUtils.getLayer(w2));
     252            return !inSameLayer(w1, w2);
    255253        }
    256254
    257255    }
     
    313311    }
    314312
    315313    static boolean isCoastline(OsmPrimitive w) {
    316         return w.hasTag("natural", "water", "coastline") || w.hasTag(LANDUSE, "reservoir");
     314        return w.hasTag("natural", "water", "coastline") || w.hasTag("landuse", "reservoir");
    317315    }
    318316
    319317    static boolean isHighway(OsmPrimitive w) {
  • src/org/openstreetmap/josm/data/validation/tests/MultipolygonTest.java

     
    206206                                    .highlight(wOuter)
    207207                                    .build());
    208208                        } else if (areaStyle) { /* style on outer way of multipolygon, but equal to polygon */
    209                             errors.add(TestError.builder(this, Severity.WARNING, OUTER_STYLE)
    210                                     .message(tr("Area style on outer way"))
     209                            errors.add(TestError.builder(this, Severity.ERROR, OUTER_STYLE)
     210                                    .message(tr("Area style repeated on outer way"))
    211211                                    .primitives(Arrays.asList(r, wOuter))
    212212                                    .highlight(wOuter)
    213213                                    .build());
     
    626626     * @param crossingWays list to collect crossing ways
    627627     * @param findSharedWaySegments true: find shared way segments instead of crossings
    628628     */
    629     private static void findIntersectingWay(Way w, Map<Point2D, List<WaySegment>> cellSegments,
     629    static void findIntersectingWay(Way w, Map<Point2D, List<WaySegment>> cellSegments,
    630630            Map<List<Way>, List<WaySegment>> crossingWays, boolean findSharedWaySegments) {
    631631        int nodesSize = w.getNodesCount();
    632632        for (int i = 0; i < nodesSize - 1; i++) {
  • src/org/openstreetmap/josm/data/validation/tests/OverlappingAreas.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.validation.tests;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.awt.geom.Area;
     7import java.awt.geom.PathIterator;
     8import java.awt.geom.Point2D;
     9import java.util.ArrayList;
     10import java.util.HashMap;
     11import java.util.HashSet;
     12import java.util.List;
     13import java.util.Map;
     14import java.util.Set;
     15
     16import org.openstreetmap.josm.data.coor.EastNorth;
     17import org.openstreetmap.josm.data.osm.BBox;
     18import org.openstreetmap.josm.data.osm.DataSet;
     19import org.openstreetmap.josm.data.osm.Node;
     20import org.openstreetmap.josm.data.osm.OsmDataManager;
     21import org.openstreetmap.josm.data.osm.OsmPrimitive;
     22import org.openstreetmap.josm.data.osm.Relation;
     23import org.openstreetmap.josm.data.osm.Way;
     24import org.openstreetmap.josm.data.osm.WaySegment;
     25import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
     26import org.openstreetmap.josm.data.validation.Severity;
     27import org.openstreetmap.josm.data.validation.Test;
     28import org.openstreetmap.josm.data.validation.TestError;
     29import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     30import org.openstreetmap.josm.tools.Geometry;
     31import org.openstreetmap.josm.tools.Geometry.PolygonIntersection;
     32import org.openstreetmap.josm.tools.MultiMap;
     33import org.openstreetmap.josm.tools.Pair;
     34
     35/**
     36 * Tests if there are overlapping areas
     37 *
     38 * @author Gerd Petermann
     39 * @since xxx
     40 */
     41public class OverlappingAreas extends Test {
     42    protected static final int OVERLAPPING_AREA = 3800; // allows insideness
     43    protected static final int OVERLAPPING_WATER = 3801;
     44    protected static final int OVERLAPPING_IDENTICAL_NATURAL = 3802;
     45    protected static final int OVERLAPPING_IDENTICAL_LANDLUSE = 3803;
     46    protected static final int OVERLAPPING_BUILDINGS = 3804;
     47    protected static final int OVERLAPPING_BUILDING_RESIDENTIAL = 3805;
     48
     49    private DataSet ds;
     50    // collections to suppress duplicate tests
     51    private MultiMap<Relation, Way> mpWayMap;
     52    private Set<Way> seenWays;
     53    private Set<Relation> seenRelations;
     54
     55    /** Constructor */
     56    public OverlappingAreas() {
     57        super(tr("Overlapping areas"),
     58                tr("This test checks if two areas intersect in the same layer."));
     59    }
     60
     61    @Override
     62    public void startTest(ProgressMonitor monitor) {
     63        super.startTest(monitor);
     64        super.setShowElements(true);
     65        // collections to suppress duplicated tests */
     66        seenWays = new HashSet<>();
     67        seenRelations = new HashSet<>();
     68        mpWayMap = new MultiMap<>();
     69        ds = OsmDataManager.getInstance().getEditDataSet();
     70    }
     71
     72    @Override
     73    public void endTest() {
     74        // clean up references
     75        seenRelations = null;
     76        seenWays = null;
     77        mpWayMap = null;
     78        ds = null;
     79        super.endTest();
     80    }
     81
     82    /**
     83     * Check intersection between two areas. If empty or extremely small ignore it, else add error
     84     * with highlighted area.
     85     * @param a1 the first area (east/north space)
     86     * @param a2 the second area (east/north space)
     87     * @param p1 primitive describing 1st polygon
     88     * @param p2 primitive describing 2nd polygon
     89     * @param code the error code
     90     */
     91    private void checkIntersection(Area a1, Area a2, OsmPrimitive p1, OsmPrimitive p2, int code) {
     92        Pair<PolygonIntersection, Area> pair = Geometry.polygonIntersectionResult(a1, a2, Geometry.INTERSECTION_EPS_EAST_NORTH);
     93        switch (pair.a) {
     94        case OUTSIDE:
     95            return;
     96        case FIRST_INSIDE_SECOND:
     97            if (code == OVERLAPPING_AREA || code == OVERLAPPING_BUILDING_RESIDENTIAL && isBuilding(p1))
     98                return;
     99            if (code == OVERLAPPING_WATER && p2.hasTag("natural", "wetland"))
     100                return;
     101            break;
     102        case SECOND_INSIDE_FIRST:
     103            if (code == OVERLAPPING_AREA || code == OVERLAPPING_BUILDING_RESIDENTIAL && isBuilding(p2))
     104                return;
     105            if (code == OVERLAPPING_WATER && p1.hasTag("natural", "wetland"))
     106                return;
     107            break;
     108        case CROSSING:
     109            break;
     110        }
     111
     112        final String reason = getReason(code, pair.a == PolygonIntersection.CROSSING);
     113        errors.add(TestError.builder(this, code == OVERLAPPING_AREA ? Severity.OTHER : Severity.WARNING, code)
     114                .message("OA: " + reason).primitives(p1, p2)
     115                .highlightNodePairs(getHiliteNodesForArea(pair.b))
     116                .build());
     117    }
     118
     119    /**
     120     * Calculate list of node pairs describing the intersection area.
     121     * @param intersection the intersection area
     122     * @return list of node pairs describing the intersection area
     123     */
     124    private static List<List<Node>> getHiliteNodesForArea(Area intersection) {
     125        List<List<Node>> hilite = new ArrayList<>();
     126        PathIterator pit = intersection.getPathIterator(null);
     127        double[] res = new double[6];
     128        List<Node> nodes = new ArrayList<>();
     129        while (!pit.isDone()) {
     130            int type = pit.currentSegment(res);
     131            Node n = new Node(new EastNorth(res[0], res[1]));
     132            switch (type) {
     133            case PathIterator.SEG_MOVETO:
     134                if (!nodes.isEmpty()) {
     135                    hilite.add(nodes);
     136                }
     137                nodes = new ArrayList<>();
     138                nodes.add(n);
     139                break;
     140            case PathIterator.SEG_LINETO:
     141                nodes.add(n);
     142                break;
     143            case PathIterator.SEG_CLOSE:
     144                if (!nodes.isEmpty()) {
     145                    nodes.add(nodes.get(0));
     146                    hilite.add(nodes);
     147                    nodes = new ArrayList<>();
     148                }
     149                break;
     150            default:
     151                break;
     152            }
     153            pit.next();
     154        }
     155        if (nodes.size() > 1) {
     156            hilite.add(nodes);
     157        }
     158        return hilite;
     159    }
     160
     161    /**
     162     * Check if the two objects are allowed to overlap regarding tags.
     163     * @param p1 1st primitive
     164     * @param p2 2nd primitive
     165     * @return the error code if the tags of the objects don't allow that the areas overlap, else 0
     166     */
     167    private static int tagsAllowOverlap(OsmPrimitive p1, OsmPrimitive p2) {
     168        if (!inSameLayer(p1, p2)) {
     169            return 0;
     170        }
     171        // order is significant, highest severity should come first
     172        if (isWaterArea(p1) && isWaterArea(p2)) {
     173            return OVERLAPPING_WATER;
     174        }
     175        if (isSetAndEqual(p1, p2, "natural")) {
     176            return OVERLAPPING_IDENTICAL_NATURAL;
     177        }
     178        if (isSetAndEqual(p1, p2, "landuse")) {
     179            return OVERLAPPING_IDENTICAL_LANDLUSE;
     180        }
     181        if (isBuilding(p1) && isBuilding(p2)) {
     182            return OVERLAPPING_BUILDINGS;
     183        }
     184        if (isBuilding(p1) && isResidentialArea(p2) || isBuilding(p2) && isResidentialArea(p1)) {
     185            return OVERLAPPING_BUILDING_RESIDENTIAL;
     186        }
     187        if (ValidatorPrefHelper.PREF_OTHER.get() && p1.concernsArea() && p2.concernsArea()) {
     188            return OVERLAPPING_AREA;
     189        }
     190        return 0;
     191    }
     192
     193    private static boolean isWaterArea(OsmPrimitive p) {
     194        return p.hasTag("natural", "water", "wetland") || p.hasTag("landuse", "reservoir");
     195    }
     196
     197    private static boolean isSetAndEqual(OsmPrimitive p1, OsmPrimitive p2, String key) {
     198        String v1 = p1.get(key);
     199        String v2 = p2.get(key);
     200        return v1 != null && v1.equals(v2);
     201    }
     202
     203    private Area checkDetailsWithArea(Area a1, OsmPrimitive p1, OsmPrimitive p2) {
     204        int code = tagsAllowOverlap(p1, p2);
     205        if (code > 0) {
     206            // check geometry details
     207            if (a1 == null) {
     208                a1 = Geometry.getArea(p1);
     209            }
     210            checkIntersection(a1, Geometry.getArea(p2), p1, p2, code);
     211        }
     212        return a1;
     213    }
     214
     215    @Override
     216    public void visit(Way w1) {
     217        if (w1.isArea()) {
     218            /** performance: calculate area only if needed and only once */
     219            Area a1 = null;
     220            BBox bbox1 = w1.getBBox();
     221            List<Way> nearWays = ds.searchWays(bbox1);
     222            // way-way overlaps
     223            for (Way w2 : nearWays) {
     224                if (w1 != w2 && w2.isArea() && !seenWays.contains(w2)) {
     225                    a1 = checkDetailsWithArea(a1, w1, w2);
     226                }
     227            }
     228            // way-multipolygon overlaps
     229            if (partialSelection) {
     230                List<Relation> nearRelations = ds.searchRelations(bbox1);
     231                for (Relation rel : nearRelations) {
     232                    if (rel.isMultipolygon() && w1.referrers(Relation.class).noneMatch(parent -> parent == rel)) {
     233                        Set<Way> checkedWays = mpWayMap.get(rel);
     234                        if (checkedWays != null && checkedWays.contains(w1))
     235                            continue;
     236                        mpWayMap.put(rel, w1);
     237                        if (!rel.isIncomplete() && !rel.hasIncompleteMembers()) {
     238                            a1 = checkDetailsWithArea(a1, w1, rel);
     239                        } else {
     240                            int code = tagsAllowOverlap(w1, rel);
     241                            if (code > 0) {
     242                                checkWayCrossingRelation(code, w1, rel, null, w1);
     243                            }
     244                        }
     245                    }
     246                }
     247            }
     248            seenWays.add(w1);
     249        }
     250        if (partialSelection) {
     251            // if the member of a relation is selected, visit also the relation ?
     252            // w1.referrers(Relation.class).filter(Relation::isSelected).forEach(this::visit);
     253        }
     254    }
     255
     256    @Override
     257    public void visit(Relation r1) {
     258        if (r1.isMultipolygon()) {
     259            BBox bbox1 = r1.getBBox();
     260            /** performance: calculate area only if needed and only once */
     261            Area a1 = null;
     262            // multipolygon -way overlaps
     263            List<Way> nearWays = ds.searchWays(bbox1);
     264            List<Relation> nearRelations = ds.searchRelations(bbox1);
     265            if (nearWays.isEmpty() && nearRelations.isEmpty())
     266                return;
     267
     268            /** All way segments, grouped by cells */
     269            final Map<Point2D, List<WaySegment>> mpCellSegments = new HashMap<>(1000);
     270            /** The already detected ways in error */
     271            final Map<List<Way>, List<WaySegment>> problemWays = new HashMap<>(50);
     272            for (Way relWay : r1.getMemberPrimitives(Way.class)) {
     273                MultipolygonTest.findIntersectingWay(relWay, mpCellSegments, problemWays, false);
     274            }
     275            if (!problemWays.isEmpty()) {
     276                // invalid multipolygon
     277                return;
     278            }
     279
     280            for (Way way : nearWays) {
     281                if (!way.isArea() || way.referrers(Relation.class).anyMatch(parent -> parent == r1)) {
     282                    // ignore members of current multipolygon
     283                    continue;
     284                }
     285                if (partialSelection) {
     286                    Set<Way> checkedWays = mpWayMap.get(r1);
     287                    if (checkedWays != null && checkedWays.contains(way))
     288                        continue;
     289                    mpWayMap.put(r1, way);
     290                }
     291                if (r1.isIncomplete() || r1.hasIncompleteMembers()) {
     292                    int code = tagsAllowOverlap(r1, way);
     293                    if (code > 0) {
     294                        checkWayCrossingRelation(code, way, r1, mpCellSegments, way);
     295                    }
     296                } else {
     297                    a1 = checkDetailsWithArea(a1, r1, way);
     298                }
     299            }
     300            for (Relation r2 : nearRelations) {
     301                if (r1 != r2 && r2.isMultipolygon() && !seenRelations.contains(r2)) {
     302                    if (!r1.isIncomplete() && !r1.hasIncompleteMembers() && !r2.isIncomplete()
     303                            && !r2.hasIncompleteMembers()) {
     304                        a1 = checkDetailsWithArea(a1, r1, r2);
     305                    } else {
     306                        int code = tagsAllowOverlap(r1, r2);
     307                        if (code > 0) {
     308                            for (Way way : r2.getMemberPrimitives(Way.class)) {
     309                                checkWayCrossingRelation(code, way, r1, mpCellSegments, r2);
     310                            }
     311                        }
     312                    }
     313                }
     314            }
     315            seenRelations.add(r1);
     316        }
     317    }
     318
     319    /**
     320     * Check if a way crosses any of the multipolygon relation members.
     321     * @param code the error code to use if a crossing is found
     322     * @param way the way to check
     323     * @param rel the multipolygon relation
     324     * @param mpCellSegments optional index for way segments for the multipolygon ways, can be null
     325     * @param p2 gives the 2nd primitive to use in the error message (which might be the parent relation of the way)
     326     */
     327    private void checkWayCrossingRelation(int code, Way way, Relation rel, Map<Point2D, List<WaySegment>> mpCellSegments, OsmPrimitive p2) {
     328        final Map<List<Way>, List<WaySegment>> problemWays = new HashMap<>(50);
     329        Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>();
     330        if (mpCellSegments == null) {
     331            for (Way relWay : rel.getMemberPrimitives(Way.class)) {
     332                MultipolygonTest.findIntersectingWay(relWay, cellSegments, problemWays, false);
     333            }
     334        } else {
     335            cellSegments.putAll(mpCellSegments);
     336        }
     337        MultipolygonTest.findIntersectingWay(way, cellSegments, problemWays, false);
     338        // TODO: Doesn't find overlaps at shared nodes.
     339        // Maybe add check similar to MultipolygonTest.checkOverlapAtSharedNodes() if way.isArea() is true
     340        if (!problemWays.isEmpty()) {
     341            final String reason = getReason(code, true);
     342            for (List<WaySegment> waySegments : problemWays.values()) {
     343                errors.add(TestError.builder(this, code == OVERLAPPING_AREA ? Severity.OTHER : Severity.WARNING, code)
     344                        .message("OA: " + reason).primitives(rel, p2)
     345                        .highlightWaySegments(waySegments)
     346                        .build());
     347
     348            }
     349        }
     350    }
     351
     352    /**
     353     * Create message string. Should only be called if an intersection is found.
     354     * @param code the reason code
     355     * @param isCrossing if false it is assumed that one object is inside the other
     356     * @return the message
     357     */
     358    private static String getReason(int code, boolean isCrossing) {
     359        //
     360        switch (code) {
     361        case OVERLAPPING_WATER:
     362            return isCrossing ? tr("Overlapping Water Areas") : tr("Water Area inside Water Area");
     363        case OVERLAPPING_IDENTICAL_NATURAL:
     364            return tr("Overlapping identical natural areas");
     365        case OVERLAPPING_IDENTICAL_LANDLUSE:
     366            return tr("Overlapping identical landuses");
     367        case OVERLAPPING_BUILDINGS:
     368            return isCrossing ? tr("Overlapping buildings") : tr("Building inside building");
     369        case OVERLAPPING_BUILDING_RESIDENTIAL:
     370            return tr("Overlapping building/residential area");
     371        default:
     372            return tr("Overlapping area");
     373        }
     374
     375    }
     376}
  • src/org/openstreetmap/josm/tools/Geometry.java

     
    526526    }
    527527
    528528    /**
     529     * Calculate area in east/north space for given primitive.
     530     * @param p the primitive
     531     * @return the area in east/north space, might be empty if the primitive is incomplete or a node
     532     * since xxx
     533     */
     534    public static Area getArea(IPrimitive p) {
     535        if (p instanceof Way) {
     536            return getArea(((Way) p).getNodes());
     537        }
     538        if (p instanceof Relation && !p.isIncomplete()) {
     539            Multipolygon mp = new Multipolygon((Relation) p);
     540            Path2D path = new Path2D.Double();
     541            path.setWindingRule(Path2D.WIND_EVEN_ODD);
     542            for (Multipolygon.PolyData pd : mp.getCombinedPolygons()) {
     543                path.append(pd.get(), false);
     544            }
     545            return new Area(path);
     546        }
     547        return new Area();
     548    }
     549
     550    /**
    529551     * Builds a path from a list of nodes
    530552     * @param polygon Nodes, forming a closed polygon
    531553     * @param path2d path to add to; can be null, then a new path is created
     
    598620     * @return intersection kind
    599621     */
    600622    public static PolygonIntersection polygonIntersection(Area a1, Area a2, double eps) {
     623        return polygonIntersectionResult(a1, a2, eps).a;
     624    }
    601625
     626    /**
     627     * Calculate intersection area and kind of intersection between two polygons.
     628     * @param a1 Area of first polygon
     629     * @param a2 Area of second polygon
     630     * @param eps an area threshold, everything below is considered an empty intersection
     631     * @return pair with intersection kind and intersection area (never null, but maybe empty)
     632     * @since xxx
     633     */
     634    public static Pair<PolygonIntersection, Area> polygonIntersectionResult(Area a1, Area a2, double eps) {
    602635        Area inter = new Area(a1);
    603636        inter.intersect(a2);
    604637
    605638        if (inter.isEmpty() || !checkIntersection(inter, eps)) {
    606             return PolygonIntersection.OUTSIDE;
     639            return new Pair<>(PolygonIntersection.OUTSIDE, inter);
    607640        } else if (a2.getBounds2D().contains(a1.getBounds2D()) && inter.equals(a1)) {
    608             return PolygonIntersection.FIRST_INSIDE_SECOND;
     641            return new Pair<>(PolygonIntersection.FIRST_INSIDE_SECOND, inter);
    609642        } else if (a1.getBounds2D().contains(a2.getBounds2D()) && inter.equals(a2)) {
    610             return PolygonIntersection.SECOND_INSIDE_FIRST;
     643            return new Pair<>(PolygonIntersection.SECOND_INSIDE_FIRST, inter);
    611644        } else {
    612             return PolygonIntersection.CROSSING;
     645            return new Pair<>(PolygonIntersection.CROSSING, inter);
    613646        }
    614647    }
    615648