// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.osm;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
import org.openstreetmap.josm.data.osm.visitor.Visitor;
import org.openstreetmap.josm.gui.DefaultNameFormatter;
import org.openstreetmap.josm.tools.CopyList;
import org.openstreetmap.josm.tools.Pair;
import org.openstreetmap.josm.tools.Utils;
/**
* One full way, consisting of a list of way {@link Node nodes}.
*
* @author imi
* @since 64
*/
public final class Way extends OsmPrimitive implements IWay {
/**
* All way nodes in this way
*
*/
private Node[] nodes = new Node[0];
private BBox bbox;
/**
*
* You can modify returned list but changes will not be propagated back
* to the Way. Use {@link #setNodes(List)} to update this way
* @return Nodes composing the way
* @since 1862
*/
public List<Node> getNodes() {
return new CopyList<>(nodes);
}
/**
* Set new list of nodes to way. This method is preferred to multiple calls to addNode/removeNode
* and similar methods because nodes are internally saved as array which means lower memory overhead
* but also slower modifying operations.
* @param nodes New way nodes. Can be null, in that case all way nodes are removed
* @since 1862
*/
public void setNodes(List<Node> nodes) {
boolean locked = writeLock();
try {
for (Node node:this.nodes) {
node.removeReferrer(this);
node.clearCachedStyle();
}
if (nodes == null) {
this.nodes = new Node[0];
} else {
this.nodes = nodes.toArray(new Node[nodes.size()]);
}
for (Node node: this.nodes) {
node.addReferrer(this);
node.clearCachedStyle();
}
clearCachedStyle();
fireNodesChanged();
} finally {
writeUnlock(locked);
}
}
/**
* Prevent directly following identical nodes in ways.
* @param nodes list of nodes
* @return {@code nodes} with consecutive identical nodes removed
*/
private static List<Node> removeDouble(List<Node> nodes) {
Node last = null;
int count = nodes.size();
for (int i = 0; i < count && count > 2;) {
Node n = nodes.get(i);
if (last == n) {
nodes.remove(i);
--count;
} else {
last = n;
++i;
}
}
return nodes;
}
@Override
public int getNodesCount() {
return nodes.length;
}
/**
* Replies the real number of nodes in this way (full number of nodes minus one if this way is closed)
*
* @return the real number of nodes in this way.
*
* @see #getNodesCount()
* @see #isClosed()
* @since 5847
*/
public int getRealNodesCount() {
int count = getNodesCount();
return isClosed() ? count-1 : count;
}
/**
* Replies the node at position <code>index</code>.
*
* @param index the position
* @return the node at position <code>index</code>
* @throws IndexOutOfBoundsException if <code>index</code> < 0
* or <code>index</code> >= {@link #getNodesCount()}
* @since 1862
*/
public Node getNode(int index) {
return nodes[index];
}
@Override
public long getNodeId(int idx) {
return nodes[idx].getUniqueId();
}
/**
* Replies true if this way contains the node <code>node</code>, false
* otherwise. Replies false if <code>node</code> is null.
*
* @param node the node. May be null.
* @return true if this way contains the node <code>node</code>, false
* otherwise
* @since 1911
*/
public boolean containsNode(Node node) {
if (node == null) return false;
Node[] nodes = this.nodes;
for (Node n : nodes) {
if (n.equals(node))
return true;
}
return false;
}
/**
* Return nodes adjacent to <code>node</code>
*
* @param node the node. May be null.
* @return Set of nodes adjacent to <code>node</code>
* @since 4671
*/
public Set<Node> getNeighbours(Node node) {
Set<Node> neigh = new HashSet<>();
if (node == null) return neigh;
Node[] nodes = this.nodes;
for (int i = 0; i < nodes.length; i++) {
if (nodes[i].equals(node)) {
if (i > 0)
neigh.add(nodes[i-1]);
if (i < nodes.length-1)
neigh.add(nodes[i+1]);
}
}
return neigh;
}
/**
* Replies the ordered {@link List} of chunks of this way. Each chunk is replied as a {@link Pair} of {@link Node nodes}.
* @param sort If true, the nodes of each pair are sorted as defined by {@link Pair#sort}.
* If false, Pair.a and Pair.b are in the way order
* (i.e for a given Pair(n), Pair(n-1).b == Pair(n).a, Pair(n).b == Pair(n+1).a, etc.)
* @return The ordered list of chunks of this way.
* @since 3348
*/
public List<Pair<Node, Node>> getNodePairs(boolean sort) {
List<Pair<Node, Node>> chunkSet = new ArrayList<>();
if (isIncomplete()) return chunkSet;
Node lastN = null;
Node[] nodes = this.nodes;
for (Node n : nodes) {
if (lastN == null) {
lastN = n;
continue;
}
Pair<Node, Node> np = new Pair<>(lastN, n);
if (sort) {
Pair.sort(np);
}
chunkSet.add(np);
lastN = n;
}
return chunkSet;
}
@Override public void accept(Visitor visitor) {
visitor.visit(this);
}
@Override public void accept(PrimitiveVisitor visitor) {
visitor.visit(this);
}
protected Way(long id, boolean allowNegative) {
super(id, allowNegative);
}
/**
* Contructs a new {@code Way} with id 0.
* @since 86
*/
public Way() {
super(0, false);
}
/**
* Contructs a new {@code Way} from an existing {@code Way}.
* @param original The original {@code Way} to be identically cloned. Must not be null
* @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}.
* If {@code false}, does nothing
* @since 2410
*/
public Way(Way original, boolean clearMetadata) {
super(original.getUniqueId(), true);
cloneFrom(original);
if (clearMetadata) {
clearOsmMetadata();
}
}
/**
* Contructs a new {@code Way} from an existing {@code Way} (including its id).
* @param original The original {@code Way} to be identically cloned. Must not be null
* @since 86
*/
public Way(Way original) {
this(original, false);
}
/**
* Contructs a new {@code Way} for the given id. If the id > 0, the way is marked
* as incomplete. If id == 0 then way is marked as new
*
* @param id the id. >= 0 required
* @throws IllegalArgumentException if id < 0
* @since 343
*/
public Way(long id) {
super(id, false);
}
/**
* Contructs a new {@code Way} with given id and version.
* @param id the id. >= 0 required
* @param version the version
* @throws IllegalArgumentException if id < 0
* @since 2620
*/
public Way(long id, int version) {
super(id, version, false);
}
@Override
public void load(PrimitiveData data) {
if (!(data instanceof WayData))
throw new IllegalArgumentException("Not a way data: " + data);
boolean locked = writeLock();
try {
super.load(data);
WayData wayData = (WayData) data;
if (!wayData.getNodes().isEmpty() && getDataSet() == null) {
throw new AssertionError("Data consistency problem - way without dataset detected");
}
List<Node> newNodes = new ArrayList<>(wayData.getNodes().size());
for (Long nodeId : wayData.getNodes()) {
Node node = (Node) getDataSet().getPrimitiveById(nodeId, OsmPrimitiveType.NODE);
if (node != null) {
newNodes.add(node);
} else {
throw new AssertionError("Data consistency problem - way with missing node detected");
}
}
setNodes(newNodes);
} finally {
writeUnlock(locked);
}
}
@Override
public WayData save() {
WayData data = new WayData();
saveCommonAttributes(data);
for (Node node:nodes) {
data.getNodes().add(node.getUniqueId());
}
return data;
}
@Override
public void cloneFrom(OsmPrimitive osm) {
if (!(osm instanceof Way))
throw new IllegalArgumentException("Not a way: " + osm);
boolean locked = writeLock();
try {
super.cloneFrom(osm);
Way otherWay = (Way) osm;
setNodes(otherWay.getNodes());
} finally {
writeUnlock(locked);
}
}
@Override
public String toString() {
String nodesDesc = isIncomplete() ? "(incomplete)" : ("nodes=" + Arrays.toString(nodes));
return "{Way id=" + getUniqueId() + " version=" + getVersion()+ ' ' + getFlagsAsString() + ' ' + nodesDesc + '}';
}
@Override
public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) {
if (!(other instanceof Way))
return false;
Way w = (Way) other;
if (getNodesCount() != w.getNodesCount()) return false;
if (!super.hasEqualSemanticAttributes(other, testInterestingTagsOnly))
return false;
for (int i = 0; i < getNodesCount(); i++) {
if (!getNode(i).hasEqualSemanticAttributes(w.getNode(i)))
return false;
}
return true;
}
@Override
public int compareTo(OsmPrimitive o) {
if (o instanceof Relation)
return 1;
return o instanceof Way ? Long.compare(getUniqueId(), o.getUniqueId()) : -1;
}
/**
* Removes the given {@link Node} from this way. Ignored, if n is null.
* @param n The node to remove. Ignored, if null
* @since 1463
*/
public void removeNode(Node n) {
if (n == null || isIncomplete()) return;
boolean locked = writeLock();
try {
boolean closed = lastNode() == n && firstNode() == n;
int i;
List<Node> copy = getNodes();
while ((i = copy.indexOf(n)) >= 0) {
copy.remove(i);
}
i = copy.size();
if (closed && i > 2) {
copy.add(copy.get(0));
} else if (i >= 2 && i <= 3 && copy.get(0) == copy.get(i-1)) {
copy.remove(i-1);
}
setNodes(removeDouble(copy));
n.clearCachedStyle();
} finally {
writeUnlock(locked);
}
}
/**
* Removes the given set of {@link Node nodes} from this way. Ignored, if selection is null.
* @param selection The selection of nodes to remove. Ignored, if null
* @since 5408
*/
public void removeNodes(Set<? extends Node> selection) {
if (selection == null || isIncomplete()) return;
boolean locked = writeLock();
try {
boolean closed = isClosed() && selection.contains(lastNode());
List<Node> copy = new ArrayList<>();
for (Node n: nodes) {
if (!selection.contains(n)) {
copy.add(n);
}
}
int i = copy.size();
if (closed && i > 2) {
copy.add(copy.get(0));
} else if (i >= 2 && i <= 3 && copy.get(0) == copy.get(i-1)) {
copy.remove(i-1);
}
setNodes(removeDouble(copy));
for (Node n : selection) {
n.clearCachedStyle();
}
} finally {
writeUnlock(locked);
}
}
/**
* Adds a node to the end of the list of nodes. Ignored, if n is null.
*
* @param n the node. Ignored, if null
* @throws IllegalStateException if this way is marked as incomplete. We can't add a node
* to an incomplete way
* @since 1313
*/
public void addNode(Node n) {
if (n == null) return;
boolean locked = writeLock();
try {
if (isIncomplete())
throw new IllegalStateException(tr("Cannot add node {0} to incomplete way {1}.", n.getId(), getId()));
clearCachedStyle();
n.addReferrer(this);
nodes = Utils.addInArrayCopy(nodes, n);
n.clearCachedStyle();
fireNodesChanged();
} finally {
writeUnlock(locked);
}
}
/**
* Adds a node at position offs.
*
* @param offs the offset
* @param n the node. Ignored, if null.
* @throws IllegalStateException if this way is marked as incomplete. We can't add a node
* to an incomplete way
* @throws IndexOutOfBoundsException if offs is out of bounds
* @since 1313
*/
public void addNode(int offs, Node n) {
if (n == null) return;
boolean locked = writeLock();
try {
if (isIncomplete())
throw new IllegalStateException(tr("Cannot add node {0} to incomplete way {1}.", n.getId(), getId()));
clearCachedStyle();
n.addReferrer(this);
Node[] newNodes = new Node[nodes.length + 1];
System.arraycopy(nodes, 0, newNodes, 0, offs);
System.arraycopy(nodes, offs, newNodes, offs + 1, nodes.length - offs);
newNodes[offs] = n;
nodes = newNodes;
n.clearCachedStyle();
fireNodesChanged();
} finally {
writeUnlock(locked);
}
}
@Override
public void setDeleted(boolean deleted) {
boolean locked = writeLock();
try {
for (Node n:nodes) {
if (deleted) {
n.removeReferrer(this);
} else {
n.addReferrer(this);
}
n.clearCachedStyle();
}
fireNodesChanged();
super.setDeleted(deleted);
} finally {
writeUnlock(locked);
}
}
@Override
public boolean isClosed() {
if (isIncomplete()) return false;
Node[] nodes = this.nodes;
return nodes.length >= 3 && nodes[nodes.length-1] == nodes[0];
}
/**
* Determines if this way denotes an area (closed way with at least three distinct nodes).
* @return {@code true} if this way is closed and contains at least three distinct nodes
* @see #isClosed
* @since 5490
*/
public boolean isArea() {
if (this.nodes.length >= 4 && isClosed()) {
Node distinctNode = null;
for (int i = 1; i < nodes.length-1; i++) {
if (distinctNode == null && nodes[i] != nodes[0]) {
distinctNode = nodes[i];
} else if (distinctNode != null && nodes[i] != nodes[0] && nodes[i] != distinctNode) {
return true;
}
}
}
return false;
}
/**
* Returns the last node of this way.
* The result equals <tt>{@link #getNode getNode}({@link #getNodesCount getNodesCount} - 1)</tt>.
* @return the last node of this way
* @since 1400
*/
public Node lastNode() {
Node[] nodes = this.nodes;
if (isIncomplete() || nodes.length == 0) return null;
return nodes[nodes.length-1];
}
/**
* Returns the first node of this way.
* The result equals {@link #getNode getNode}{@code (0)}.
* @return the first node of this way
* @since 1400
*/
public Node firstNode() {
Node[] nodes = this.nodes;
if (isIncomplete() || nodes.length == 0) return null;
return nodes[0];
}
/**
* Replies true if the given node is the first or the last one of this way, false otherwise.
* @param n The node to test
* @return true if the {@code n} is the first or the last node, false otherwise.
* @since 1400
*/
public boolean isFirstLastNode(Node n) {
Node[] nodes = this.nodes;
if (isIncomplete() || nodes.length == 0) return false;
return n == nodes[0] || n == nodes[nodes.length -1];
}
/**
* Replies true if the given node is an inner node of this way, false otherwise.
* @param n The node to test
* @return true if the {@code n} is an inner node, false otherwise.
* @since 3515
*/
public boolean isInnerNode(Node n) {
Node[] nodes = this.nodes;
if (isIncomplete() || nodes.length <= 2) return false;
/* circular ways have only inner nodes, so return true for them! */
if (n == nodes[0] && n == nodes[nodes.length-1]) return true;
for (int i = 1; i < nodes.length - 1; ++i) {
if (nodes[i] == n) return true;
}
return false;
}
@Override
public String getDisplayName(NameFormatter formatter) {
return formatter.format(this);
}
@Override
public OsmPrimitiveType getType() {
return OsmPrimitiveType.WAY;
}
@Override
public OsmPrimitiveType getDisplayType() {
return isClosed() ? OsmPrimitiveType.CLOSEDWAY : OsmPrimitiveType.WAY;
}
private void checkNodes() {
DataSet dataSet = getDataSet();
if (dataSet != null) {
Node[] nodes = this.nodes;
for (Node n: nodes) {
if (n.getDataSet() != dataSet)
throw new DataIntegrityProblemException("Nodes in way must be in the same dataset",
tr("Nodes in way must be in the same dataset"));
if (n.isDeleted())
throw new DataIntegrityProblemException("Deleted node referenced: " + toString(),
"<html>" + tr("Deleted node referenced by {0}",
DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(this)) + "</html>");
}
if (Main.pref.getBoolean("debug.checkNullCoor", true)) {
for (Node n: nodes) {
if (n.isVisible() && !n.isIncomplete() && !n.isLatLonKnown())
throw new DataIntegrityProblemException("Complete visible node with null coordinates: " + toString(),
"<html>" + tr("Complete node {0} with null coordinates in way {1}",
DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(n),
DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(this)) + "</html>");
}
}
}
}
private void fireNodesChanged() {
checkNodes();
if (getDataSet() != null) {
getDataSet().fireWayNodesChanged(this);
}
}
@Override
void setDataset(DataSet dataSet) {
super.setDataset(dataSet);
checkNodes();
}
@Override
public BBox getBBox() {
if (getDataSet() == null)
return new BBox(this);
if (bbox == null) {
bbox = new BBox(this);
}
return new BBox(bbox);
}
@Override
protected void addToBBox(BBox box, Set<PrimitiveId> visited) {
box.add(getBBox());
}
@Override
public void updatePosition() {
bbox = new BBox(this);
}
/**
* Replies true if this way has incomplete nodes, false otherwise.
* @return true if this way has incomplete nodes, false otherwise.
* @since 2587
*/
public boolean hasIncompleteNodes() {
Node[] nodes = this.nodes;
for (Node node : nodes) {
if (node.isIncomplete())
return true;
}
return false;
}
@Override
public boolean isUsable() {
return super.isUsable() && !hasIncompleteNodes();
}
@Override
public boolean isDrawable() {
return super.isDrawable() && !hasIncompleteNodes();
}
/**
* Replies the length of the way, in metres, as computed by {@link LatLon#greatCircleDistance}.
* @return The length of the way, in metres
* @since 4138
*/
public double getLength() {
double length = 0;
Node lastN = null;
for (Node n:nodes) {
if (lastN != null) {
LatLon lastNcoor = lastN.getCoor();
LatLon coor = n.getCoor();
if (lastNcoor != null && coor != null) {
length += coor.greatCircleDistance(lastNcoor);
}
}
lastN = n;
}
return length;
}
/**
* Replies the length of the longest segment of the way, in metres, as computed by {@link LatLon#greatCircleDistance}.
* @return The length of the segment, in metres
* @since 8320
*/
public double getLongestSegmentLength() {
double length = 0;
Node lastN = null;
for (Node n:nodes) {
if (lastN != null) {
LatLon lastNcoor = lastN.getCoor();
LatLon coor = n.getCoor();
if (lastNcoor != null && coor != null) {
double l = coor.greatCircleDistance(lastNcoor);
if (l > length) {
length = l;
}
}
}
lastN = n;
}
return length;
}
/**
* Tests if this way is a oneway.
* @return {@code 1} if the way is a oneway,
* {@code -1} if the way is a reversed oneway,
* {@code 0} otherwise.
* @since 5199
*/
public int isOneway() {
String oneway = get("oneway");
if (oneway != null) {
if ("-1".equals(oneway)) {
return -1;
} else {
Boolean isOneway = OsmUtils.getOsmBoolean(oneway);
if (isOneway != null && isOneway) {
return 1;
}
}
}
return 0;
}
/**
* Replies the first node of this way, respecting or not its oneway state.
* @param respectOneway If true and if this way is a reversed oneway, replies the last node. Otherwise, replies the first node.
* @return the first node of this way, according to {@code respectOneway} and its oneway state.
* @since 5199
*/
public Node firstNode(boolean respectOneway) {
return !respectOneway || isOneway() != -1 ? firstNode() : lastNode();
}
/**
* Replies the last node of this way, respecting or not its oneway state.
* @param respectOneway If true and if this way is a reversed oneway, replies the first node. Otherwise, replies the last node.
* @return the last node of this way, according to {@code respectOneway} and its oneway state.
* @since 5199
*/
public Node lastNode(boolean respectOneway) {
return !respectOneway || isOneway() != -1 ? lastNode() : firstNode();
}
@Override
public boolean concernsArea() {
return hasAreaTags();
}
@Override
public boolean isOutsideDownloadArea() {
for (final Node n : nodes) {
if (n.isOutsideDownloadArea()) {
return true;
}
}
return false;
}
@Override
protected void keysChangedImpl(Map<String, String> originalKeys) {
super.keysChangedImpl(originalKeys);
clearCachedNodeStyles();
}
public void clearCachedNodeStyles() {
for (final Node n : nodes) {
n.clearCachedStyle();
}
}
}