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