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