// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.turnrestrictions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
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.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.plugins.turnrestrictions.editor.TurnRestrictionLegRole;
import org.openstreetmap.josm.plugins.turnrestrictions.editor.TurnRestrictionType;
import org.openstreetmap.josm.tools.CheckParameterUtil;
/**
* TurnRestrictionBuilder creates a turn restriction and initializes it with
* objects from a selection of OSM primitives, i.e. the current selection
* in a {@link OsmDataLayer}.
*
*/
public class TurnRestrictionBuilder {
/**
* Replies the angle phi in the polar coordinates (r,phi) representing the first
* segment of the way {@code w}, where w is moved such that the start node of {@code w} is
* in the origin (0, 0).
*
* @param w the way. Must not be null. At least two nodes required.
* @return phi in the polar coordinates
* @throws IllegalArgumentException thrown if w is null
* @throws IllegalArgumentException thrown if w is too short (at least two nodes required)
*/
public static double phi(Way w) throws IllegalArgumentException {
return phi(w, false /* not inverse */);
}
/**
* <p>Replies the angle phi in the polar coordinates (r,phi) representing the first
* segment of the way {@code w}, where w is moved such that the start node of {@code w} is
* in the origin (0, 0).</p>
*
* <p>If {@code doInvert} is true, computes phi for the way in reversed direction.</p>
*
* @param w the way. Must not be null. At least two nodes required.
* @param doInvert if true, computes phi for the reversed way
* @return phi in the polar coordinates
* @throws IllegalArgumentException thrown if w is null
* @throws IllegalArgumentException thrown if w is too short (at least two nodes required)
*/
public static double phi(Way w, boolean doInvert) throws IllegalArgumentException {
CheckParameterUtil.ensureParameterNotNull(w, "w");
if (w.getNodesCount() < 2) {
throw new IllegalArgumentException("can't compute phi for way with less than 2 nodes");
}
List<Node> nodes = w.getNodes();
if (doInvert) Collections.reverse(nodes);
Node n0 = nodes.get(0);
Node n1 = nodes.get(1);
double x = n1.getCoor().getX() - n0.getCoor().getX();
double y = n1.getCoor().getY() - n0.getCoor().getY();
return Math.atan2(y, x);
}
/**
* Replies the unique common node of two ways, or null, if either no
* such node or multiple common nodes exist.
*
* @param w1 the first way
* @param w2 the second way
* @return the common node or null, if w1 is null, or if w2 is null or if
* w1 and w2 don't share exactly one node
*/
public static Node getUniqueCommonNode(Way w1, Way w2) throws IllegalArgumentException {
Set<Node> w1Nodes = new HashSet<>(w1.getNodes());
w1Nodes.retainAll(w2.getNodes());
if (w1Nodes.size() != 1) return null;
return w1Nodes.iterator().next();
}
/**
* Replies true, if {@code n} is the start node of the way {@code w}.
*
* @param w the way. Must not be null.
* @param n the node. Must not be null.
* @return true, if {@code n} is the start node of the way {@code w}.
*/
public static boolean isStartNode(Way w, Node n) {
if (w.getNodesCount() == 0) return false;
return w.getNode(0).equals(n);
}
/**
* Replies true, if {@code n} is the end node of the way {@code w}.
*
* @param w the way. Must not be null.
* @param n the node. Must not be null.
* @return true, if {@code n} is the end node of the way {@code w}.
*/
public static boolean isEndNode(Way w, Node n) {
if (w.getNodesCount() == 0) return false;
return w.getNode(w.getNodesCount()-1).equals(n);
}
/**
* Replies true, if {@code n} is a node in the way {@code w} but {@code n}
* is neither the start nor the end node.
*
* @param w the way
* @param n the node
* @return true if {@code n} is an "inner" node
*/
public static boolean isInnerNode(Way w, Node n) {
if (!w.getNodes().contains(n)) return false;
if (isStartNode(w, n)) return false;
if (isEndNode(w, n)) return false;
return true;
}
/**
* <p>Replies the angle at which way {@code from} and {@code to} are connected
* at exactly one common node.</p>
*
* <p>If the result is positive, the way {@code from} bends to the right, if it
* is negative, the {@code to} bends to the left.</p>
*
* <p>The two ways must not be null and they must be connected at exactly one
* common node. They must <strong>not intersect</code> at this node.</p>.
*
* @param from the from way
* @param to the to way
* @return the intersection angle
* @throws IllegalArgumentException thrown if the two nodes don't have exactly one common
* node at which they are connected
*
*/
public static double intersectionAngle(Way from, Way to) throws IllegalArgumentException {
Node via = getUniqueCommonNode(from, to);
if (via == null)
throw new IllegalArgumentException("the two ways must share exactly one common node"); // no I18n required
if (!isStartNode(from, via) && !isEndNode(from, via))
throw new IllegalArgumentException("via node must be start or end node of from-way"); // no I18n required
if (!isStartNode(to, via) && !isEndNode(to, via))
throw new IllegalArgumentException("via node must be start or end node of to-way"); // no I18n required
double phi1 = phi(from, isStartNode(from, via));
double phi2 = phi(to, isEndNode(to, via));
return phi1 - phi2;
}
public enum RelativeWayJoinOrientation {
LEFT,
RIGHT
}
/**
* <p>Determines the orientation in which two ways {@code from} and {@code to}
* are connected, with respect to the direction of the way {@code from}.</p>
*
* <p>The following preconditions must be met:
* <ul>
* <li>{@code from} and {@code to} must not be null</li>
* <li>they must have exactly one common node <em>n</em> </li>
* <li><em>n</em> must occur exactly once in {@code from} and {@code to}, i.e. the
* two ways must not be closed at <em>n</em></li>
* <li><em>n</em> must be the start or the end node of both ways </li>
* </ul>
* </p>
*
* <p>Here's a typical configuration:</p>
* <pre>
* to1 to2
* -------------> o -------------->
* ^
* | from
* |
* </pre>
*
* <p>Replies null, if the preconditions aren't met and the method fails to
* determine the join orientation.</p>
*
* @param from the "from"-way
* @param to the "to"-way
* @return the join orientation or null, if the method fails to compute the
* join orientation
*/
public static RelativeWayJoinOrientation determineWayJoinOrientation(Way from, Way to) {
Node via = getUniqueCommonNode(from, to);
if (via == null) return null;
if (!isConnectingNode(from, to, via)) return null;
// if either w1 or w2 are closed at the via node, we can't determine automatically
// whether the connection at "via" is a "left turn" or a "right turn"
if (isClosedAt(from, via)) return null;
if (isClosedAt(to, via)) return null;
double phi = intersectionAngle(from, to);
if (phi >= 0 && phi <= Math.PI) {
return RelativeWayJoinOrientation.RIGHT;
} else {
return RelativeWayJoinOrientation.LEFT;
}
}
/**
* <p>Selects either of the two ways resulting from the split of a way
* in the role {@link TurnRestrictionLegRole#TO TO}.</p>
*
* <p>This methods operates on three ways for which the following
* preconditions must be met:
* <ul>
* <li>{@code t1} and {@code t2} are connected at a common node <em>n</em></li>
* <li>{@code from} is also connected to the node <em>n</em>. <em>n</em> occurs
* exactly once in {@code from} and is either the start or the end node of {@code from}.</li>
* </ul>
* </p>
*
* <p>Here's a typical configuration:</p>
* <pre>
* to1 to2
* -------------> o -------------->
* ^
* | from
* |
* </pre>
*
* <p>Depending on {@code restrictionType}, this method either returns {@code to1}
* or {@code to2}. If {@code restrictionType} indicates that our context is a
* "left turn", {@code to1} is replied. If our context is a "right turn", {@code to2}
* is returned.</p>
*
* <p>Replies null, if the expected preconditions aren't met or if we can't infer
* from {@code restrictionType} whether our context is a "left turn" or a "right turn".</p>
*
* @param from the from-way
* @param to1 the first part of the split to-way
* @param to2 the second part of the split to-way
* @param restrictionType the restriction type
* @return either {@code to1}, {@code to2}, or {@code null}.
*/
public static Way selectToWayAfterSplit(Way from, Way to1, Way to2, TurnRestrictionType restrictionType) {
if (restrictionType == null) return null;
Node cn1 = TurnRestrictionBuilder.getUniqueCommonNode(from, to1);
if (cn1 == null) return null;
Node cn2 = TurnRestrictionBuilder.getUniqueCommonNode(from, to2);
if (cn2 == null) return null;
if (cn1 != cn2) return null;
if (!isStartNode(from, cn1) && !isEndNode(from, cn1)) {
/*
* the now split to-way still *intersects* the from-way. We
* can't adjust the split decisions.
*/
return null;
}
RelativeWayJoinOrientation o1 = determineWayJoinOrientation(from, to1);
RelativeWayJoinOrientation o2 = determineWayJoinOrientation(from, to2);
switch(restrictionType) {
case NO_LEFT_TURN:
case ONLY_LEFT_TURN:
if (RelativeWayJoinOrientation.LEFT.equals(o1)) return to1;
else if (RelativeWayJoinOrientation.LEFT.equals(o2)) return to2;
else return null;
case NO_RIGHT_TURN:
case ONLY_RIGHT_TURN:
if (RelativeWayJoinOrientation.RIGHT.equals(o1)) return to1;
else if (RelativeWayJoinOrientation.RIGHT.equals(o2)) return to2;
else return null;
default:
/*
* For restriction types like NO_U_TURN, NO_STRAIGHT_ON, etc. we
* can select a "left" or "right" way after splitting.
*/
return null;
}
}
public TurnRestrictionBuilder() {
}
/**
* Creates and initializes a new turn restriction based on the primitives
* currently selected in layer {@code layer}.
*
* @param layer the layer. Must not be null.
* @return the new initialized turn restriction. The turn restriction isn't
* added to the layer yet.
* @throws IllegalArgumentException thrown if layer is null
*/
public synchronized Relation buildFromSelection(OsmDataLayer layer) {
CheckParameterUtil.ensureParameterNotNull(layer, "layer");
List<OsmPrimitive> selection = new ArrayList<>(layer.data.getSelected());
return build(selection);
}
/**
* Tries to initialize a No-U-Turn restriction from the primitives in
* <code>primitives</code>. If successful, replies true, otherwise false.
*
* @param primitives the primitives
* @return true, if we can propose a U-turn restriction for the primitives
* in <code>primitives</code>
*/
protected Relation initNoUTurnRestriction(List<OsmPrimitive> primitives) {
if (primitives.size() != 2) return null;
// we need exactly one node and one way in the selection ...
List<Node> nodes = OsmPrimitive.getFilteredList(primitives, Node.class);
List<Way> ways = OsmPrimitive.getFilteredList(primitives, Way.class);
if (nodes.size() != 1 || ways.size() != 1) return null;
// .. and the node has to be the start or the node of the way
Way way = ways.get(0);
Node node = nodes.get(0);
List<Node> wayNodes = way.getNodes();
if (wayNodes.size() < 2) return null; // shouldn't happen - just in case
if (!(wayNodes.get(0).equals(node) || wayNodes.get(wayNodes.size()-1).equals(node))) return null;
Relation tr = new Relation();
tr.put("type", "restriction");
tr.addMember(new RelationMember("from", way));
tr.addMember(new RelationMember("to", way));
tr.addMember(new RelationMember("via", node));
tr.put("restriction", TurnRestrictionType.NO_U_TURN.getTagValue());
return tr;
}
/**
* <p>Replies true, if the ways {@code w1} and {@code w2} are connected
* at the node {@code n}.</p>
*
* <p>If {@code w1} and {@code w2} <em>intersect</em> at the node {@code n},
* this method replies false.</p>
*
* @param w1 the first way
* @param w2 the second way
* @param n the node
*/
public static boolean isConnectingNode(Way w1, Way w2, Node n) {
if (isStartNode(w1, n)) {
return isStartNode(w2, n) | isEndNode(w2, n);
} else if (isEndNode(w1, n)) {
return isStartNode(w2, n) | isEndNode(w2, n);
}
return false;
}
/**
* Replies true, if the way {@code w} is closed at the node {@code n}.
*
* @param w the way
* @param n the node
* @return true, if the way {@code w} is closed at the node {@code n}.
*/
public static boolean isClosedAt(Way w, Node n) {
List<Node> nodes = w.getNodes();
nodes.retainAll(Collections.singletonList(n));
return nodes.size() >= 2;
}
protected Relation initTurnRestrictionFromTwoWays(List<OsmPrimitive> primitives) {
Way w1 = null;
Way w2 = null;
Node via = null;
if (primitives.size() == 2) {
// if we have exactly two selected primitives, we expect two ways.
// See initNoUTurnRestriction() for the case where we have a selected way
// and a selected node
List<Way> selWays = OsmPrimitive.getFilteredList(primitives, Way.class);
if (selWays.size() != 2) return null;
w1 = selWays.get(0);
w2 = selWays.get(1);
via = getUniqueCommonNode(w1, w2);
} else if (primitives.size() == 3) {
// if we have exactly three selected primitives, we need two ways and a
// node, which should be an acceptable via node
List<Way> selWays = OsmPrimitive.getFilteredList(primitives, Way.class);
List<Node> selNodes = OsmPrimitive.getFilteredList(primitives, Node.class);
if (selWays.size() != 2) return null;
if (selNodes.size() != 1) return null;
w1 = selWays.get(0);
w2 = selWays.get(1);
via = selNodes.get(0);
if (!w1.getNodes().contains(via) || !w2.getNodes().contains(via)) {
// the selected node is not an acceptable via node
via = null;
}
} else {
// the selection doesn't consists of primitives for which we can build
// a turn restriction
return null;
}
// if we get here, we know the two "legs" of the turn restriction. We may
// or may not know a via node, though
assert w1 != null;
assert w2 != null;
Relation tr = new Relation();
tr.put("type", "restriction");
tr.addMember(new RelationMember("from", w1));
tr.addMember(new RelationMember("to", w2));
if (via != null) {
tr.addMember(new RelationMember("via", via));
RelativeWayJoinOrientation orientation = determineWayJoinOrientation(w1, w2);
if (orientation != null) {
switch(orientation) {
case LEFT:
tr.put("restriction", TurnRestrictionType.NO_LEFT_TURN.getTagValue());
break;
case RIGHT:
tr.put("restriction", TurnRestrictionType.NO_RIGHT_TURN.getTagValue());
break;
}
}
}
return tr;
}
protected Relation initEmptyTurnRestriction() {
Relation tr = new Relation();
tr.put("type", "restriction");
return tr;
}
/**
* Creates and initializes a new turn restriction based on primitives
* in {@code primitives}.
*
* @param primitives the primitives
* @return the new initialized turn restriction. The turn restriction isn't
* added to the layer yet.
*/
public synchronized Relation build(List<OsmPrimitive> primitives) {
if (primitives == null || primitives.isEmpty()) {
return initEmptyTurnRestriction();
}
Relation tr;
switch(primitives.size()) {
// case 0 already handled
case 1:
tr = initEmptyTurnRestriction();
if (OsmPrimitive.getFilteredList(primitives, Way.class).size() == 1) {
// we have exactly one selected way? -> init the "from" leg
// of the turn restriction with it
tr.addMember(new RelationMember("from", primitives.get(0)));
}
return tr;
case 2:
tr = initNoUTurnRestriction(primitives);
if (tr != null) return tr;
tr = initTurnRestrictionFromTwoWays(primitives);
if (tr != null) return tr;
return initEmptyTurnRestriction();
default:
tr = initTurnRestrictionFromTwoWays(primitives);
if (tr != null) return tr;
return initEmptyTurnRestriction();
}
}
}