// License: GPL. Copyright 2007 by Immanuel Scholz and others
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.Collection;
import java.util.List;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.osm.visitor.Visitor;
import org.openstreetmap.josm.tools.CopyList;
import org.openstreetmap.josm.tools.Pair;
/**
* One full way, consisting of a list of way nodes.
*
* @author imi
*/
public final class Way extends OsmPrimitive {
/**
* 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<Node>(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) {
for (Node node:this.nodes) {
node.removeReferrer(this);
}
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);
}
clearCached();
fireNodesChanged();
}
/**
* Replies the number of nodes in this ways.
*
* @return the number of nodes in this ways.
* @since 1862
*/
public int getNodesCount() {
return nodes.length;
}
/**
* Replies the node at position <code>index</code>.
*
* @param index the position
* @return the node at position <code>index</code>
* @exception IndexOutOfBoundsException thrown if <code>index</code> < 0
* or <code>index</code> >= {@see #getNodesCount()}
* @since 1862
*/
public Node getNode(int index) {
return nodes[index];
}
/**
* 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 1909
*/
public boolean containsNode(Node node) {
if (node == null) return false;
for (int i=0; i<nodes.length; i++) {
if (nodes[i].equals(node))
return true;
}
return false;
}
/* mappaint data */
public boolean isMappaintArea = false;
public Integer mappaintDrawnAreaCode = 0;
/* end of mappaint data */
@Override protected void clearCached() {
super.clearCached();
isMappaintArea = false;
mappaintDrawnAreaCode = 0;
}
public ArrayList<Pair<Node,Node>> getNodePairs(boolean sort) {
ArrayList<Pair<Node,Node>> chunkSet = new ArrayList<Pair<Node,Node>>();
if (isIncomplete()) return chunkSet;
Node lastN = null;
for (Node n : this.nodes) {
if (lastN == null) {
lastN = n;
continue;
}
Pair<Node,Node> np = new Pair<Node,Node>(lastN, n);
if (sort) {
Pair.sort(np);
}
chunkSet.add(np);
lastN = n;
}
return chunkSet;
}
@Override public void visit(Visitor visitor) {
visitor.visit(this);
}
protected Way(long id, boolean allowNegative) {
super(id, allowNegative);
}
/**
* Creates a new way with id 0.
*
*/
public Way(){
super(0, false);
}
/**
*
* @param original
* @param clearId
*/
public Way(Way original, boolean clearId) {
super(original.getUniqueId(), true);
cloneFrom(original);
if (clearId) {
clearOsmId();
}
}
/**
* Create an identical clone of the argument (including the id).
*
* @param original the original way. Must not be null.
*/
public Way(Way original) {
this(original, false);
}
/**
* Creates a new 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 thrown if id < 0
*/
public Way(long id) throws IllegalArgumentException {
super(id, false);
}
/**
* Creates new way with given id and version.
* @param id
* @param version
*/
public Way(long id, int version) {
super(id, version, false);
}
/**
*
* @param data
*/
@Override
public void load(PrimitiveData data) {
super.load(data);
WayData wayData = (WayData) data;
List<Node> newNodes = new ArrayList<Node>(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);
}
@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) {
super.cloneFrom(osm);
Way otherWay = (Way)osm;
setNodes(otherWay.getNodes());
}
@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) {
if (other == null || ! (other instanceof Way) )
return false;
if (! super.hasEqualSemanticAttributes(other))
return false;
Way w = (Way)other;
if (getNodesCount() != w.getNodesCount()) return false;
for (int i=0;i<getNodesCount();i++) {
if (! getNode(i).hasEqualSemanticAttributes(w.getNode(i)))
return false;
}
return true;
}
public int compareTo(OsmPrimitive o) {
if (o instanceof Relation)
return 1;
return o instanceof Way ? Long.valueOf(getUniqueId()).compareTo(o.getUniqueId()) : -1;
}
public void removeNode(Node n) {
if (isIncomplete()) return;
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(copy);
}
public void removeNodes(Collection<? extends OsmPrimitive> selection) {
if (isIncomplete()) return;
for(OsmPrimitive p : selection) {
if (p instanceof Node) {
removeNode((Node)p);
}
}
}
/**
* 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 thrown, if this way is marked as incomplete. We can't add a node
* to an incomplete way
*/
public void addNode(Node n) throws IllegalStateException {
if (n==null) return;
if (isIncomplete())
throw new IllegalStateException(tr("Cannot add node {0} to incomplete way {1}.", n.getId(), getId()));
clearCached();
n.addReferrer(this);
Node[] newNodes = new Node[nodes.length + 1];
System.arraycopy(nodes, 0, newNodes, 0, nodes.length);
newNodes[nodes.length] = n;
nodes = newNodes;
fireNodesChanged();
}
/**
* Adds a node at position offs.
*
* @param int offs the offset
* @param n the node. Ignored, if null.
* @throws IllegalStateException thrown, if this way is marked as incomplete. We can't add a node
* to an incomplete way
* @throws IndexOutOfBoundsException thrown if offs is out of bounds
*/
public void addNode(int offs, Node n) throws IllegalStateException, IndexOutOfBoundsException {
if (n==null) return;
if (isIncomplete())
throw new IllegalStateException(tr("Cannot add node {0} to incomplete way {1}.", n.getId(), getId()));
clearCached();
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;
fireNodesChanged();
}
@Override
public void setDeleted(boolean deleted) {
for (Node n:nodes) {
if (deleted) {
n.removeReferrer(this);
} else {
n.addReferrer(this);
}
}
fireNodesChanged();
super.setDeleted(deleted);
}
public boolean isClosed() {
if (isIncomplete()) return false;
return nodes.length >= 3 && lastNode() == firstNode();
}
public Node lastNode() {
if (isIncomplete() || nodes.length == 0) return null;
return nodes[nodes.length-1];
}
public Node firstNode() {
if (isIncomplete() || nodes.length == 0) return null;
return nodes[0];
}
public boolean isFirstLastNode(Node n) {
if (isIncomplete() || nodes.length == 0) return false;
return n == firstNode() || n == lastNode();
}
@Override
public String getDisplayName(NameFormatter formatter) {
return formatter.format(this);
}
public OsmPrimitiveType getType() {
return OsmPrimitiveType.WAY;
}
private void checkNodes() {
DataSet dataSet = getDataSet();
if (dataSet != null) {
for (Node n: nodes) {
if (n.getDataSet() != dataSet)
throw new DataIntegrityProblemException("Nodes in way must be in the same dataset");
}
if (Main.pref.getBoolean("debug.checkDeleteReferenced", true)) {
for (Node n: nodes) {
if (n.isDeleted())
throw new DataIntegrityProblemException("Deleted node referenced: " + toString());
}
}
}
}
private void fireNodesChanged() {
checkNodes();
if (getDataSet() != null) {
getDataSet().fireWayNodesChanged(this);
}
}
@Override
public 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
public void updatePosition() {
bbox = new BBox(this);
}
public boolean hasIncompleteNodes() {
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();
}
}