source: josm/trunk/src/org/openstreetmap/josm/data/validation/tests/DuplicateNode.java@ 13250

Last change on this file since 13250 was 12846, checked in by bastiK, 7 years ago

see #15229 - use Config.getPref() wherever possible

  • Property svn:eol-style set to native
File size: 14.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation.tests;
3
4import static org.openstreetmap.josm.data.validation.tests.CrossingWays.HIGHWAY;
5import static org.openstreetmap.josm.data.validation.tests.CrossingWays.RAILWAY;
6import static org.openstreetmap.josm.data.validation.tests.CrossingWays.WATERWAY;
7import static org.openstreetmap.josm.tools.I18n.tr;
8
9import java.util.ArrayList;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.HashMap;
13import java.util.HashSet;
14import java.util.Iterator;
15import java.util.LinkedHashSet;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Map;
19import java.util.Map.Entry;
20import java.util.Set;
21
22import org.openstreetmap.josm.actions.MergeNodesAction;
23import org.openstreetmap.josm.command.Command;
24import org.openstreetmap.josm.data.coor.LatLon;
25import org.openstreetmap.josm.data.osm.AbstractPrimitive;
26import org.openstreetmap.josm.data.osm.Hash;
27import org.openstreetmap.josm.data.osm.Node;
28import org.openstreetmap.josm.data.osm.OsmPrimitive;
29import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
30import org.openstreetmap.josm.data.osm.Storage;
31import org.openstreetmap.josm.data.osm.Way;
32import org.openstreetmap.josm.data.validation.Severity;
33import org.openstreetmap.josm.data.validation.Test;
34import org.openstreetmap.josm.data.validation.TestError;
35import org.openstreetmap.josm.gui.progress.ProgressMonitor;
36import org.openstreetmap.josm.spi.preferences.Config;
37import org.openstreetmap.josm.tools.MultiMap;
38
39/**
40 * Tests if there are duplicate nodes
41 *
42 * @author frsantos
43 */
44public class DuplicateNode extends Test {
45
46 private static class NodeHash implements Hash<Object, Object> {
47
48 private final double precision = Config.getPref().getDouble("validator.duplicatenodes.precision", 0.);
49
50 private LatLon roundCoord(LatLon coor) {
51 return new LatLon(
52 Math.round(coor.lat() / precision) * precision,
53 Math.round(coor.lon() / precision) * precision
54 );
55 }
56
57 @SuppressWarnings("unchecked")
58 private LatLon getLatLon(Object o) {
59 if (o instanceof Node) {
60 LatLon coor = ((Node) o).getCoor();
61 if (coor == null)
62 return null;
63 if (precision == 0)
64 return coor.getRoundedToOsmPrecision();
65 return roundCoord(coor);
66 } else if (o instanceof List<?>) {
67 LatLon coor = ((List<Node>) o).get(0).getCoor();
68 if (coor == null)
69 return null;
70 if (precision == 0)
71 return coor.getRoundedToOsmPrecision();
72 return roundCoord(coor);
73 } else
74 throw new AssertionError();
75 }
76
77 @Override
78 public boolean equals(Object k, Object t) {
79 LatLon coorK = getLatLon(k);
80 LatLon coorT = getLatLon(t);
81 return coorK == coorT || (coorK != null && coorT != null && coorK.equals(coorT));
82 }
83
84 @Override
85 public int getHashCode(Object k) {
86 LatLon coorK = getLatLon(k);
87 return coorK == null ? 0 : coorK.hashCode();
88 }
89 }
90
91 protected static final int DUPLICATE_NODE = 1;
92 protected static final int DUPLICATE_NODE_MIXED = 2;
93 protected static final int DUPLICATE_NODE_OTHER = 3;
94 protected static final int DUPLICATE_NODE_BUILDING = 10;
95 protected static final int DUPLICATE_NODE_BOUNDARY = 11;
96 protected static final int DUPLICATE_NODE_HIGHWAY = 12;
97 protected static final int DUPLICATE_NODE_LANDUSE = 13;
98 protected static final int DUPLICATE_NODE_NATURAL = 14;
99 protected static final int DUPLICATE_NODE_POWER = 15;
100 protected static final int DUPLICATE_NODE_RAILWAY = 16;
101 protected static final int DUPLICATE_NODE_WATERWAY = 17;
102
103 private static final String[] TYPES = {
104 "none", HIGHWAY, RAILWAY, WATERWAY, "boundary", "power", "natural", "landuse", "building"};
105
106 /** The map of potential duplicates.
107 *
108 * If there is exactly one node for a given pos, the map includes a pair &lt;pos, Node&gt;.
109 * If there are multiple nodes for a given pos, the map includes a pair
110 * &lt;pos, NodesByEqualTagsMap&gt;
111 */
112 private Storage<Object> potentialDuplicates;
113
114 /**
115 * Constructor
116 */
117 public DuplicateNode() {
118 super(tr("Duplicated nodes"),
119 tr("This test checks that there are no nodes at the very same location."));
120 }
121
122 @Override
123 public void startTest(ProgressMonitor monitor) {
124 super.startTest(monitor);
125 potentialDuplicates = new Storage<>(new NodeHash());
126 }
127
128 @SuppressWarnings("unchecked")
129 @Override
130 public void endTest() {
131 for (Object v: potentialDuplicates) {
132 if (v instanceof Node) {
133 // just one node at this position. Nothing to report as error
134 continue;
135 }
136
137 // multiple nodes at the same position -> check if all nodes have a distinct elevation
138 List<Node> nodes = (List<Node>) v;
139 Set<String> eles = new HashSet<>();
140 for (Node n : nodes) {
141 String ele = n.get("ele");
142 if (ele != null) {
143 eles.add(ele);
144 }
145 }
146 if (eles.size() == nodes.size()) {
147 // All nodes at this position have a distinct elevation.
148 // This is normal in some particular cases (for example, geodesic points in France)
149 // Do not report this as an error
150 continue;
151 }
152
153 // report errors
154 errors.addAll(buildTestErrors(this, nodes));
155 }
156 super.endTest();
157 potentialDuplicates = null;
158 }
159
160 /**
161 * Returns the list of "duplicate nodes" errors for the given selection of node and parent test
162 * @param parentTest The parent test of returned errors
163 * @param nodes The nodes selction to look into
164 * @return the list of "duplicate nodes" errors
165 */
166 public List<TestError> buildTestErrors(Test parentTest, List<Node> nodes) {
167 List<TestError> errors = new ArrayList<>();
168
169 MultiMap<Map<String, String>, OsmPrimitive> mm = new MultiMap<>();
170 for (Node n: nodes) {
171 mm.put(n.getKeys(), n);
172 }
173
174 Map<String, Boolean> typeMap = new HashMap<>();
175
176 // check whether we have multiple nodes at the same position with the same tag set
177 for (Iterator<Map<String, String>> it = mm.keySet().iterator(); it.hasNext();) {
178 Set<OsmPrimitive> primitives = mm.get(it.next());
179 if (primitives.size() > 1) {
180
181 for (String type: TYPES) {
182 typeMap.put(type, Boolean.FALSE);
183 }
184
185 for (OsmPrimitive p : primitives) {
186 if (p.getType() == OsmPrimitiveType.NODE) {
187 Node n = (Node) p;
188 List<OsmPrimitive> lp = n.getReferrers();
189 for (OsmPrimitive sp: lp) {
190 if (sp.getType() == OsmPrimitiveType.WAY) {
191 boolean typed = false;
192 Way w = (Way) sp;
193 Map<String, String> keys = w.getKeys();
194 for (String type: typeMap.keySet()) {
195 if (keys.containsKey(type)) {
196 typeMap.put(type, Boolean.TRUE);
197 typed = true;
198 }
199 }
200 if (!typed) {
201 typeMap.put("none", Boolean.TRUE);
202 }
203 }
204 }
205 }
206 }
207
208 long nbType = typeMap.entrySet().stream().filter(Entry::getValue).count();
209
210 if (nbType > 1) {
211 errors.add(TestError.builder(parentTest, Severity.WARNING, DUPLICATE_NODE_MIXED)
212 .message(tr("Mixed type duplicated nodes"))
213 .primitives(primitives)
214 .build());
215 } else if (typeMap.get(HIGHWAY)) {
216 errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_HIGHWAY)
217 .message(tr("Highway duplicated nodes"))
218 .primitives(primitives)
219 .build());
220 } else if (typeMap.get(RAILWAY)) {
221 errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_RAILWAY)
222 .message(tr("Railway duplicated nodes"))
223 .primitives(primitives)
224 .build());
225 } else if (typeMap.get(WATERWAY)) {
226 errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_WATERWAY)
227 .message(tr("Waterway duplicated nodes"))
228 .primitives(primitives)
229 .build());
230 } else if (typeMap.get("boundary")) {
231 errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_BOUNDARY)
232 .message(tr("Boundary duplicated nodes"))
233 .primitives(primitives)
234 .build());
235 } else if (typeMap.get("power")) {
236 errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_POWER)
237 .message(tr("Power duplicated nodes"))
238 .primitives(primitives)
239 .build());
240 } else if (typeMap.get("natural")) {
241 errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_NATURAL)
242 .message(tr("Natural duplicated nodes"))
243 .primitives(primitives)
244 .build());
245 } else if (typeMap.get("building")) {
246 errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_BUILDING)
247 .message(tr("Building duplicated nodes"))
248 .primitives(primitives)
249 .build());
250 } else if (typeMap.get("landuse")) {
251 errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_LANDUSE)
252 .message(tr("Landuse duplicated nodes"))
253 .primitives(primitives)
254 .build());
255 } else {
256 errors.add(TestError.builder(parentTest, Severity.WARNING, DUPLICATE_NODE_OTHER)
257 .message(tr("Other duplicated nodes"))
258 .primitives(primitives)
259 .build());
260 }
261 it.remove();
262 }
263 }
264
265 // check whether we have multiple nodes at the same position with differing tag sets
266 if (!mm.isEmpty()) {
267 List<OsmPrimitive> duplicates = new ArrayList<>();
268 for (Set<OsmPrimitive> l: mm.values()) {
269 duplicates.addAll(l);
270 }
271 if (duplicates.size() > 1) {
272 errors.add(TestError.builder(parentTest, Severity.WARNING, DUPLICATE_NODE)
273 .message(tr("Nodes at same position"))
274 .primitives(duplicates)
275 .build());
276 }
277 }
278 return errors;
279 }
280
281 @SuppressWarnings("unchecked")
282 @Override
283 public void visit(Node n) {
284 if (n.isUsable()) {
285 if (potentialDuplicates.get(n) == null) {
286 // in most cases there is just one node at a given position. We
287 // avoid to create an extra object and add remember the node
288 // itself at this position
289 potentialDuplicates.put(n);
290 } else if (potentialDuplicates.get(n) instanceof Node) {
291 // we have an additional node at the same position. Create an extra
292 // object to keep track of the nodes at this position.
293 //
294 Node n1 = (Node) potentialDuplicates.get(n);
295 List<Node> nodes = new ArrayList<>(2);
296 nodes.add(n1);
297 nodes.add(n);
298 potentialDuplicates.put(nodes);
299 } else if (potentialDuplicates.get(n) instanceof List<?>) {
300 // we have multiple nodes at the same position.
301 //
302 List<Node> nodes = (List<Node>) potentialDuplicates.get(n);
303 nodes.add(n);
304 }
305 }
306 }
307
308 /**
309 * Merge the nodes into one.
310 * Copied from UtilsPlugin.MergePointsAction
311 */
312 @Override
313 public Command fixError(TestError testError) {
314 Collection<OsmPrimitive> sel = new LinkedList<>(testError.getPrimitives());
315 Set<Node> nodes = new LinkedHashSet<>(OsmPrimitive.getFilteredList(sel, Node.class));
316
317 // Filter nodes that have already been deleted (see #5764 and #5773)
318 nodes.removeIf(AbstractPrimitive::isDeleted);
319
320 // Merge only if at least 2 nodes remain
321 if (nodes.size() >= 2) {
322 // Use first existing node or first node if all nodes are new
323 Node target = null;
324 for (Node n: nodes) {
325 if (!n.isNew()) {
326 target = n;
327 break;
328 }
329 }
330 if (target == null) {
331 target = nodes.iterator().next();
332 }
333
334 if (Command.checkOutlyingOrIncompleteOperation(nodes, Collections.singleton(target)) == Command.IS_OK)
335 return MergeNodesAction.mergeNodes(nodes, target);
336 }
337
338 return null; // undoRedo handling done in mergeNodes
339 }
340
341 @Override
342 public boolean isFixable(TestError testError) {
343 if (!(testError.getTester() instanceof DuplicateNode)) return false;
344 // never merge nodes with different tags.
345 if (testError.getCode() == DUPLICATE_NODE) return false;
346 // cannot merge nodes outside download area
347 final Iterator<? extends OsmPrimitive> it = testError.getPrimitives().iterator();
348 return it.hasNext() && !it.next().isOutsideDownloadArea();
349 // everything else is ok to merge
350 }
351}
Note: See TracBrowser for help on using the repository browser.