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