// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.validation.tests; import static org.openstreetmap.josm.tools.I18n.tr; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.MergeNodesAction; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.data.osm.AbstractPrimitive; import org.openstreetmap.josm.data.osm.Hash; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.OsmPrimitiveType; import org.openstreetmap.josm.data.osm.Storage; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.data.validation.Severity; import org.openstreetmap.josm.data.validation.Test; import org.openstreetmap.josm.data.validation.TestError; import org.openstreetmap.josm.gui.progress.ProgressMonitor; import org.openstreetmap.josm.tools.MultiMap; /** * Tests if there are duplicate nodes * * @author frsantos */ public class DuplicateNode extends Test { private static class NodeHash implements Hash<Object, Object> { private final double precision = Main.pref.getDouble("validator.duplicatenodes.precision", 0.); private LatLon roundCoord(LatLon coor) { return new LatLon( Math.round(coor.lat() / precision) * precision, Math.round(coor.lon() / precision) * precision ); } @SuppressWarnings("unchecked") private LatLon getLatLon(Object o) { if (o instanceof Node) { LatLon coor = ((Node) o).getCoor(); if (coor == null) return null; if (precision == 0) return coor.getRoundedToOsmPrecision(); return roundCoord(coor); } else if (o instanceof List<?>) { LatLon coor = ((List<Node>) o).get(0).getCoor(); if (coor == null) return null; if (precision == 0) return coor.getRoundedToOsmPrecision(); return roundCoord(coor); } else throw new AssertionError(); } @Override public boolean equals(Object k, Object t) { LatLon coorK = getLatLon(k); LatLon coorT = getLatLon(t); return coorK == coorT || (coorK != null && coorT != null && coorK.equals(coorT)); } @Override public int getHashCode(Object k) { LatLon coorK = getLatLon(k); return coorK == null ? 0 : coorK.hashCode(); } } protected static final int DUPLICATE_NODE = 1; protected static final int DUPLICATE_NODE_MIXED = 2; protected static final int DUPLICATE_NODE_OTHER = 3; protected static final int DUPLICATE_NODE_BUILDING = 10; protected static final int DUPLICATE_NODE_BOUNDARY = 11; protected static final int DUPLICATE_NODE_HIGHWAY = 12; protected static final int DUPLICATE_NODE_LANDUSE = 13; protected static final int DUPLICATE_NODE_NATURAL = 14; protected static final int DUPLICATE_NODE_POWER = 15; protected static final int DUPLICATE_NODE_RAILWAY = 16; protected static final int DUPLICATE_NODE_WATERWAY = 17; private static final String[] TYPES = { "none", "highway", "railway", "waterway", "boundary", "power", "natural", "landuse", "building"}; /** The map of potential duplicates. * * If there is exactly one node for a given pos, the map includes a pair <pos, Node>. * If there are multiple nodes for a given pos, the map includes a pair * <pos, NodesByEqualTagsMap> */ private Storage<Object> potentialDuplicates; /** * Constructor */ public DuplicateNode() { super(tr("Duplicated nodes"), tr("This test checks that there are no nodes at the very same location.")); } @Override public void startTest(ProgressMonitor monitor) { super.startTest(monitor); potentialDuplicates = new Storage<>(new NodeHash()); } @SuppressWarnings("unchecked") @Override public void endTest() { for (Object v: potentialDuplicates) { if (v instanceof Node) { // just one node at this position. Nothing to report as error continue; } // multiple nodes at the same position -> check if all nodes have a distinct elevation List<Node> nodes = (List<Node>) v; Set<String> eles = new HashSet<>(); for (Node n : nodes) { String ele = n.get("ele"); if (ele != null) { eles.add(ele); } } if (eles.size() == nodes.size()) { // All nodes at this position have a distinct elevation. // This is normal in some particular cases (for example, geodesic points in France) // Do not report this as an error continue; } // report errors errors.addAll(buildTestErrors(this, nodes)); } super.endTest(); potentialDuplicates = null; } /** * Returns the list of "duplicate nodes" errors for the given selection of node and parent test * @param parentTest The parent test of returned errors * @param nodes The nodes selction to look into * @return the list of "duplicate nodes" errors */ public List<TestError> buildTestErrors(Test parentTest, List<Node> nodes) { List<TestError> errors = new ArrayList<>(); MultiMap<Map<String, String>, OsmPrimitive> mm = new MultiMap<>(); for (Node n: nodes) { mm.put(n.getKeys(), n); } Map<String, Boolean> typeMap = new HashMap<>(); // check whether we have multiple nodes at the same position with the same tag set for (Iterator<Map<String, String>> it = mm.keySet().iterator(); it.hasNext();) { Set<OsmPrimitive> primitives = mm.get(it.next()); if (primitives.size() > 1) { for (String type: TYPES) { typeMap.put(type, Boolean.FALSE); } for (OsmPrimitive p : primitives) { if (p.getType() == OsmPrimitiveType.NODE) { Node n = (Node) p; List<OsmPrimitive> lp = n.getReferrers(); for (OsmPrimitive sp: lp) { if (sp.getType() == OsmPrimitiveType.WAY) { boolean typed = false; Way w = (Way) sp; Map<String, String> keys = w.getKeys(); for (String type: typeMap.keySet()) { if (keys.containsKey(type)) { typeMap.put(type, Boolean.TRUE); typed = true; } } if (!typed) { typeMap.put("none", Boolean.TRUE); } } } } } long nbType = typeMap.entrySet().stream().filter(Entry::getValue).count(); if (nbType > 1) { errors.add(TestError.builder(parentTest, Severity.WARNING, DUPLICATE_NODE_MIXED) .message(tr("Mixed type duplicated nodes")) .primitives(primitives) .build()); } else if (typeMap.get("highway")) { errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_HIGHWAY) .message(tr("Highway duplicated nodes")) .primitives(primitives) .build()); } else if (typeMap.get("railway")) { errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_RAILWAY) .message(tr("Railway duplicated nodes")) .primitives(primitives) .build()); } else if (typeMap.get("waterway")) { errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_WATERWAY) .message(tr("Waterway duplicated nodes")) .primitives(primitives) .build()); } else if (typeMap.get("boundary")) { errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_BOUNDARY) .message(tr("Boundary duplicated nodes")) .primitives(primitives) .build()); } else if (typeMap.get("power")) { errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_POWER) .message(tr("Power duplicated nodes")) .primitives(primitives) .build()); } else if (typeMap.get("natural")) { errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_NATURAL) .message(tr("Natural duplicated nodes")) .primitives(primitives) .build()); } else if (typeMap.get("building")) { errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_BUILDING) .message(tr("Building duplicated nodes")) .primitives(primitives) .build()); } else if (typeMap.get("landuse")) { errors.add(TestError.builder(parentTest, Severity.ERROR, DUPLICATE_NODE_LANDUSE) .message(tr("Landuse duplicated nodes")) .primitives(primitives) .build()); } else { errors.add(TestError.builder(parentTest, Severity.WARNING, DUPLICATE_NODE_OTHER) .message(tr("Other duplicated nodes")) .primitives(primitives) .build()); } it.remove(); } } // check whether we have multiple nodes at the same position with differing tag sets if (!mm.isEmpty()) { List<OsmPrimitive> duplicates = new ArrayList<>(); for (Set<OsmPrimitive> l: mm.values()) { duplicates.addAll(l); } if (duplicates.size() > 1) { errors.add(TestError.builder(parentTest, Severity.WARNING, DUPLICATE_NODE) .message(tr("Nodes at same position")) .primitives(duplicates) .build()); } } return errors; } @SuppressWarnings("unchecked") @Override public void visit(Node n) { if (n.isUsable()) { if (potentialDuplicates.get(n) == null) { // in most cases there is just one node at a given position. We // avoid to create an extra object and add remember the node // itself at this position potentialDuplicates.put(n); } else if (potentialDuplicates.get(n) instanceof Node) { // we have an additional node at the same position. Create an extra // object to keep track of the nodes at this position. // Node n1 = (Node) potentialDuplicates.get(n); List<Node> nodes = new ArrayList<>(2); nodes.add(n1); nodes.add(n); potentialDuplicates.put(nodes); } else if (potentialDuplicates.get(n) instanceof List<?>) { // we have multiple nodes at the same position. // List<Node> nodes = (List<Node>) potentialDuplicates.get(n); nodes.add(n); } } } /** * Merge the nodes into one. * Copied from UtilsPlugin.MergePointsAction */ @Override public Command fixError(TestError testError) { Collection<OsmPrimitive> sel = new LinkedList<>(testError.getPrimitives()); Set<Node> nodes = new LinkedHashSet<>(OsmPrimitive.getFilteredList(sel, Node.class)); // Filter nodes that have already been deleted (see #5764 and #5773) nodes.removeIf(AbstractPrimitive::isDeleted); // Merge only if at least 2 nodes remain if (nodes.size() >= 2) { // Use first existing node or first node if all nodes are new Node target = null; for (Node n: nodes) { if (!n.isNew()) { target = n; break; } } if (target == null) { target = nodes.iterator().next(); } if (Command.checkOutlyingOrIncompleteOperation(nodes, Collections.singleton(target)) == Command.IS_OK) return MergeNodesAction.mergeNodes(Main.getLayerManager().getEditLayer(), nodes, target); } return null; // undoRedo handling done in mergeNodes } @Override public boolean isFixable(TestError testError) { if (!(testError.getTester() instanceof DuplicateNode)) return false; // never merge nodes with different tags. if (testError.getCode() == DUPLICATE_NODE) return false; // cannot merge nodes outside download area final Iterator<? extends OsmPrimitive> it = testError.getPrimitives().iterator(); if (!it.hasNext() || it.next().isOutsideDownloadArea()) return false; // everything else is ok to merge return true; } }