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}