001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.validation.tests; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.geom.Area; 007import java.util.ArrayList; 008import java.util.Collection; 009import java.util.Collections; 010import java.util.HashSet; 011import java.util.Iterator; 012import java.util.LinkedList; 013import java.util.List; 014import java.util.Set; 015 016import org.openstreetmap.josm.command.ChangeCommand; 017import org.openstreetmap.josm.command.Command; 018import org.openstreetmap.josm.data.osm.Node; 019import org.openstreetmap.josm.data.osm.OsmPrimitive; 020import org.openstreetmap.josm.data.osm.Way; 021import org.openstreetmap.josm.data.validation.Severity; 022import org.openstreetmap.josm.data.validation.Test; 023import org.openstreetmap.josm.data.validation.TestError; 024import org.openstreetmap.josm.gui.progress.ProgressMonitor; 025import org.openstreetmap.josm.tools.Geometry; 026 027/** 028 * Check coastlines for errors 029 * 030 * @author frsantos 031 * @author Teemu Koskinen 032 * @author Gerd Petermann 033 */ 034public class Coastlines extends Test { 035 036 protected static final int UNORDERED_COASTLINE = 901; 037 protected static final int REVERSED_COASTLINE = 902; 038 protected static final int UNCONNECTED_COASTLINE = 903; 039 protected static final int WRONG_ORDER_COASTLINE = 904; 040 041 private List<Way> coastlineWays; 042 043 /** 044 * Constructor 045 */ 046 public Coastlines() { 047 super(tr("Coastlines"), tr("This test checks that coastlines are correct.")); 048 } 049 050 @Override 051 public void startTest(ProgressMonitor monitor) { 052 super.startTest(monitor); 053 coastlineWays = new LinkedList<>(); 054 } 055 056 @Override 057 public void endTest() { 058 checkConnections(); 059 checkDirection(); 060 coastlineWays = null; 061 super.endTest(); 062 } 063 064 /** 065 * Check connections between coastline ways. 066 * The nodes of a coastline way have to fulfil these rules: 067 * 1) the first node must be connected to the last node of a coastline way (which might be the same way) 068 * 2) the last node must be connected to the first node of a coastline way (which might be the same way) 069 * 3) all other nodes must not be connected to a coastline way 070 * 4) the number of referencing coastline ways must be 1 or 2 071 * Nodes outside the download area are special cases, we may not know enough about the connected ways. 072 */ 073 private void checkConnections() { 074 Area downloadedArea = null; 075 for (Way w : coastlineWays) { 076 if (downloadedArea == null) { 077 downloadedArea = w.getDataSet().getDataSourceArea(); 078 } 079 int numNodes = w.getNodesCount(); 080 for (int i = 0; i < numNodes; i++) { 081 Node n = w.getNode(i); 082 List<OsmPrimitive> refs = n.getReferrers(); 083 Set<Way> connectedWays = new HashSet<>(); 084 for (OsmPrimitive p : refs) { 085 if (p != w && isCoastline(p)) { 086 connectedWays.add((Way) p); 087 } 088 } 089 if (i == 0) { 090 if (connectedWays.isEmpty() && n != w.lastNode() && n.getCoor().isIn(downloadedArea)) { 091 addError(UNCONNECTED_COASTLINE, w, null, n); 092 } 093 if (connectedWays.size() == 1 && n != connectedWays.iterator().next().lastNode()) { 094 checkIfReversed(w, connectedWays.iterator().next(), n); 095 } 096 if (connectedWays.size() == 1 && w.isClosed() && connectedWays.iterator().next().isClosed()) { 097 addError(UNORDERED_COASTLINE, w, connectedWays, n); 098 } 099 } else if (i == numNodes - 1) { 100 if (connectedWays.isEmpty() && n != w.firstNode() && n.getCoor().isIn(downloadedArea)) { 101 addError(UNCONNECTED_COASTLINE, w, null, n); 102 } 103 if (connectedWays.size() == 1 && n != connectedWays.iterator().next().firstNode()) { 104 checkIfReversed(w, connectedWays.iterator().next(), n); 105 } 106 } else if (!connectedWays.isEmpty()) { 107 addError(UNORDERED_COASTLINE, w, connectedWays, n); 108 } 109 } 110 } 111 } 112 113 /** 114 * Check if two or more coastline ways form a closed clockwise way 115 */ 116 private void checkDirection() { 117 HashSet<Way> done = new HashSet<>(); 118 for (Way w : coastlineWays) { 119 if (done.contains(w)) 120 continue; 121 List<Way> visited = new ArrayList<>(); 122 done.add(w); 123 visited.add(w); 124 List<Node> nodes = new ArrayList<>(w.getNodes()); 125 Way curr = w; 126 while (nodes.get(0) != nodes.get(nodes.size()-1)) { 127 boolean foundContinuation = false; 128 for (OsmPrimitive p : curr.lastNode().getReferrers()) { 129 if (p != curr && isCoastline(p)) { 130 Way other = (Way) p; 131 if (done.contains(other)) 132 continue; 133 if (other.firstNode() == curr.lastNode()) { 134 foundContinuation = true; 135 curr = other; 136 done.add(curr); 137 visited.add(curr); 138 nodes.remove(nodes.size()-1); // remove duplicate connection node 139 nodes.addAll(curr.getNodes()); 140 break; 141 } 142 } 143 } 144 if (!foundContinuation) 145 break; 146 } 147 // simple closed ways are reported by WronglyOrderedWays 148 if (visited.size() > 1 && nodes.get(0) == nodes.get(nodes.size()-1) && Geometry.isClockwise(nodes)) { 149 errors.add(TestError.builder(this, Severity.WARNING, WRONG_ORDER_COASTLINE) 150 .message(tr("Reversed coastline: land not on left side")) 151 .primitives(visited) 152 .build()); 153 } 154 } 155 } 156 157 /** 158 * Check if a reversed way would fit, if yes, add fixable "reversed" error, "unordered" else 159 * @param w way that might be reversed 160 * @param other other way that is connected to w 161 * @param n1 node at which w and other are connected 162 */ 163 private void checkIfReversed(Way w, Way other, Node n1) { 164 boolean headFixedWithReverse = false; 165 boolean tailFixedWithReverse = false; 166 int otherCoastlineWays = 0; 167 Way connectedToFirst = null; 168 for (int i = 0; i < 2; i++) { 169 Node n = (i == 0) ? w.firstNode() : w.lastNode(); 170 List<OsmPrimitive> refs = n.getReferrers(); 171 for (OsmPrimitive p : refs) { 172 if (p != w && isCoastline(p)) { 173 Way cw = (Way) p; 174 if (i == 0 && cw.firstNode() == n) { 175 headFixedWithReverse = true; 176 connectedToFirst = cw; 177 } else if (i == 1 && cw.lastNode() == n) { 178 if (cw != connectedToFirst) 179 tailFixedWithReverse = true; 180 } else 181 otherCoastlineWays++; 182 } 183 } 184 } 185 if (otherCoastlineWays == 0 && headFixedWithReverse && tailFixedWithReverse) 186 addError(REVERSED_COASTLINE, w, null, null); 187 else 188 addError(UNORDERED_COASTLINE, w, Collections.singletonList(other), n1); 189 } 190 191 /** 192 * Add error if not already done 193 * @param errCode the error code 194 * @param w the way that is in error 195 * @param otherWays collection of other ways in error or null 196 * @param n the node to be highlighted or null 197 */ 198 private void addError(int errCode, Way w, Collection<Way> otherWays, Node n) { 199 String msg; 200 switch (errCode) { 201 case UNCONNECTED_COASTLINE: 202 msg = tr("Unconnected coastline"); 203 break; 204 case UNORDERED_COASTLINE: 205 msg = tr("Unordered coastline"); 206 break; 207 case REVERSED_COASTLINE: 208 msg = tr("Reversed coastline"); 209 break; 210 default: 211 msg = tr("invalid coastline"); // should not happen 212 } 213 Set<OsmPrimitive> primitives = new HashSet<>(); 214 primitives.add(w); 215 if (otherWays != null) 216 primitives.addAll(otherWays); 217 // check for repeated error 218 for (TestError e : errors) { 219 if (e.getCode() != errCode) 220 continue; 221 if (errCode != REVERSED_COASTLINE && !e.getHighlighted().contains(n)) 222 continue; 223 if (e.getPrimitives().size() != primitives.size()) 224 continue; 225 if (!e.getPrimitives().containsAll(primitives)) 226 continue; 227 return; // we already know this error 228 } 229 if (errCode != REVERSED_COASTLINE) 230 errors.add(TestError.builder(this, Severity.ERROR, errCode) 231 .message(msg) 232 .primitives(primitives) 233 .highlight(n) 234 .build()); 235 else 236 errors.add(TestError.builder(this, Severity.ERROR, errCode) 237 .message(msg) 238 .primitives(primitives) 239 .build()); 240 } 241 242 @Override 243 public void visit(Way way) { 244 if (!way.isUsable()) 245 return; 246 247 if (isCoastline(way)) { 248 coastlineWays.add(way); 249 } 250 } 251 252 private static boolean isCoastline(OsmPrimitive osm) { 253 return osm instanceof Way && osm.hasTag("natural", "coastline"); 254 } 255 256 @Override 257 public Command fixError(TestError testError) { 258 if (isFixable(testError)) { 259 // primitives list can be empty if all primitives have been purged 260 Iterator<? extends OsmPrimitive> it = testError.getPrimitives().iterator(); 261 if (it.hasNext()) { 262 Way way = (Way) it.next(); 263 Way newWay = new Way(way); 264 265 List<Node> nodesCopy = newWay.getNodes(); 266 Collections.reverse(nodesCopy); 267 newWay.setNodes(nodesCopy); 268 269 return new ChangeCommand(way, newWay); 270 } 271 } 272 return null; 273 } 274 275 @Override 276 public boolean isFixable(TestError testError) { 277 if (testError.getTester() instanceof Coastlines) 278 return testError.getCode() == REVERSED_COASTLINE; 279 280 return false; 281 } 282}