1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.data.validation.tests;
|
---|
3 |
|
---|
4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
5 |
|
---|
6 | import java.awt.geom.Area;
|
---|
7 | import java.util.ArrayList;
|
---|
8 | import java.util.Collection;
|
---|
9 | import java.util.Collections;
|
---|
10 | import java.util.HashSet;
|
---|
11 | import java.util.Iterator;
|
---|
12 | import java.util.LinkedList;
|
---|
13 | import java.util.List;
|
---|
14 | import java.util.Set;
|
---|
15 |
|
---|
16 | import org.openstreetmap.josm.command.ChangeCommand;
|
---|
17 | import org.openstreetmap.josm.command.Command;
|
---|
18 | import org.openstreetmap.josm.data.osm.Node;
|
---|
19 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
---|
20 | import org.openstreetmap.josm.data.osm.Way;
|
---|
21 | import org.openstreetmap.josm.data.validation.Severity;
|
---|
22 | import org.openstreetmap.josm.data.validation.Test;
|
---|
23 | import org.openstreetmap.josm.data.validation.TestError;
|
---|
24 | import org.openstreetmap.josm.gui.progress.ProgressMonitor;
|
---|
25 | import org.openstreetmap.josm.tools.Geometry;
|
---|
26 |
|
---|
27 | /**
|
---|
28 | * Check coastlines for errors
|
---|
29 | *
|
---|
30 | * @author frsantos
|
---|
31 | * @author Teemu Koskinen
|
---|
32 | * @author Gerd Petermann
|
---|
33 | */
|
---|
34 | public class Coastlines extends Test {
|
---|
35 |
|
---|
36 | protected static final int UNORDERED_COASTLINE = 901;
|
---|
37 | protected static final int REVERSED_COASTLINE = 902;
|
---|
38 | protected static final int UNCONNECTED_COASTLINE = 903;
|
---|
39 | protected static final int WRONG_ORDER_COASTLINE = 904;
|
---|
40 |
|
---|
41 | private List<Way> coastlineWays;
|
---|
42 |
|
---|
43 | /**
|
---|
44 | * Constructor
|
---|
45 | */
|
---|
46 | public Coastlines() {
|
---|
47 | super(tr("Coastlines"), tr("This test checks that coastlines are correct."));
|
---|
48 | }
|
---|
49 |
|
---|
50 | @Override
|
---|
51 | public void startTest(ProgressMonitor monitor) {
|
---|
52 | super.startTest(monitor);
|
---|
53 | coastlineWays = new LinkedList<>();
|
---|
54 | }
|
---|
55 |
|
---|
56 | @Override
|
---|
57 | public void endTest() {
|
---|
58 | checkConnections();
|
---|
59 | checkDirection();
|
---|
60 | coastlineWays = null;
|
---|
61 | super.endTest();
|
---|
62 | }
|
---|
63 |
|
---|
64 | /**
|
---|
65 | * Check connections between coastline ways.
|
---|
66 | * The nodes of a coastline way have to fullfil these rules:
|
---|
67 | * 1) the first node must be connected to the last node of a coastline way (which might be the same way)
|
---|
68 | * 2) the last node must be connected to the first node of a coastline way (which might be the same way)
|
---|
69 | * 3) all other nodes must not be connected to a coastline way
|
---|
70 | * 4) the number of referencing coastline ways must be 1 or 2
|
---|
71 | * Nodes outside the download area are special cases, we may not know enough about the connected ways.
|
---|
72 | */
|
---|
73 | private void checkConnections() {
|
---|
74 | Area downloadedArea = null;
|
---|
75 | for (Way w : coastlineWays) {
|
---|
76 | if (downloadedArea == null) {
|
---|
77 | downloadedArea = w.getDataSet().getDataSourceArea();
|
---|
78 | }
|
---|
79 | int numNodes = w.getNodesCount();
|
---|
80 | for (int i = 0; i < numNodes; i++) {
|
---|
81 | Node n = w.getNode(i);
|
---|
82 | List<OsmPrimitive> refs = n.getReferrers();
|
---|
83 | Set<Way> connectedWays = new HashSet<>();
|
---|
84 | for (OsmPrimitive p : refs) {
|
---|
85 | if (p != w && isCoastline(p)) {
|
---|
86 | connectedWays.add((Way) p);
|
---|
87 | }
|
---|
88 | }
|
---|
89 | if (i == 0) {
|
---|
90 | if (connectedWays.isEmpty() && n != w.lastNode() && n.getCoor().isIn(downloadedArea)) {
|
---|
91 | addError(UNCONNECTED_COASTLINE, w, null, n);
|
---|
92 | }
|
---|
93 | if (connectedWays.size() == 1 && n != connectedWays.iterator().next().lastNode()) {
|
---|
94 | checkIfReversed(w, connectedWays.iterator().next(), n);
|
---|
95 | }
|
---|
96 | if (connectedWays.size() == 1 && w.isClosed() && connectedWays.iterator().next().isClosed()) {
|
---|
97 | addError(UNORDERED_COASTLINE, w, connectedWays, n);
|
---|
98 | }
|
---|
99 | } 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 && "coastline".equals(osm.get("natural"));
|
---|
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 | }
|
---|