// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.validation.tests;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.geom.Area;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.openstreetmap.josm.command.ChangeCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
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.Geometry;
/**
* Check coastlines for errors
*
* @author frsantos
* @author Teemu Koskinen
* @author Gerd Petermann
*/
public class Coastlines extends Test {
protected static final int UNORDERED_COASTLINE = 901;
protected static final int REVERSED_COASTLINE = 902;
protected static final int UNCONNECTED_COASTLINE = 903;
protected static final int WRONG_ORDER_COASTLINE = 904;
private List<Way> coastlineWays;
/**
* Constructor
*/
public Coastlines() {
super(tr("Coastlines"), tr("This test checks that coastlines are correct."));
}
@Override
public void startTest(ProgressMonitor monitor) {
super.startTest(monitor);
coastlineWays = new LinkedList<>();
}
@Override
public void endTest() {
checkConnections();
checkDirection();
coastlineWays = null;
super.endTest();
}
/**
* Check connections between coastline ways.
* The nodes of a coastline way have to fullfil these rules:
* 1) the first node must be connected to the last node of a coastline way (which might be the same way)
* 2) the last node must be connected to the first node of a coastline way (which might be the same way)
* 3) all other nodes must not be connected to a coastline way
* 4) the number of referencing coastline ways must be 1 or 2
* Nodes outside the download area are special cases, we may not know enough about the connected ways.
*/
private void checkConnections() {
Area downloadedArea = null;
for (Way w : coastlineWays) {
if (downloadedArea == null) {
downloadedArea = w.getDataSet().getDataSourceArea();
}
int numNodes = w.getNodesCount();
for (int i = 0; i < numNodes; i++) {
Node n = w.getNode(i);
List<OsmPrimitive> refs = n.getReferrers();
Set<Way> connectedWays = new HashSet<>();
for (OsmPrimitive p : refs) {
if (p != w && isCoastline(p)) {
connectedWays.add((Way) p);
}
}
if (i == 0) {
if (connectedWays.isEmpty() && n != w.lastNode() && n.getCoor().isIn(downloadedArea)) {
addError(UNCONNECTED_COASTLINE, w, null, n);
}
if (connectedWays.size() == 1 && n != connectedWays.iterator().next().lastNode()) {
checkIfReversed(w, connectedWays.iterator().next(), n);
}
if (connectedWays.size() == 1 && w.isClosed() && connectedWays.iterator().next().isClosed()) {
addError(UNORDERED_COASTLINE, w, connectedWays, n);
}
} else if (i == numNodes - 1) {
if (connectedWays.isEmpty() && n != w.firstNode() && n.getCoor().isIn(downloadedArea)) {
addError(UNCONNECTED_COASTLINE, w, null, n);
}
if (connectedWays.size() == 1 && n != connectedWays.iterator().next().firstNode()) {
checkIfReversed(w, connectedWays.iterator().next(), n);
}
} else if (!connectedWays.isEmpty()) {
addError(UNORDERED_COASTLINE, w, connectedWays, n);
}
}
}
}
/**
* Check if two or more coastline ways form a closed clockwise way
*/
private void checkDirection() {
HashSet<Way> done = new HashSet<>();
for (Way w : coastlineWays) {
if (done.contains(w))
continue;
List<Way> visited = new ArrayList<>();
done.add(w);
visited.add(w);
List<Node> nodes = new ArrayList<>(w.getNodes());
Way curr = w;
while (nodes.get(0) != nodes.get(nodes.size()-1)) {
boolean foundContinuation = false;
for (OsmPrimitive p : curr.lastNode().getReferrers()) {
if (p != curr && isCoastline(p)) {
Way other = (Way) p;
if (done.contains(other))
continue;
if (other.firstNode() == curr.lastNode()) {
foundContinuation = true;
curr = other;
done.add(curr);
visited.add(curr);
nodes.remove(nodes.size()-1); // remove duplicate connection node
nodes.addAll(curr.getNodes());
break;
}
}
}
if (!foundContinuation)
break;
}
// simple closed ways are reported by WronglyOrderedWays
if (visited.size() > 1 && nodes.get(0) == nodes.get(nodes.size()-1) && Geometry.isClockwise(nodes)) {
errors.add(TestError.builder(this, Severity.WARNING, WRONG_ORDER_COASTLINE)
.message(tr("Reversed coastline: land not on left side"))
.primitives(visited)
.build());
}
}
}
/**
* Check if a reversed way would fit, if yes, add fixable "reversed" error, "unordered" else
* @param w way that might be reversed
* @param other other way that is connected to w
* @param n1 node at which w and other are connected
*/
private void checkIfReversed(Way w, Way other, Node n1) {
boolean headFixedWithReverse = false;
boolean tailFixedWithReverse = false;
int otherCoastlineWays = 0;
Way connectedToFirst = null;
for (int i = 0; i < 2; i++) {
Node n = (i == 0) ? w.firstNode() : w.lastNode();
List<OsmPrimitive> refs = n.getReferrers();
for (OsmPrimitive p : refs) {
if (p != w && isCoastline(p)) {
Way cw = (Way) p;
if (i == 0 && cw.firstNode() == n) {
headFixedWithReverse = true;
connectedToFirst = cw;
} else if (i == 1 && cw.lastNode() == n) {
if (cw != connectedToFirst)
tailFixedWithReverse = true;
} else
otherCoastlineWays++;
}
}
}
if (otherCoastlineWays == 0 && headFixedWithReverse && tailFixedWithReverse)
addError(REVERSED_COASTLINE, w, null, null);
else
addError(UNORDERED_COASTLINE, w, Collections.singletonList(other), n1);
}
/**
* Add error if not already done
* @param errCode the error code
* @param w the way that is in error
* @param otherWays collection of other ways in error or null
* @param n the node to be highlighted or null
*/
private void addError(int errCode, Way w, Collection<Way> otherWays, Node n) {
String msg;
switch (errCode) {
case UNCONNECTED_COASTLINE:
msg = tr("Unconnected coastline");
break;
case UNORDERED_COASTLINE:
msg = tr("Unordered coastline");
break;
case REVERSED_COASTLINE:
msg = tr("Reversed coastline");
break;
default:
msg = tr("invalid coastline"); // should not happen
}
Set<OsmPrimitive> primitives = new HashSet<>();
primitives.add(w);
if (otherWays != null)
primitives.addAll(otherWays);
// check for repeated error
for (TestError e : errors) {
if (e.getCode() != errCode)
continue;
if (errCode != REVERSED_COASTLINE && !e.getHighlighted().contains(n))
continue;
if (e.getPrimitives().size() != primitives.size())
continue;
if (!e.getPrimitives().containsAll(primitives))
continue;
return; // we already know this error
}
if (errCode != REVERSED_COASTLINE)
errors.add(TestError.builder(this, Severity.ERROR, errCode)
.message(msg)
.primitives(primitives)
.highlight(n)
.build());
else
errors.add(TestError.builder(this, Severity.ERROR, errCode)
.message(msg)
.primitives(primitives)
.build());
}
@Override
public void visit(Way way) {
if (!way.isUsable())
return;
if (isCoastline(way)) {
coastlineWays.add(way);
}
}
private static boolean isCoastline(OsmPrimitive osm) {
return osm instanceof Way && osm.hasTag("natural", "coastline");
}
@Override
public Command fixError(TestError testError) {
if (isFixable(testError)) {
// primitives list can be empty if all primitives have been purged
Iterator<? extends OsmPrimitive> it = testError.getPrimitives().iterator();
if (it.hasNext()) {
Way way = (Way) it.next();
Way newWay = new Way(way);
List<Node> nodesCopy = newWay.getNodes();
Collections.reverse(nodesCopy);
newWay.setNodes(nodesCopy);
return new ChangeCommand(way, newWay);
}
}
return null;
}
@Override
public boolean isFixable(TestError testError) {
if (testError.getTester() instanceof Coastlines)
return testError.getCode() == REVERSED_COASTLINE;
return false;
}
}