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
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 (!Objects.equals(OsmUtils.getLayer(w1), OsmUtils.getLayer(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 !Objects.equals(OsmUtils.getLayer(w1), OsmUtils.getLayer(w2));
255        }
256
257    }
258
259    /**
260     * Self crossing ways test (for all the rest)
261     */
262    public static class SelfCrossing extends CrossingWays {
263
264        protected static final int CROSSING_SELF = 604;
265
266        CrossingWays.Ways normalTest = new Ways();
267        CrossingWays.Barrier barrierTest = new Barrier();
268        CrossingWays.Boundaries boundariesTest = new Boundaries();
269
270        /**
271         * Constructs a new SelfIntersection test.
272         */
273        public SelfCrossing() {
274            super(tr("Self crossing ways"), CROSSING_SELF);
275        }
276
277        @Override
278        public boolean isPrimitiveUsable(OsmPrimitive p) {
279            return super.isPrimitiveUsable(p) && !(normalTest.isPrimitiveUsable(p) || barrierTest.isPrimitiveUsable(p)
280                    || boundariesTest.isPrimitiveUsable(p));
281        }
282
283        @Override
284        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
285            return w1 != w2; // should not happen
286        }
287    }
288
289    /**
290     * Constructs a new {@code CrossingWays} test.
291     * @param title The test title
292     * @param code The test code
293     * @since 12958
294     */
295    public CrossingWays(String title, int code) {
296        super(title, tr("This test checks if two roads, railways, waterways or buildings crosses in the same layer, " +
297                "but are not connected by a node."));
298        this.code = code;
299    }
300
301    @Override
302    public void startTest(ProgressMonitor monitor) {
303        super.startTest(monitor);
304        cellSegments.clear();
305        seenWays.clear();
306    }
307
308    @Override
309    public void endTest() {
310        super.endTest();
311        cellSegments.clear();
312        seenWays.clear();
313    }
314
315    static boolean isCoastline(OsmPrimitive w) {
316        return w.hasTag("natural", "water", "coastline") || w.hasTag(LANDUSE, "reservoir");
317    }
318
319    static boolean isHighway(OsmPrimitive w) {
320        return w.hasTagDifferent(HIGHWAY, "rest_area", "services", "bus_stop", "platform");
321    }
322
323    static boolean isRailway(OsmPrimitive w) {
324        return w.hasKey(RAILWAY) && !isSubwayOrTramOrRazed(w);
325    }
326
327    static boolean isSubwayOrTramOrRazed(OsmPrimitive w) {
328        return w.hasTag(RAILWAY, "subway", "tram", "razed") ||
329              (w.hasTag(RAILWAY, "construction") && w.hasTag("construction", "tram")) ||
330              (w.hasTag(RAILWAY, "disused") && w.hasTag("disused", "tram"));
331    }
332
333    static boolean isProposedOrAbandoned(OsmPrimitive w) {
334        return w.hasTag(HIGHWAY, "proposed") || w.hasTag(RAILWAY, "proposed", "abandoned");
335    }
336
337    abstract boolean ignoreWaySegmentCombination(Way w1, Way w2);
338
339    MessageHelper createMessage(Way w1, Way w2) {
340        return new MessageHelper(this.name, this.code);
341    }
342
343    @Override
344    public void visit(Way w) {
345        if (this instanceof SelfCrossing) {
346            // free memory, we are not interested in previous ways
347            cellSegments.clear();
348            seenWays.clear();
349        }
350
351        int nodesSize = w.getNodesCount();
352        for (int i = 0; i < nodesSize - 1; i++) {
353            final WaySegment es1 = new WaySegment(w, i);
354            final EastNorth en1 = es1.getFirstNode().getEastNorth();
355            final EastNorth en2 = es1.getSecondNode().getEastNorth();
356            if (en1 == null || en2 == null) {
357                Logging.warn("Crossing ways test skipped " + es1);
358                continue;
359            }
360            for (List<WaySegment> segments : getSegments(cellSegments, en1, en2)) {
361                for (WaySegment es2 : segments) {
362                    List<Way> prims;
363                    List<WaySegment> highlight;
364
365                    if (!es1.intersects(es2) || ignoreWaySegmentCombination(es1.way, es2.way)) {
366                        continue;
367                    }
368
369                    prims = new ArrayList<>();
370                    prims.add(es1.way);
371                    if (es1.way != es2.way)
372                        prims.add(es2.way);
373                    if ((highlight = seenWays.get(prims)) == null) {
374                        highlight = new ArrayList<>();
375                        highlight.add(es1);
376                        highlight.add(es2);
377
378                        final MessageHelper message = createMessage(es1.way, es2.way);
379                        errors.add(TestError.builder(this, Severity.WARNING, message.code)
380                                .message(message.message)
381                                .primitives(prims)
382                                .highlightWaySegments(highlight)
383                                .build());
384                        seenWays.put(prims, highlight);
385                    } else {
386                        highlight.add(es1);
387                        highlight.add(es2);
388                    }
389                }
390                segments.add(es1);
391            }
392        }
393    }
394
395    /**
396     * Returns all the cells this segment crosses.  Each cell contains the list
397     * of segments already processed
398     * @param cellSegments map with already collected way segments
399     * @param n1 The first EastNorth
400     * @param n2 The second EastNorth
401     * @return A list with all the cells the segment crosses
402     */
403    public static List<List<WaySegment>> getSegments(Map<Point2D, List<WaySegment>> cellSegments, EastNorth n1, EastNorth n2) {
404        List<List<WaySegment>> cells = new ArrayList<>();
405        for (Point2D cell : ValUtil.getSegmentCells(n1, n2, OsmValidator.getGridDetail())) {
406            cells.add(cellSegments.computeIfAbsent(cell, k -> new ArrayList<>()));
407        }
408        return cells;
409    }
410}