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    /**
041     * Type of way. Entries have to be declared in alphabetical order, see sort below.
042     */
043    private enum WayType {
044        BUILDING, HIGHWAY, RAILWAY, RESIDENTIAL_AREA, WATERWAY, WAY;
045
046        static WayType of(Way w) {
047            if (isBuilding(w))
048                return BUILDING;
049            else if (w.hasKey(CrossingWays.HIGHWAY))
050                return HIGHWAY;
051            else if (isRailway(w))
052                return RAILWAY;
053            else if (isResidentialArea(w))
054                return RESIDENTIAL_AREA;
055            else if (w.hasKey(CrossingWays.WATERWAY))
056                return WATERWAY;
057            else
058                return WAY;
059        }
060    }
061
062    /** All way segments, grouped by cells */
063    private final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000);
064    /** The already detected ways in error */
065    private final Map<List<Way>, List<WaySegment>> seenWays = new HashMap<>(50);
066
067    private final int code;
068
069    /**
070     * General crossing ways test.
071     */
072    public static class Ways extends CrossingWays {
073
074        protected static final int CROSSING_WAYS = 601;
075
076        /**
077         * Constructs a new crossing {@code Ways} test.
078         */
079        public Ways() {
080            super(tr("Crossing ways"), CROSSING_WAYS);
081        }
082
083        @Override
084        public boolean isPrimitiveUsable(OsmPrimitive w) {
085            return super.isPrimitiveUsable(w)
086                    && !isProposedOrAbandoned(w)
087                    && (isHighway(w)
088                    || w.hasKey(WATERWAY)
089                    || isRailway(w)
090                    || isCoastline(w)
091                    || isBuilding(w)
092                    || isResidentialArea(w));
093        }
094
095        @Override
096        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
097            if (w1 == w2)
098                return false;
099            if (!Objects.equals(OsmUtils.getLayer(w1), OsmUtils.getLayer(w2))) {
100                return true;
101            }
102            if (w1.hasKey(HIGHWAY) && w2.hasKey(HIGHWAY) && !Objects.equals(w1.get("level"), w2.get("level"))) {
103                return true;
104            }
105            if ((w1.hasKey(HIGHWAY, RAILWAY, WATERWAY) && isResidentialArea(w2))
106             || (w2.hasKey(HIGHWAY, RAILWAY, WATERWAY) && isResidentialArea(w1)))
107                return true;
108            if (isSubwayOrTramOrRazed(w2)) {
109                return true;
110            }
111            if (isCoastline(w1) != isCoastline(w2)) {
112                return true;
113            }
114            if ((w1.hasTag(WATERWAY, "river", "stream", "canal", "drain", "ditch") && w2.hasTag(WATERWAY, "riverbank"))
115             || (w2.hasTag(WATERWAY, "river", "stream", "canal", "drain", "ditch") && w1.hasTag(WATERWAY, "riverbank"))) {
116                return true;
117            }
118            return isProposedOrAbandoned(w2);
119        }
120
121        @Override
122        String createMessage(Way w1, Way w2) {
123            WayType[] types = {WayType.of(w1), WayType.of(w2)};
124            Arrays.sort(types);
125
126            if (types[0] == types[1]) {
127                switch(types[0]) {
128                    case BUILDING:
129                        return tr("Crossing buildings");
130                    case HIGHWAY:
131                        return tr("Crossing highways");
132                    case RAILWAY:
133                        return tr("Crossing railways");
134                    case RESIDENTIAL_AREA:
135                        return tr("Crossing residential areas");
136                    case WATERWAY:
137                        return tr("Crossing waterways");
138                    case WAY:
139                    default:
140                        return tr("Crossing ways");
141                }
142            } else {
143                switch (types[0]) {
144                    case BUILDING:
145                        switch (types[1]) {
146                            case HIGHWAY:
147                                return tr("Crossing building/highway");
148                            case RAILWAY:
149                                return tr("Crossing building/railway");
150                            case RESIDENTIAL_AREA:
151                                return tr("Crossing building/residential area");
152                            case WATERWAY:
153                                return tr("Crossing building/waterway");
154                            case WAY:
155                            default:
156                                return tr("Crossing building/way");
157                        }
158                    case HIGHWAY:
159                        switch (types[1]) {
160                            case RAILWAY:
161                                return tr("Crossing highway/railway");
162                            case WATERWAY:
163                                return tr("Crossing highway/waterway");
164                            case WAY:
165                            default:
166                                return tr("Crossing highway/way");
167                        }
168                    case RAILWAY:
169                        switch (types[1]) {
170                            case WATERWAY:
171                                return tr("Crossing railway/waterway");
172                            case WAY:
173                            default:
174                                return tr("Crossing railway/way");
175                        }
176                    case RESIDENTIAL_AREA:
177                        switch (types[1]) {
178                            case WAY:
179                            default:
180                                return tr("Crossing residential area/way");
181                        }
182                    case WATERWAY:
183                    default:
184                        return tr("Crossing waterway/way");
185                }
186            }
187        }
188    }
189
190    /**
191     * Crossing boundaries ways test.
192     */
193    public static class Boundaries extends CrossingWays {
194
195        protected static final int CROSSING_BOUNDARIES = 602;
196
197        /**
198         * Constructs a new crossing {@code Boundaries} test.
199         */
200        public Boundaries() {
201            super(tr("Crossing boundaries"), CROSSING_BOUNDARIES);
202        }
203
204        @Override
205        public boolean isPrimitiveUsable(OsmPrimitive p) {
206            return super.isPrimitiveUsable(p) && p.hasKey("boundary")
207                    && (!(p instanceof Relation) || (((Relation) p).isMultipolygon() && !((Relation) p).hasIncompleteMembers()));
208        }
209
210        @Override
211        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
212            return !Objects.equals(w1.get("boundary"), w2.get("boundary"));
213        }
214
215        @Override
216        String createMessage(Way w1, Way w2) {
217            return tr("Crossing boundaries");
218        }
219
220        @Override
221        public void visit(Relation r) {
222            for (Way w : r.getMemberPrimitives(Way.class)) {
223                visit(w);
224            }
225        }
226    }
227
228    /**
229     * Crossing barriers ways test.
230     */
231    public static class Barrier extends CrossingWays {
232
233        protected static final int CROSSING_BARRIERS = 603;
234
235        /**
236         * Constructs a new crossing {@code Barrier} test.
237         */
238        public Barrier() {
239            super(tr("Crossing barriers"), CROSSING_BARRIERS);
240        }
241
242        @Override
243        public boolean isPrimitiveUsable(OsmPrimitive p) {
244            return super.isPrimitiveUsable(p) && p.hasKey("barrier");
245        }
246
247        @Override
248        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
249            return !Objects.equals(OsmUtils.getLayer(w1), OsmUtils.getLayer(w2));
250        }
251
252        @Override
253        String createMessage(Way w1, Way w2) {
254            return tr("Crossing barriers");
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"), 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        @Override
288        String createMessage(Way w1, Way w2) {
289            return tr("Self-crossing ways");
290        }
291    }
292
293    /**
294     * Constructs a new {@code CrossingWays} test.
295     * @param title The test title
296     * @param code The test code
297     * @since 12958
298     */
299    public CrossingWays(String title, int code) {
300        super(title, tr("This test checks if two roads, railways, waterways or buildings crosses in the same layer, " +
301                "but are not connected by a node."));
302        this.code = code;
303    }
304
305    @Override
306    public void startTest(ProgressMonitor monitor) {
307        super.startTest(monitor);
308        cellSegments.clear();
309        seenWays.clear();
310    }
311
312    @Override
313    public void endTest() {
314        super.endTest();
315        cellSegments.clear();
316        seenWays.clear();
317    }
318
319    static boolean isCoastline(OsmPrimitive w) {
320        return w.hasTag("natural", "water", "coastline") || w.hasTag(LANDUSE, "reservoir");
321    }
322
323    static boolean isHighway(OsmPrimitive w) {
324        return w.hasTagDifferent(HIGHWAY, "rest_area", "services");
325    }
326
327    static boolean isRailway(OsmPrimitive w) {
328        return w.hasKey(RAILWAY) && !isSubwayOrTramOrRazed(w);
329    }
330
331    static boolean isSubwayOrTramOrRazed(OsmPrimitive w) {
332        return w.hasTag(RAILWAY, "subway", "tram", "razed");
333    }
334
335    static boolean isProposedOrAbandoned(OsmPrimitive w) {
336        return w.hasTag(HIGHWAY, "proposed") || w.hasTag(RAILWAY, "proposed", "abandoned");
337    }
338
339    abstract boolean ignoreWaySegmentCombination(Way w1, Way w2);
340
341    abstract String createMessage(Way w1, Way w2);
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 String message = createMessage(es1.way, es2.way);
379                        errors.add(TestError.builder(this, Severity.WARNING, code)
380                                .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}