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