001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation.tests;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.geom.Point2D;
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.HashMap;
010import java.util.List;
011import java.util.Map;
012import java.util.Objects;
013
014import org.openstreetmap.josm.data.coor.EastNorth;
015import org.openstreetmap.josm.data.osm.OsmPrimitive;
016import org.openstreetmap.josm.data.osm.OsmUtils;
017import org.openstreetmap.josm.data.osm.Relation;
018import org.openstreetmap.josm.data.osm.Way;
019import org.openstreetmap.josm.data.osm.WaySegment;
020import org.openstreetmap.josm.data.validation.OsmValidator;
021import org.openstreetmap.josm.data.validation.Severity;
022import org.openstreetmap.josm.data.validation.Test;
023import org.openstreetmap.josm.data.validation.TestError;
024import org.openstreetmap.josm.data.validation.util.ValUtil;
025import org.openstreetmap.josm.gui.progress.ProgressMonitor;
026import org.openstreetmap.josm.tools.Logging;
027
028/**
029 * Tests if there are segments that crosses in the same layer/level
030 *
031 * @author frsantos
032 */
033public abstract class CrossingWays extends Test {
034
035    static final String HIGHWAY = "highway";
036    static final String RAILWAY = "railway";
037    static final String WATERWAY = "waterway";
038    static final String LANDUSE = "landuse";
039
040    static final class MessageHelper {
041        final String message;
042        final int code;
043
044        MessageHelper(String message, int code) {
045            this.message = message;
046            this.code = code;
047        }
048    }
049
050    /**
051     * Type of way. Entries have to be declared in alphabetical order, see sort below.
052     */
053    private enum WayType {
054        BUILDING, HIGHWAY, RAILWAY, RESIDENTIAL_AREA, WATERWAY, WAY;
055
056        static WayType of(Way w) {
057            if (isBuilding(w))
058                return BUILDING;
059            else if (w.hasKey(CrossingWays.HIGHWAY))
060                return HIGHWAY;
061            else if (isRailway(w))
062                return RAILWAY;
063            else if (isResidentialArea(w))
064                return RESIDENTIAL_AREA;
065            else if (w.hasKey(CrossingWays.WATERWAY))
066                return WATERWAY;
067            else
068                return WAY;
069        }
070    }
071
072    /** All way segments, grouped by cells */
073    private final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000);
074    /** The already detected ways in error */
075    private final Map<List<Way>, List<WaySegment>> seenWays = new HashMap<>(50);
076
077    protected final int code;
078
079    /**
080     * General crossing ways test.
081     */
082    public static class Ways extends CrossingWays {
083
084        protected static final int CROSSING_WAYS = 601;
085
086        /**
087         * Constructs a new crossing {@code Ways} test.
088         */
089        public Ways() {
090            super(tr("Crossing ways"), CROSSING_WAYS);
091        }
092
093        @Override
094        public boolean isPrimitiveUsable(OsmPrimitive w) {
095            return super.isPrimitiveUsable(w)
096                    && !isProposedOrAbandoned(w)
097                    && (isHighway(w)
098                    || w.hasKey(WATERWAY)
099                    || isRailway(w)
100                    || isCoastline(w)
101                    || isBuilding(w)
102                    || isResidentialArea(w));
103        }
104
105        @Override
106        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
107            if (w1 == w2)
108                return false;
109            if (areLayerOrLevelDifferent(w1, w2)) {
110                return true;
111            }
112            if (w1.hasKey(HIGHWAY) && w2.hasKey(HIGHWAY) && !Objects.equals(w1.get("level"), w2.get("level"))) {
113                return true;
114            }
115            if ((w1.hasKey(HIGHWAY, RAILWAY, WATERWAY) && isResidentialArea(w2))
116             || (w2.hasKey(HIGHWAY, RAILWAY, WATERWAY) && isResidentialArea(w1)))
117                return true;
118            if (isSubwayOrTramOrRazed(w2)) {
119                return true;
120            }
121            if (isCoastline(w1) != isCoastline(w2)) {
122                return true;
123            }
124            if ((w1.hasTag(WATERWAY, "river", "stream", "canal", "drain", "ditch") && w2.hasTag(WATERWAY, "riverbank"))
125             || (w2.hasTag(WATERWAY, "river", "stream", "canal", "drain", "ditch") && w1.hasTag(WATERWAY, "riverbank"))) {
126                return true;
127            }
128            return isProposedOrAbandoned(w2);
129        }
130
131        @Override
132        MessageHelper createMessage(Way w1, Way w2) {
133            WayType[] types = {WayType.of(w1), WayType.of(w2)};
134            Arrays.sort(types);
135
136            if (types[0] == types[1]) {
137                switch(types[0]) {
138                    case BUILDING:
139                        return new MessageHelper(tr("Crossing buildings"), 610);
140                    case HIGHWAY:
141                        return new MessageHelper(tr("Crossing highways"), 620);
142                    case RAILWAY:
143                        return new MessageHelper(tr("Crossing railways"), 630);
144                    case RESIDENTIAL_AREA:
145                        return new MessageHelper(tr("Crossing residential areas"), 640);
146                    case WATERWAY:
147                        return new MessageHelper(tr("Crossing waterways"), 650);
148                    case WAY:
149                    default:
150                        return new MessageHelper(tr("Crossing ways"), CROSSING_WAYS);
151                }
152            } else {
153                switch (types[0]) {
154                    case BUILDING:
155                        switch (types[1]) {
156                            case HIGHWAY:
157                                return new MessageHelper(tr("Crossing building/highway"), 612);
158                            case RAILWAY:
159                                return new MessageHelper(tr("Crossing building/railway"), 613);
160                            case RESIDENTIAL_AREA:
161                                return new MessageHelper(tr("Crossing building/residential area"), 614);
162                            case WATERWAY:
163                                return new MessageHelper(tr("Crossing building/waterway"), 615);
164                            case WAY:
165                            default:
166                                return new MessageHelper(tr("Crossing building/way"), 611);
167                        }
168                    case HIGHWAY:
169                        switch (types[1]) {
170                            case RAILWAY:
171                                return new MessageHelper(tr("Crossing highway/railway"), 622);
172                            case WATERWAY:
173                                return new MessageHelper(tr("Crossing highway/waterway"), 623);
174                            case WAY:
175                            default:
176                                return new MessageHelper(tr("Crossing highway/way"), 621);
177                        }
178                    case RAILWAY:
179                        switch (types[1]) {
180                            case WATERWAY:
181                                return new MessageHelper(tr("Crossing railway/waterway"), 632);
182                            case WAY:
183                            default:
184                                return new MessageHelper(tr("Crossing railway/way"), 631);
185                        }
186                    case RESIDENTIAL_AREA:
187                        switch (types[1]) {
188                            case WAY:
189                            default:
190                                return new MessageHelper(tr("Crossing residential area/way"), 641);
191                        }
192                    case WATERWAY:
193                    default:
194                        return new MessageHelper(tr("Crossing waterway/way"), 651);
195                }
196            }
197        }
198    }
199
200    /**
201     * Crossing boundaries ways test.
202     */
203    public static class Boundaries extends CrossingWays {
204
205        protected static final int CROSSING_BOUNDARIES = 602;
206
207        /**
208         * Constructs a new crossing {@code Boundaries} test.
209         */
210        public Boundaries() {
211            super(tr("Crossing boundaries"), CROSSING_BOUNDARIES);
212        }
213
214        @Override
215        public boolean isPrimitiveUsable(OsmPrimitive p) {
216            return super.isPrimitiveUsable(p) && p.hasKey("boundary")
217                    && (!(p instanceof Relation) || (((Relation) p).isMultipolygon() && !((Relation) p).hasIncompleteMembers()));
218        }
219
220        @Override
221        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
222            return !Objects.equals(w1.get("boundary"), w2.get("boundary"));
223        }
224
225        @Override
226        public void visit(Relation r) {
227            for (Way w : r.getMemberPrimitives(Way.class)) {
228                visit(w);
229            }
230        }
231    }
232
233    /**
234     * Crossing barriers ways test.
235     */
236    public static class Barrier extends CrossingWays {
237
238        protected static final int CROSSING_BARRIERS = 603;
239
240        /**
241         * Constructs a new crossing {@code Barrier} test.
242         */
243        public Barrier() {
244            super(tr("Crossing barriers"), CROSSING_BARRIERS);
245        }
246
247        @Override
248        public boolean isPrimitiveUsable(OsmPrimitive p) {
249            return super.isPrimitiveUsable(p) && p.hasKey("barrier");
250        }
251
252        @Override
253        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
254            return areLayerOrLevelDifferent(w1, w2);
255        }
256    }
257
258    /**
259     * Self crossing ways test (for all the rest)
260     */
261    public static class SelfCrossing extends CrossingWays {
262
263        protected static final int CROSSING_SELF = 604;
264
265        CrossingWays.Ways normalTest = new Ways();
266        CrossingWays.Barrier barrierTest = new Barrier();
267        CrossingWays.Boundaries boundariesTest = new Boundaries();
268
269        /**
270         * Constructs a new SelfIntersection test.
271         */
272        public SelfCrossing() {
273            super(tr("Self crossing ways"), CROSSING_SELF);
274        }
275
276        @Override
277        public boolean isPrimitiveUsable(OsmPrimitive p) {
278            return super.isPrimitiveUsable(p) && !(normalTest.isPrimitiveUsable(p) || barrierTest.isPrimitiveUsable(p)
279                    || boundariesTest.isPrimitiveUsable(p));
280        }
281
282        @Override
283        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
284            return w1 != w2; // should not happen
285        }
286    }
287
288    /**
289     * Constructs a new {@code CrossingWays} test.
290     * @param title The test title
291     * @param code The test code
292     * @since 12958
293     */
294    public CrossingWays(String title, int code) {
295        super(title, tr("This test checks if two roads, railways, waterways or buildings crosses in the same layer, " +
296                "but are not connected by a node."));
297        this.code = code;
298    }
299
300    @Override
301    public void startTest(ProgressMonitor monitor) {
302        super.startTest(monitor);
303        cellSegments.clear();
304        seenWays.clear();
305    }
306
307    @Override
308    public void endTest() {
309        super.endTest();
310        cellSegments.clear();
311        seenWays.clear();
312    }
313
314    static boolean isCoastline(OsmPrimitive w) {
315        return w.hasTag("natural", "water", "coastline") || w.hasTag(LANDUSE, "reservoir");
316    }
317
318    static boolean isHighway(OsmPrimitive w) {
319        return w.hasTagDifferent(HIGHWAY, "rest_area", "services", "bus_stop", "platform");
320    }
321
322    static boolean isRailway(OsmPrimitive w) {
323        return w.hasKey(RAILWAY) && !isSubwayOrTramOrRazed(w);
324    }
325
326    static boolean isSubwayOrTramOrRazed(OsmPrimitive w) {
327        return w.hasTag(RAILWAY, "subway", "tram", "razed") ||
328              (w.hasTag(RAILWAY, "construction") && w.hasTag("construction", "tram")) ||
329              (w.hasTag(RAILWAY, "disused") && w.hasTag("disused", "tram"));
330    }
331
332    static boolean isProposedOrAbandoned(OsmPrimitive w) {
333        return w.hasTag(HIGHWAY, "proposed") || w.hasTag(RAILWAY, "proposed", "abandoned");
334    }
335
336    abstract boolean ignoreWaySegmentCombination(Way w1, Way w2);
337
338    MessageHelper createMessage(Way w1, Way w2) {
339        return new MessageHelper(this.name, this.code);
340    }
341
342    @Override
343    public void visit(Way w) {
344        if (this instanceof SelfCrossing) {
345            // free memory, we are not interested in previous ways
346            cellSegments.clear();
347            seenWays.clear();
348        }
349
350        int nodesSize = w.getNodesCount();
351        for (int i = 0; i < nodesSize - 1; i++) {
352            final WaySegment es1 = new WaySegment(w, i);
353            final EastNorth en1 = es1.getFirstNode().getEastNorth();
354            final EastNorth en2 = es1.getSecondNode().getEastNorth();
355            if (en1 == null || en2 == null) {
356                Logging.warn("Crossing ways test skipped " + es1);
357                continue;
358            }
359            for (List<WaySegment> segments : getSegments(cellSegments, en1, en2)) {
360                for (WaySegment es2 : segments) {
361                    List<Way> prims;
362                    List<WaySegment> highlight;
363
364                    if (!es1.intersects(es2) || ignoreWaySegmentCombination(es1.way, es2.way)) {
365                        continue;
366                    }
367
368                    prims = new ArrayList<>();
369                    prims.add(es1.way);
370                    if (es1.way != es2.way)
371                        prims.add(es2.way);
372                    if ((highlight = seenWays.get(prims)) == null) {
373                        highlight = new ArrayList<>();
374                        highlight.add(es1);
375                        highlight.add(es2);
376
377                        final MessageHelper message = createMessage(es1.way, es2.way);
378                        errors.add(TestError.builder(this, Severity.WARNING, message.code)
379                                .message(message.message)
380                                .primitives(prims)
381                                .highlightWaySegments(highlight)
382                                .build());
383                        seenWays.put(prims, highlight);
384                    } else {
385                        highlight.add(es1);
386                        highlight.add(es2);
387                    }
388                }
389                segments.add(es1);
390            }
391        }
392    }
393
394    private static boolean areLayerOrLevelDifferent(Way w1, Way w2) {
395        return !Objects.equals(OsmUtils.getLayer(w1), OsmUtils.getLayer(w2))
396            || !Objects.equals(w1.get("level"), w2.get("level"));
397    }
398
399    /**
400     * Returns all the cells this segment crosses.  Each cell contains the list
401     * of segments already processed
402     * @param cellSegments map with already collected way segments
403     * @param n1 The first EastNorth
404     * @param n2 The second EastNorth
405     * @return A list with all the cells the segment crosses
406     */
407    public static List<List<WaySegment>> getSegments(Map<Point2D, List<WaySegment>> cellSegments, EastNorth n1, EastNorth n2) {
408        List<List<WaySegment>> cells = new ArrayList<>();
409        for (Point2D cell : ValUtil.getSegmentCells(n1, n2, OsmValidator.getGridDetail())) {
410            cells.add(cellSegments.computeIfAbsent(cell, k -> new ArrayList<>()));
411        }
412        return cells;
413    }
414}