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.HashMap;
009import java.util.List;
010import java.util.Map;
011import java.util.Objects;
012
013import org.openstreetmap.josm.data.coor.EastNorth;
014import org.openstreetmap.josm.data.osm.OsmPrimitive;
015import org.openstreetmap.josm.data.osm.OsmUtils;
016import org.openstreetmap.josm.data.osm.Relation;
017import org.openstreetmap.josm.data.osm.Way;
018import org.openstreetmap.josm.data.osm.WaySegment;
019import org.openstreetmap.josm.data.validation.OsmValidator;
020import org.openstreetmap.josm.data.validation.Severity;
021import org.openstreetmap.josm.data.validation.Test;
022import org.openstreetmap.josm.data.validation.TestError;
023import org.openstreetmap.josm.data.validation.util.ValUtil;
024import org.openstreetmap.josm.gui.progress.ProgressMonitor;
025import org.openstreetmap.josm.tools.Logging;
026
027/**
028 * Tests if there are segments that crosses in the same layer
029 *
030 * @author frsantos
031 */
032public abstract class CrossingWays extends Test {
033
034    static final String HIGHWAY = "highway";
035    static final String RAILWAY = "railway";
036    static final String WATERWAY = "waterway";
037
038    /** All way segments, grouped by cells */
039    private final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000);
040    /** The already detected ways in error */
041    private final Map<List<Way>, List<WaySegment>> seenWays = new HashMap<>(50);
042
043    private final int code;
044
045    /**
046     * General crossing ways test.
047     */
048    public static class Ways extends CrossingWays {
049
050        protected static final int CROSSING_WAYS = 601;
051
052        /**
053         * Constructs a new crossing {@code Ways} test.
054         */
055        public Ways() {
056            super(tr("Crossing ways"), CROSSING_WAYS);
057        }
058
059        @Override
060        public boolean isPrimitiveUsable(OsmPrimitive w) {
061            return super.isPrimitiveUsable(w)
062                    && !isProposedOrAbandoned(w)
063                    && (isHighway(w)
064                    || w.hasKey(WATERWAY)
065                    || isRailway(w)
066                    || isCoastline(w)
067                    || isBuilding(w));
068        }
069
070        @Override
071        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
072            if (w1 == w2)
073                return false;
074            if (!Objects.equals(OsmUtils.getLayer(w1), OsmUtils.getLayer(w2))) {
075                return true;
076            }
077            if (w1.hasKey(HIGHWAY) && w2.hasKey(HIGHWAY) && !Objects.equals(w1.get("level"), w2.get("level"))) {
078                return true;
079            }
080            if (isSubwayOrTramOrRazed(w2)) {
081                return true;
082            }
083            if (isCoastline(w1) != isCoastline(w2)) {
084                return true;
085            }
086            if ((w1.hasTag(WATERWAY, "river", "stream", "canal", "drain", "ditch") && w2.hasTag(WATERWAY, "riverbank"))
087             || (w2.hasTag(WATERWAY, "river", "stream", "canal", "drain", "ditch") && w1.hasTag(WATERWAY, "riverbank"))) {
088                return true;
089            }
090            return isProposedOrAbandoned(w2);
091        }
092
093        @Override
094        String createMessage(Way w1, Way w2) {
095            if (isBuilding(w1)) {
096                return tr("Crossing buildings");
097            } else if (w1.hasKey(WATERWAY) && w2.hasKey(WATERWAY)) {
098                return tr("Crossing waterways");
099            } else if ((w1.hasKey(HIGHWAY) && w2.hasKey(WATERWAY))
100                    || (w2.hasKey(HIGHWAY) && w1.hasKey(WATERWAY))) {
101                return tr("Crossing waterway/highway");
102            } else {
103                return tr("Crossing ways");
104            }
105        }
106    }
107
108    /**
109     * Crossing boundaries ways test.
110     */
111    public static class Boundaries extends CrossingWays {
112
113        protected static final int CROSSING_BOUNDARIES = 602;
114
115        /**
116         * Constructs a new crossing {@code Boundaries} test.
117         */
118        public Boundaries() {
119            super(tr("Crossing boundaries"), CROSSING_BOUNDARIES);
120        }
121
122        @Override
123        public boolean isPrimitiveUsable(OsmPrimitive p) {
124            return super.isPrimitiveUsable(p) && p.hasKey("boundary")
125                    && (!(p instanceof Relation) || (((Relation) p).isMultipolygon() && !((Relation) p).hasIncompleteMembers()));
126        }
127
128        @Override
129        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
130            return !Objects.equals(w1.get("boundary"), w2.get("boundary"));
131        }
132
133        @Override
134        String createMessage(Way w1, Way w2) {
135            return tr("Crossing boundaries");
136        }
137
138        @Override
139        public void visit(Relation r) {
140            for (Way w : r.getMemberPrimitives(Way.class)) {
141                visit(w);
142            }
143        }
144    }
145
146    /**
147     * Crossing barriers ways test.
148     */
149    public static class Barrier extends CrossingWays {
150
151        protected static final int CROSSING_BARRIERS = 603;
152
153        /**
154         * Constructs a new crossing {@code Barrier} test.
155         */
156        public Barrier() {
157            super(tr("Crossing barriers"), CROSSING_BARRIERS);
158        }
159
160        @Override
161        public boolean isPrimitiveUsable(OsmPrimitive p) {
162            return super.isPrimitiveUsable(p) && p.hasKey("barrier");
163        }
164
165        @Override
166        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
167            return !Objects.equals(OsmUtils.getLayer(w1), OsmUtils.getLayer(w2));
168        }
169
170        @Override
171        String createMessage(Way w1, Way w2) {
172            return tr("Crossing barriers");
173        }
174    }
175
176    /**
177     * Self crossing ways test (for all the rest)
178     */
179    public static class SelfCrossing extends CrossingWays {
180
181        protected static final int CROSSING_SELF = 604;
182
183        CrossingWays.Ways normalTest = new Ways();
184        CrossingWays.Barrier barrierTest = new Barrier();
185        CrossingWays.Boundaries boundariesTest = new Boundaries();
186
187        /**
188         * Constructs a new SelfIntersection test.
189         */
190        public SelfCrossing() {
191            super(tr("Self crossing"), CROSSING_SELF);
192        }
193
194        @Override
195        public boolean isPrimitiveUsable(OsmPrimitive p) {
196            return super.isPrimitiveUsable(p) && !(normalTest.isPrimitiveUsable(p) || barrierTest.isPrimitiveUsable(p)
197                    || boundariesTest.isPrimitiveUsable(p));
198        }
199
200        @Override
201        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
202            return w1 != w2; // should not happen
203        }
204
205        @Override
206        String createMessage(Way w1, Way w2) {
207            return tr("Self-crossing ways");
208        }
209    }
210
211    /**
212     * Constructs a new {@code CrossingWays} test.
213     * @param title The test title
214     * @param code The test code
215     * @since 12958
216     */
217    public CrossingWays(String title, int code) {
218        super(title, tr("This test checks if two roads, railways, waterways or buildings crosses in the same layer, " +
219                "but are not connected by a node."));
220        this.code = code;
221    }
222
223    @Override
224    public void startTest(ProgressMonitor monitor) {
225        super.startTest(monitor);
226        cellSegments.clear();
227        seenWays.clear();
228    }
229
230    @Override
231    public void endTest() {
232        super.endTest();
233        cellSegments.clear();
234        seenWays.clear();
235    }
236
237    static boolean isCoastline(OsmPrimitive w) {
238        return w.hasTag("natural", "water", "coastline") || w.hasTag("landuse", "reservoir");
239    }
240
241    static boolean isHighway(OsmPrimitive w) {
242        return w.hasTagDifferent(HIGHWAY, "rest_area", "services");
243    }
244
245    static boolean isRailway(OsmPrimitive w) {
246        return w.hasKey(RAILWAY) && !isSubwayOrTramOrRazed(w);
247    }
248
249    static boolean isSubwayOrTramOrRazed(OsmPrimitive w) {
250        return w.hasTag(RAILWAY, "subway", "tram", "razed");
251    }
252
253    static boolean isProposedOrAbandoned(OsmPrimitive w) {
254        return w.hasTag(HIGHWAY, "proposed") || w.hasTag(RAILWAY, "proposed", "abandoned");
255    }
256
257    abstract boolean ignoreWaySegmentCombination(Way w1, Way w2);
258
259    abstract String createMessage(Way w1, Way w2);
260
261    @Override
262    public void visit(Way w) {
263        if (this instanceof SelfCrossing) {
264            // free memory, we are not interested in previous ways
265            cellSegments.clear();
266            seenWays.clear();
267        }
268
269        int nodesSize = w.getNodesCount();
270        for (int i = 0; i < nodesSize - 1; i++) {
271            final WaySegment es1 = new WaySegment(w, i);
272            final EastNorth en1 = es1.getFirstNode().getEastNorth();
273            final EastNorth en2 = es1.getSecondNode().getEastNorth();
274            if (en1 == null || en2 == null) {
275                Logging.warn("Crossing ways test skipped "+es1);
276                continue;
277            }
278            for (List<WaySegment> segments : getSegments(cellSegments, en1, en2)) {
279                for (WaySegment es2 : segments) {
280                    List<Way> prims;
281                    List<WaySegment> highlight;
282
283                    if (!es1.intersects(es2) || ignoreWaySegmentCombination(es1.way, es2.way)) {
284                        continue;
285                    }
286
287                    prims = new ArrayList<>();
288                    prims.add(es1.way);
289                    if (es1.way != es2.way)
290                        prims.add(es2.way);
291                    if ((highlight = seenWays.get(prims)) == null) {
292                        highlight = new ArrayList<>();
293                        highlight.add(es1);
294                        highlight.add(es2);
295
296                        final String message = createMessage(es1.way, es2.way);
297                        errors.add(TestError.builder(this, Severity.WARNING, code)
298                                .message(message)
299                                .primitives(prims)
300                                .highlightWaySegments(highlight)
301                                .build());
302                        seenWays.put(prims, highlight);
303                    } else {
304                        highlight.add(es1);
305                        highlight.add(es2);
306                    }
307                }
308                segments.add(es1);
309            }
310        }
311    }
312
313    /**
314     * Returns all the cells this segment crosses.  Each cell contains the list
315     * of segments already processed
316     * @param cellSegments map with already collected way segments
317     * @param n1 The first EastNorth
318     * @param n2 The second EastNorth
319     * @return A list with all the cells the segment crosses
320     */
321    public static List<List<WaySegment>> getSegments(Map<Point2D, List<WaySegment>> cellSegments, EastNorth n1, EastNorth n2) {
322        List<List<WaySegment>> cells = new ArrayList<>();
323        for (Point2D cell : ValUtil.getSegmentCells(n1, n2, OsmValidator.getGridDetail())) {
324            cells.add(cellSegments.computeIfAbsent(cell, k -> new ArrayList<>()));
325        }
326        return cells;
327    }
328}