Ticket #17011: 17011-v4.patch

File 17011-v4.patch, 17.2 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;
     
    106105        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
    107106            if (w1 == w2)
    108107                return false;
    109             if (!Objects.equals(OsmUtils.getLayer(w1), OsmUtils.getLayer(w2))) {
     108            if (!inSameLayer(w1, w2)) {
    110109                return true;
    111110            }
    112111            if (w1.hasKey(HIGHWAY) && w2.hasKey(HIGHWAY) && !Objects.equals(w1.get("level"), w2.get("level"))) {
     
    251250
    252251        @Override
    253252        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
    254             return !Objects.equals(OsmUtils.getLayer(w1), OsmUtils.getLayer(w2));
     253            return !inSameLayer(w1, w2);
    255254        }
    256255
    257256    }
  • 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.Path2D;
     8import java.awt.geom.PathIterator;
     9import java.util.ArrayList;
     10import java.util.HashSet;
     11import java.util.List;
     12import java.util.Set;
     13
     14import org.openstreetmap.josm.data.coor.EastNorth;
     15import org.openstreetmap.josm.data.osm.BBox;
     16import org.openstreetmap.josm.data.osm.DataSet;
     17import org.openstreetmap.josm.data.osm.Node;
     18import org.openstreetmap.josm.data.osm.OsmDataManager;
     19import org.openstreetmap.josm.data.osm.OsmPrimitive;
     20import org.openstreetmap.josm.data.osm.Relation;
     21import org.openstreetmap.josm.data.osm.Way;
     22import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
     23import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
     24import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
     25import org.openstreetmap.josm.data.validation.Severity;
     26import org.openstreetmap.josm.data.validation.Test;
     27import org.openstreetmap.josm.data.validation.TestError;
     28import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     29import org.openstreetmap.josm.tools.Geometry;
     30import org.openstreetmap.josm.tools.Geometry.PolygonIntersection;
     31import org.openstreetmap.josm.tools.MultiMap;
     32import org.openstreetmap.josm.tools.Pair;
     33
     34/**
     35 * Tests if there are overlapping areas
     36 *
     37 * @author Gerd Petermann
     38 * @since xxx
     39 */
     40public class OverlappingAreas extends Test {
     41    protected static final int OVERLAPPING_AREA = 3800; // allows insideness
     42    protected static final int OVERLAPPING_WATER = 3801;
     43    protected static final int OVERLAPPING_IDENTICAL_NATURAL = 3802;
     44    protected static final int OVERLAPPING_IDENTICAL_LANDLUSE = 3803;
     45    protected static final int OVERLAPPING_BUILDINGS = 3804;
     46    protected static final int OVERLAPPING_BUILDING_RESIDENTIAL = 3805;
     47
     48    private String reason;
     49    private int code;
     50    private Set<Way> seenWays;
     51    private MultiMap<Relation, Way> mpWayMap;
     52    private DataSet ds;
     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     * Calculate area in east/north space for given primitive.
     84     * @param p the primitive, must be a closed way or multipolygon
     85     * @return the area in east/north space
     86     */
     87    private static Area getEastNorthArea(OsmPrimitive p) {
     88        if (p instanceof Way) {
     89            return Geometry.getArea(((Way) p).getNodes());
     90        }
     91        Multipolygon mp = MultipolygonCache.getInstance().get((Relation) p);
     92        Path2D path = new Path2D.Double();
     93        path.setWindingRule(Path2D.WIND_EVEN_ODD);
     94        for (Multipolygon.PolyData pd : mp.getCombinedPolygons()) {
     95            path.append(pd.get(), false);
     96        }
     97        return new Area(path);
     98    }
     99
     100    /**
     101     * Check intersection between two areas. If empty or extremely small ignore it, else add error
     102     * with highlighted area.
     103     * @param a1 the first area (EastNorth)
     104     * @param a2 the second area (EastNorth)
     105     * @param p1 primitive describing 1st polygon
     106     * @param p2 primitive describing 2nd polygon
     107     */
     108    private void checkIntersection(Area a1, Area a2, OsmPrimitive p1, OsmPrimitive p2) {
     109        Pair<PolygonIntersection, Area> pair = Geometry.polygonIntersectionResult(a1, a2, Geometry.INTERSECTION_EPS_EAST_NORTH);
     110        switch (pair.a) {
     111        case OUTSIDE:
     112            return;
     113        case FIRST_INSIDE_SECOND:
     114            if (code == OVERLAPPING_AREA || code == OVERLAPPING_BUILDING_RESIDENTIAL && isBuilding(p1))
     115                return;
     116            if (code == OVERLAPPING_WATER && p2.hasTag("natural", "wetland"))
     117                return;
     118            break;
     119        case SECOND_INSIDE_FIRST:
     120            if (code == OVERLAPPING_AREA || code == OVERLAPPING_BUILDING_RESIDENTIAL && isBuilding(p2))
     121                return;
     122            if (code == OVERLAPPING_WATER && p1.hasTag("natural", "wetland"))
     123                return;
     124            break;
     125        case CROSSING:
     126            break;
     127        }
     128
     129        // we have an intersection, calculate the elements to highlight
     130        PathIterator pit = pair.b.getPathIterator(null);
     131        double[] res = new double[6];
     132        List<List<Node>> hilite = new ArrayList<>();
     133        List<Node> nodes = new ArrayList<>();
     134        while (!pit.isDone()) {
     135            int type = pit.currentSegment(res);
     136            Node n = new Node(new EastNorth(res[0], res[1]));
     137            switch (type) {
     138            case PathIterator.SEG_MOVETO:
     139                if (!nodes.isEmpty()) {
     140                    hilite.add(nodes);
     141                }
     142                nodes = new ArrayList<>();
     143                nodes.add(n);
     144                break;
     145            case PathIterator.SEG_LINETO:
     146                nodes.add(n);
     147                break;
     148            case PathIterator.SEG_CLOSE:
     149                if (!nodes.isEmpty()) {
     150                    hilite.add(nodes);
     151                    nodes = new ArrayList<>();
     152                }
     153                break;
     154            default:
     155                break;
     156            }
     157            pit.next();
     158        }
     159        if (nodes.size() > 1) {
     160            hilite.add(nodes);
     161        }
     162
     163        switch (code) {
     164        case OVERLAPPING_WATER:
     165            reason = tr("Overlapping Water Areas");
     166            break;
     167        case OVERLAPPING_IDENTICAL_NATURAL:
     168            reason = tr("Overlapping identical natural areas");
     169            break;
     170        case OVERLAPPING_IDENTICAL_LANDLUSE:
     171            reason = tr("Overlapping identical landuses");
     172            break;
     173        case OVERLAPPING_BUILDINGS:
     174            if (pair.a == PolygonIntersection.CROSSING)
     175                reason = tr("Overlapping buildings");
     176            else
     177                reason = tr("Building inside building");
     178            break;
     179        case OVERLAPPING_BUILDING_RESIDENTIAL:
     180            reason = tr("Overlapping building/residential area");
     181            break;
     182        default:
     183            reason = tr("Overlapping area");
     184
     185        }
     186        errors.add(TestError
     187                .builder(this, code == OVERLAPPING_AREA ? Severity.OTHER : Severity.WARNING, code)
     188                .message("OA: " + reason).primitives(p1, p2)
     189                .highlightNodePairs(hilite).build());
     190    }
     191
     192    /**
     193     * Check if the two objects are allowed to overlap regarding tags.
     194     * @param p1 1st primitive
     195     * @param p2 2nd primitive
     196     * @return the error code if the tags of the objects don't allow that the areas overlap, else 0
     197     */
     198    private static int tagsAllowOverlap(OsmPrimitive p1, OsmPrimitive p2) {
     199        if (!inSameLayer(p1, p2)) {
     200            return 0;
     201        }
     202        // order is significant, highest severity should come first
     203        if (isWaterArea(p1) && isWaterArea(p2)) {
     204            return OVERLAPPING_WATER;
     205        }
     206        if (isSetAndEqual(p1, p2, "natural")) {
     207            return OVERLAPPING_IDENTICAL_NATURAL;
     208        }
     209        if (isSetAndEqual(p1, p2, "landuse")) {
     210            return OVERLAPPING_IDENTICAL_LANDLUSE;
     211        }
     212        if (isBuilding(p1) && isBuilding(p2)) {
     213            return OVERLAPPING_BUILDINGS;
     214        }
     215        if (isBuilding(p1) && isResidentialArea(p2) || isBuilding(p2) && isResidentialArea(p1)) {
     216            return OVERLAPPING_BUILDING_RESIDENTIAL;
     217        }
     218        if (ValidatorPrefHelper.PREF_OTHER.get() && p1.concernsArea() && p2.concernsArea()) {
     219            return OVERLAPPING_AREA;
     220        }
     221        return 0;
     222    }
     223
     224    private static boolean isWaterArea(OsmPrimitive p) {
     225        return p.hasTag("natural", "water", "wetland") || p.hasTag("landuse", "reservoir");
     226    }
     227
     228    private static boolean isSetAndEqual(OsmPrimitive p1, OsmPrimitive p2, String key) {
     229        String v1 = p1.get(key);
     230        String v2 = p2.get(key);
     231        return v1 != null && v1.equals(v2);
     232    }
     233
     234    private Area checkDetails(Area a1, OsmPrimitive p1, OsmPrimitive p2) {
     235        reason = null;
     236        code = tagsAllowOverlap(p1, p2);
     237        if (code > 0) {
     238            // check geometry details
     239            if (a1 == null) {
     240                a1 = getEastNorthArea(p1);
     241            }
     242            checkIntersection(a1, getEastNorthArea(p2), p1, p2);
     243        }
     244        return a1;
     245    }
     246
     247    @Override
     248    public void visit(Way w1) {
     249        if (w1.isArea()) {
     250            /** performance: calculate only if needed and only once */
     251            Area a1 = null;
     252            BBox bbox1 = w1.getBBox();
     253            List<Way> nearWays = ds.searchWays(bbox1);
     254            // way-way overlaps
     255            for (Way w2 : nearWays) {
     256                if (w1 != w2 && w2.isArea() && !seenWays.contains(w2)) {
     257                    a1 = checkDetails(a1, w1, w2);
     258                }
     259            }
     260            // way-multipolygon overlaps
     261            List<Relation> nearRelations = ds.searchRelations(bbox1);
     262            for (Relation rel : nearRelations) {
     263                if (rel.isMultipolygon() && w1.referrers(Relation.class).noneMatch(parent -> parent == rel)) {
     264                    Set<Way> checkedWays = mpWayMap.get(rel);
     265                    if (checkedWays != null && checkedWays.contains(w1))
     266                        continue;
     267                    mpWayMap.put(rel, w1);
     268                    a1 = checkDetails(a1, w1, rel);
     269                }
     270            }
     271            seenWays.add(w1);
     272        }
     273    }
     274
     275    @Override
     276    public void visit(Relation r1) {
     277        if (r1.isMultipolygon()) {
     278            BBox bbox1 = r1.getBBox();
     279            /** performance: calculate only if needed and only once */
     280            Area a1 = null;
     281            // multipolygon -way overlaps
     282            List<Way> nearWays = ds.searchWays(bbox1);
     283            for (Way way : nearWays) {
     284                if (!way.isArea() || way.referrers(Relation.class).anyMatch(parent -> parent == r1)) {
     285                    // ignore members of multipolygon
     286                    continue;
     287                }
     288                Set<Way> checkedWays = mpWayMap.get(r1);
     289                if (checkedWays != null && checkedWays.contains(way))
     290                    continue;
     291                mpWayMap.put(r1, way);
     292                a1 = checkDetails(a1, r1, way);
     293            }
     294            List<Relation> nearRelations = ds.searchRelations(bbox1);
     295            for (Relation r2 : nearRelations) {
     296                if (r1 != r2 && r2.isMultipolygon() && !seenRelations.contains(r2)) {
     297                    a1 = checkDetails(a1, r1, r2);
     298                }
     299            }
     300            seenRelations.add(r1);
     301        }
     302    }
     303}
  • src/org/openstreetmap/josm/tools/Geometry.java

     
    602602     * @return intersection kind
    603603     */
    604604    public static PolygonIntersection polygonIntersection(Area a1, Area a2, double eps) {
     605        return polygonIntersectionResult(a1, a2, eps).a;
     606    }
    605607
     608    /**
     609     * Calculate intersection area and kind of intersection between two polygons.
     610     * @param a1 Area of first polygon
     611     * @param a2 Area of second polygon
     612     * @param eps an area threshold, everything below is considered an empty intersection
     613     * @return pair with intersection kind and intersection area (never null, but maybe empty)
     614     * @since xxx
     615     */
     616    public static Pair<PolygonIntersection, Area> polygonIntersectionResult(Area a1, Area a2, double eps) {
    606617        Area inter = new Area(a1);
    607618        inter.intersect(a2);
    608619
    609620        if (inter.isEmpty() || !checkIntersection(inter, eps)) {
    610             return PolygonIntersection.OUTSIDE;
     621            return new Pair<>(PolygonIntersection.OUTSIDE, inter);
    611622        } else if (a2.getBounds2D().contains(a1.getBounds2D()) && inter.equals(a1)) {
    612             return PolygonIntersection.FIRST_INSIDE_SECOND;
     623            return new Pair<>(PolygonIntersection.FIRST_INSIDE_SECOND, inter);
    613624        } else if (a1.getBounds2D().contains(a2.getBounds2D()) && inter.equals(a2)) {
    614             return PolygonIntersection.SECOND_INSIDE_FIRST;
     625            return new Pair<>(PolygonIntersection.SECOND_INSIDE_FIRST, inter);
    615626        } else {
    616             return PolygonIntersection.CROSSING;
     627            return new Pair<>(PolygonIntersection.CROSSING, inter);
    617628        }
    618629    }
    619630