001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation.tests;
003
004import static org.openstreetmap.josm.data.validation.tests.CrossingWays.HIGHWAY;
005import static org.openstreetmap.josm.data.validation.tests.CrossingWays.RAILWAY;
006import static org.openstreetmap.josm.tools.I18n.tr;
007
008import java.util.ArrayList;
009import java.util.Arrays;
010import java.util.Collection;
011import java.util.Collections;
012import java.util.HashMap;
013import java.util.HashSet;
014import java.util.List;
015import java.util.Map;
016import java.util.Set;
017import java.util.TreeSet;
018
019import org.openstreetmap.josm.data.osm.Node;
020import org.openstreetmap.josm.data.osm.OsmPrimitive;
021import org.openstreetmap.josm.data.osm.OsmUtils;
022import org.openstreetmap.josm.data.osm.Relation;
023import org.openstreetmap.josm.data.osm.Way;
024import org.openstreetmap.josm.data.osm.WaySegment;
025import org.openstreetmap.josm.data.preferences.ListProperty;
026import org.openstreetmap.josm.data.validation.Severity;
027import org.openstreetmap.josm.data.validation.Test;
028import org.openstreetmap.josm.data.validation.TestError;
029import org.openstreetmap.josm.gui.progress.ProgressMonitor;
030import org.openstreetmap.josm.tools.MultiMap;
031import org.openstreetmap.josm.tools.Pair;
032
033/**
034 * Tests if there are overlapping ways.
035 *
036 * @author frsantos
037 */
038public class OverlappingWays extends Test {
039
040    /** Bag of all way segments */
041    private MultiMap<Pair<Node, Node>, WaySegment> nodePairs;
042
043    protected static final int OVERLAPPING_HIGHWAY = 101;
044    protected static final int OVERLAPPING_RAILWAY = 102;
045    protected static final int OVERLAPPING_WAY = 103;
046    protected static final int OVERLAPPING_HIGHWAY_AREA = 111;
047    protected static final int OVERLAPPING_RAILWAY_AREA = 112;
048    protected static final int OVERLAPPING_WAY_AREA = 113;
049    protected static final int OVERLAPPING_AREA = 120;
050    protected static final int DUPLICATE_WAY_SEGMENT = 121;
051
052    protected static final ListProperty IGNORED_KEYS = new ListProperty(
053            "overlapping-ways.ignored-keys", Arrays.asList(
054                    "barrier", "building", "historic:building", "demolished:building",
055                    "removed:building", "disused:building", "abandoned:building", "proposed:building", "man_made"));
056
057    /** Constructor */
058    public OverlappingWays() {
059        super(tr("Overlapping ways"),
060                tr("This test checks that a connection between two nodes "
061                        + "is not used by more than one way."));
062    }
063
064    @Override
065    public void startTest(ProgressMonitor monitor) {
066        super.startTest(monitor);
067        nodePairs = new MultiMap<>(1000);
068    }
069
070    private static boolean parentMultipolygonConcernsArea(OsmPrimitive p) {
071        for (Relation r : OsmPrimitive.getFilteredList(p.getReferrers(), Relation.class)) {
072            if (r.concernsArea()) {
073                return true;
074            }
075        }
076        return false;
077    }
078
079    @Override
080    public void endTest() {
081        Map<List<Way>, Set<WaySegment>> seenWays = new HashMap<>(500);
082
083        Collection<TestError> preliminaryErrors = new ArrayList<>();
084        for (Set<WaySegment> duplicated : nodePairs.values()) {
085            int ways = duplicated.size();
086
087            if (ways > 1) {
088                List<OsmPrimitive> prims = new ArrayList<>();
089                List<Way> currentWays = new ArrayList<>();
090                Collection<WaySegment> highlight;
091                int highway = 0;
092                int railway = 0;
093                int area = 0;
094
095                for (WaySegment ws : duplicated) {
096                    if (ws.way.hasKey(HIGHWAY)) {
097                        highway++;
098                    } else if (ws.way.hasKey(RAILWAY)) {
099                        railway++;
100                    }
101                    Boolean ar = OsmUtils.getOsmBoolean(ws.way.get("area"));
102                    if (ar != null && ar) {
103                        area++;
104                    }
105                    if (ws.way.concernsArea() || parentMultipolygonConcernsArea(ws.way)) {
106                        area++;
107                        ways--;
108                    }
109
110                    prims.add(ws.way);
111                    currentWays.add(ws.way);
112                }
113                // These ways not seen before
114                // If two or more of the overlapping ways are highways or railways mark a separate error
115                if ((highlight = seenWays.get(currentWays)) == null) {
116                    String errortype;
117                    int type;
118
119                    if (area > 0) {
120                        if (ways == 0 || duplicated.size() == area) {
121                            errortype = tr("Areas share segment");
122                            type = OVERLAPPING_AREA;
123                        } else if (highway == ways) {
124                            errortype = tr("Highways share segment with area");
125                            type = OVERLAPPING_HIGHWAY_AREA;
126                        } else if (railway == ways) {
127                            errortype = tr("Railways share segment with area");
128                            type = OVERLAPPING_RAILWAY_AREA;
129                        } else {
130                            errortype = tr("Ways share segment with area");
131                            type = OVERLAPPING_WAY_AREA;
132                        }
133                    } else if (highway == ways) {
134                        errortype = tr("Overlapping highways");
135                        type = OVERLAPPING_HIGHWAY;
136                    } else if (railway == ways) {
137                        errortype = tr("Overlapping railways");
138                        type = OVERLAPPING_RAILWAY;
139                    } else {
140                        errortype = tr("Overlapping ways");
141                        type = OVERLAPPING_WAY;
142                    }
143
144                    Severity severity = type < OVERLAPPING_HIGHWAY_AREA ? Severity.WARNING : Severity.OTHER;
145                    preliminaryErrors.add(TestError.builder(this, severity, type)
146                            .message(errortype)
147                            .primitives(prims)
148                            .highlightWaySegments(duplicated)
149                            .build());
150                    seenWays.put(currentWays, duplicated);
151                } else { /* way seen, mark highlight layer only */
152                    highlight.addAll(duplicated);
153                }
154            }
155        }
156
157        // see ticket #9598 - only report if at least 3 segments are shared, except for overlapping ways, i.e warnings (see #9820)
158        for (TestError error : preliminaryErrors) {
159            if (error.getSeverity().equals(Severity.WARNING) || error.getHighlighted().size() / error.getPrimitives().size() >= 3) {
160                boolean ignore = false;
161                for (String ignoredKey : IGNORED_KEYS.get()) {
162                    if (error.getPrimitives().stream().anyMatch(p -> p.hasKey(ignoredKey))) {
163                        ignore = true;
164                        break;
165                    }
166                }
167                if (!ignore) {
168                    errors.add(error);
169                }
170            }
171        }
172
173        super.endTest();
174        nodePairs = null;
175    }
176
177    protected static Set<WaySegment> checkDuplicateWaySegment(Way w) {
178        // test for ticket #4959
179        Set<WaySegment> segments = new TreeSet<>((o1, o2) -> {
180            final List<Node> n1 = Arrays.asList(o1.getFirstNode(), o1.getSecondNode());
181            final List<Node> n2 = Arrays.asList(o2.getFirstNode(), o2.getSecondNode());
182            Collections.sort(n1);
183            Collections.sort(n2);
184            final int first = n1.get(0).compareTo(n2.get(0));
185            final int second = n1.get(1).compareTo(n2.get(1));
186            return first != 0 ? first : second;
187        });
188        final Set<WaySegment> duplicateWaySegments = new HashSet<>();
189
190        for (int i = 0; i < w.getNodesCount() - 1; i++) {
191            final WaySegment segment = new WaySegment(w, i);
192            final boolean wasInSet = !segments.add(segment);
193            if (wasInSet) {
194                duplicateWaySegments.add(segment);
195            }
196        }
197        if (duplicateWaySegments.size() > 1) {
198            return duplicateWaySegments;
199        } else {
200            return null;
201        }
202    }
203
204    @Override
205    public void visit(Way w) {
206
207        final Set<WaySegment> duplicateWaySegment = checkDuplicateWaySegment(w);
208        if (duplicateWaySegment != null) {
209            errors.add(TestError.builder(this, Severity.ERROR, DUPLICATE_WAY_SEGMENT)
210                    .message(tr("Way contains segment twice"))
211                    .primitives(w)
212                    .highlightWaySegments(duplicateWaySegment)
213                    .build());
214            return;
215        }
216
217        Node lastN = null;
218        int i = -2;
219        for (Node n : w.getNodes()) {
220            i++;
221            if (lastN == null) {
222                lastN = n;
223                continue;
224            }
225            nodePairs.put(Pair.sort(new Pair<>(lastN, n)),
226                    new WaySegment(w, i));
227            lastN = n;
228        }
229    }
230}