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.Main;
017import org.openstreetmap.josm.data.coor.EastNorth;
018import org.openstreetmap.josm.data.osm.OsmPrimitive;
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;
028
029/**
030 * Tests if there are segments that crosses in the same layer
031 *
032 * @author frsantos
033 */
034public abstract class CrossingWays extends Test {
035    protected static final int CROSSING_WAYS = 601;
036
037    private static final String HIGHWAY = "highway";
038    private static final String RAILWAY = "railway";
039    private static final String WATERWAY = "waterway";
040
041    /** All way segments, grouped by cells */
042    private Map<Point2D, List<WaySegment>> cellSegments;
043    /** The already detected errors */
044    private Set<WaySegment> errorSegments;
045    /** The already detected ways in error */
046    private Map<List<Way>, List<WaySegment>> seenWays;
047
048    /**
049     * General crossing ways test.
050     */
051    public static class Ways extends CrossingWays {
052
053        /**
054         * Constructs a new crossing {@code Ways} test.
055         */
056        public Ways() {
057            super(tr("Crossing ways"));
058        }
059
060        @Override
061        public boolean isPrimitiveUsable(OsmPrimitive w) {
062            return super.isPrimitiveUsable(w)
063                    && !isProposedOrAbandoned(w)
064                    && ((w.hasKey(HIGHWAY) && !w.hasTag(HIGHWAY, "rest_area", "services"))
065                    || w.hasKey(WATERWAY)
066                    || (w.hasKey(RAILWAY) && !isSubwayOrTram(w))
067                    || isCoastline(w)
068                    || isBuilding(w));
069        }
070
071        @Override
072        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
073            if (!Objects.equals(getLayer(w1), getLayer(w2))) {
074                return true;
075            }
076            if (w1.hasKey(HIGHWAY) && w2.hasKey(HIGHWAY) && !Objects.equals(w1.get("level"), w2.get("level"))) {
077                return true;
078            }
079            if (isSubwayOrTram(w2)) {
080                return true;
081            }
082            if (isCoastline(w1) != isCoastline(w2)) {
083                return true;
084            }
085            if ((w1.hasTag(WATERWAY, "river") && w2.hasTag(WATERWAY, "riverbank"))
086                    || (w2.hasTag(WATERWAY, "river") && w1.hasTag(WATERWAY, "riverbank"))) {
087                return true;
088            }
089            if (isProposedOrAbandoned(w2)) {
090                return true;
091            }
092            return false;
093        }
094
095        @Override
096        String createMessage(Way w1, Way w2) {
097            if (isBuilding(w1)) {
098                return tr("Crossing buildings");
099            } else if (w1.hasKey(WATERWAY) && w2.hasKey(WATERWAY)) {
100                return tr("Crossing waterways");
101            } else if ((w1.hasKey(HIGHWAY) && w2.hasKey(WATERWAY))
102                    || (w2.hasKey(HIGHWAY) && w1.hasKey(WATERWAY))) {
103                return tr("Crossing waterway/highway");
104            } else {
105                return tr("Crossing ways");
106            }
107        }
108    }
109
110    /**
111     * Crossing boundaries ways test.
112     */
113    public static class Boundaries extends CrossingWays {
114
115        /**
116         * Constructs a new crossing {@code Boundaries} test.
117         */
118        public Boundaries() {
119            super(tr("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        /**
152         * Constructs a new crossing {@code Barrier} test.
153         */
154        public Barrier() {
155            super(tr("Crossing barriers"));
156        }
157
158        @Override
159        public boolean isPrimitiveUsable(OsmPrimitive p) {
160            return super.isPrimitiveUsable(p) && p.hasKey("barrier");
161        }
162
163        @Override
164        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
165            if (!Objects.equals(getLayer(w1), getLayer(w2))) {
166                return true;
167            }
168            return false;
169        }
170
171        @Override
172        String createMessage(Way w1, Way w2) {
173            return tr("Crossing barriers");
174        }
175    }
176
177    /**
178     * Constructs a new {@code CrossingWays} test.
179     * @param title The test title
180     * @since 6691
181     */
182    public CrossingWays(String title) {
183        super(title, tr("This test checks if two roads, railways, waterways or buildings crosses in the same layer, " +
184                "but are not connected by a node."));
185    }
186
187    @Override
188    public void startTest(ProgressMonitor monitor) {
189        super.startTest(monitor);
190        cellSegments = new HashMap<>(1000);
191        errorSegments = new HashSet<>();
192        seenWays = new HashMap<>(50);
193    }
194
195    @Override
196    public void endTest() {
197        super.endTest();
198        cellSegments = null;
199        errorSegments = null;
200        seenWays = null;
201    }
202
203    static String getLayer(OsmPrimitive w) {
204        String layer1 = w.get("layer");
205        if ("0".equals(layer1)) {
206            layer1 = null; // 0 is default value for layer.
207        }
208        return layer1;
209    }
210
211    static boolean isCoastline(OsmPrimitive w) {
212        return w.hasTag("natural", "water", "coastline") || w.hasTag("landuse", "reservoir");
213    }
214
215    static boolean isSubwayOrTram(OsmPrimitive w) {
216        return w.hasTag(RAILWAY, "subway", "tram");
217    }
218
219    static boolean isProposedOrAbandoned(OsmPrimitive w) {
220        return w.hasTag(HIGHWAY, "proposed") || w.hasTag(RAILWAY, "proposed", "abandoned");
221    }
222
223    abstract boolean ignoreWaySegmentCombination(Way w1, Way w2);
224
225    abstract String createMessage(Way w1, Way w2);
226
227    @Override
228    public void visit(Way w) {
229
230        int nodesSize = w.getNodesCount();
231        for (int i = 0; i < nodesSize - 1; i++) {
232            final WaySegment es1 = new WaySegment(w, i);
233            final EastNorth en1 = es1.getFirstNode().getEastNorth();
234            final EastNorth en2 = es1.getSecondNode().getEastNorth();
235            if (en1 == null || en2 == null) {
236                Main.warn("Crossing ways test skipped "+es1);
237                continue;
238            }
239            for (List<WaySegment> segments : getSegments(en1, en2)) {
240                for (WaySegment es2 : segments) {
241                    List<Way> prims;
242                    List<WaySegment> highlight;
243
244                    if (errorSegments.contains(es1) && errorSegments.contains(es2)
245                            || !es1.intersects(es2)
246                            || ignoreWaySegmentCombination(es1.way, es2.way)) {
247                        continue;
248                    }
249
250                    prims = Arrays.asList(es1.way, es2.way);
251                    if ((highlight = seenWays.get(prims)) == null) {
252                        highlight = new ArrayList<>();
253                        highlight.add(es1);
254                        highlight.add(es2);
255
256                        final String message = createMessage(es1.way, es2.way);
257                        errors.add(new TestError(this, Severity.WARNING,
258                                message,
259                                CROSSING_WAYS,
260                                prims,
261                                highlight));
262                        seenWays.put(prims, highlight);
263                    } else {
264                        highlight.add(es1);
265                        highlight.add(es2);
266                    }
267                }
268                segments.add(es1);
269            }
270        }
271    }
272
273    /**
274     * Returns all the cells this segment crosses.  Each cell contains the list
275     * of segments already processed
276     *
277     * @param n1 The first EastNorth
278     * @param n2 The second EastNorth
279     * @return A list with all the cells the segment crosses
280     */
281    public List<List<WaySegment>> getSegments(EastNorth n1, EastNorth n2) {
282
283        List<List<WaySegment>> cells = new ArrayList<>();
284        for (Point2D cell : ValUtil.getSegmentCells(n1, n2, OsmValidator.griddetail)) {
285            List<WaySegment> segments = cellSegments.get(cell);
286            if (segments == null) {
287                segments = new ArrayList<>();
288                cellSegments.put(cell, segments);
289            }
290            cells.add(segments);
291        }
292        return cells;
293    }
294}