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}