// 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.List;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
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;
/**
* Checks if turnrestrictions are valid
* @since 3669
*/
public class TurnrestrictionTest extends Test {
protected static final int NO_VIA = 1801;
protected static final int NO_FROM = 1802;
protected static final int NO_TO = 1803;
protected static final int MORE_VIA = 1804;
protected static final int MORE_FROM = 1805;
protected static final int MORE_TO = 1806;
protected static final int UNKNOWN_ROLE = 1807;
protected static final int UNKNOWN_TYPE = 1808;
protected static final int FROM_VIA_NODE = 1809;
protected static final int TO_VIA_NODE = 1810;
protected static final int FROM_VIA_WAY = 1811;
protected static final int TO_VIA_WAY = 1812;
protected static final int MIX_VIA = 1813;
protected static final int UNCONNECTED_VIA = 1814;
protected static final int SUPERFLUOUS = 1815;
protected static final int FROM_EQUALS_TO = 1816;
/**
* Constructs a new {@code TurnrestrictionTest}.
*/
public TurnrestrictionTest() {
super(tr("Turnrestrictions"), tr("This test checks if turnrestrictions are valid."));
}
@Override
public void visit(Relation r) {
if (!r.hasTag("type", "restriction"))
return;
Way fromWay = null;
Way toWay = null;
List<OsmPrimitive> via = new ArrayList<>();
boolean morefrom = false;
boolean moreto = false;
boolean morevia = false;
boolean mixvia = false;
/* find the "from", "via" and "to" elements */
for (RelationMember m : r.getMembers()) {
if (m.getMember().isIncomplete())
return;
List<OsmPrimitive> l = new ArrayList<>();
l.add(r);
l.add(m.getMember());
if (m.isWay()) {
Way w = m.getWay();
if (w.getNodesCount() < 2) {
continue;
}
switch (m.getRole()) {
case "from":
if (fromWay != null) {
morefrom = true;
} else {
fromWay = w;
}
break;
case "to":
if (toWay != null) {
moreto = true;
} else {
toWay = w;
}
break;
case "via":
if (!via.isEmpty() && via.get(0) instanceof Node) {
mixvia = true;
} else {
via.add(w);
}
break;
default:
errors.add(TestError.builder(this, Severity.WARNING, UNKNOWN_ROLE)
.message(tr("Unknown role"))
.primitives(l)
.highlight(m.getMember())
.build());
}
} else if (m.isNode()) {
Node n = m.getNode();
if ("via".equals(m.getRole())) {
if (!via.isEmpty()) {
if (via.get(0) instanceof Node) {
morevia = true;
} else {
mixvia = true;
}
} else {
via.add(n);
}
} else {
errors.add(TestError.builder(this, Severity.WARNING, UNKNOWN_ROLE)
.message(tr("Unknown role"))
.primitives(l)
.highlight(m.getMember())
.build());
}
} else {
errors.add(TestError.builder(this, Severity.WARNING, UNKNOWN_TYPE)
.message(tr("Unknown member type"))
.primitives(l)
.highlight(m.getMember())
.build());
}
}
if (morefrom) {
errors.add(TestError.builder(this, Severity.ERROR, MORE_FROM)
.message(tr("More than one \"from\" way found"))
.primitives(r)
.build());
}
if (moreto) {
errors.add(TestError.builder(this, Severity.ERROR, MORE_TO)
.message(tr("More than one \"to\" way found"))
.primitives(r)
.build());
}
if (morevia) {
errors.add(TestError.builder(this, Severity.ERROR, MORE_VIA)
.message(tr("More than one \"via\" node found"))
.primitives(r)
.build());
}
if (mixvia) {
errors.add(TestError.builder(this, Severity.ERROR, MIX_VIA)
.message(tr("Cannot mix node and way for role \"via\""))
.primitives(r)
.build());
}
if (fromWay == null) {
errors.add(TestError.builder(this, Severity.ERROR, NO_FROM)
.message(tr("No \"from\" way found"))
.primitives(r)
.build());
return;
}
if (toWay == null) {
errors.add(TestError.builder(this, Severity.ERROR, NO_TO)
.message(tr("No \"to\" way found"))
.primitives(r)
.build());
return;
}
if (fromWay.equals(toWay)) {
Severity severity = r.hasTag("restriction", "no_u_turn") ? Severity.OTHER : Severity.WARNING;
errors.add(TestError.builder(this, severity, FROM_EQUALS_TO)
.message(tr("\"from\" way equals \"to\" way"))
.primitives(r)
.build());
}
if (via.isEmpty()) {
errors.add(TestError.builder(this, Severity.ERROR, NO_VIA)
.message(tr("No \"via\" node or way found"))
.primitives(r)
.build());
return;
}
if (via.get(0) instanceof Node) {
final Node viaNode = (Node) via.get(0);
final Way viaPseudoWay = new Way();
viaPseudoWay.addNode(viaNode);
checkIfConnected(fromWay, viaPseudoWay,
tr("The \"from\" way does not start or end at a \"via\" node."), FROM_VIA_NODE);
if (toWay.isOneway() != 0 && viaNode.equals(toWay.lastNode(true))) {
errors.add(TestError.builder(this, Severity.WARNING, SUPERFLUOUS)
.message(tr("Superfluous turnrestriction as \"to\" way is oneway"))
.primitives(r)
.build());
return;
}
checkIfConnected(viaPseudoWay, toWay,
tr("The \"to\" way does not start or end at a \"via\" node."), TO_VIA_NODE);
} else {
// check if consecutive ways are connected: from/via[0], via[i-1]/via[i], via[last]/to
checkIfConnected(fromWay, (Way) via.get(0),
tr("The \"from\" and the first \"via\" way are not connected."), FROM_VIA_WAY);
if (via.size() > 1) {
for (int i = 1; i < via.size(); i++) {
Way previous = (Way) via.get(i - 1);
Way current = (Way) via.get(i);
checkIfConnected(previous, current,
tr("The \"via\" ways are not connected."), UNCONNECTED_VIA);
}
}
if (toWay.isOneway() != 0 && ((Way) via.get(via.size() - 1)).isFirstLastNode(toWay.lastNode(true))) {
errors.add(TestError.builder(this, Severity.WARNING, SUPERFLUOUS)
.message(tr("Superfluous turnrestriction as \"to\" way is oneway"))
.primitives(r)
.build());
return;
}
checkIfConnected((Way) via.get(via.size() - 1), toWay,
tr("The last \"via\" and the \"to\" way are not connected."), TO_VIA_WAY);
}
}
private static boolean isFullOneway(Way w) {
return w.isOneway() != 0 && !w.hasTag("oneway:bicycle", "no");
}
private void checkIfConnected(Way previous, Way current, String msg, int code) {
boolean c;
if (isFullOneway(previous) && isFullOneway(current)) {
// both oneways: end/start node must be equal
c = previous.lastNode(true).equals(current.firstNode(true));
} else if (isFullOneway(previous)) {
// previous way is oneway: end of previous must be start/end of current
c = current.isFirstLastNode(previous.lastNode(true));
} else if (isFullOneway(current)) {
// current way is oneway: start of current must be start/end of previous
c = previous.isFirstLastNode(current.firstNode(true));
} else {
// otherwise: start/end of previous must be start/end of current
c = current.isFirstLastNode(previous.firstNode()) || current.isFirstLastNode(previous.lastNode());
}
if (!c) {
errors.add(TestError.builder(this, Severity.ERROR, code)
.message(msg)
.primitives(previous, current)
.build());
}
}
}