package de.blau.android.osm; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import org.xmlpull.v1.XmlSerializer; import android.util.Log; import de.blau.android.App; import de.blau.android.R; import de.blau.android.resources.DataStyle.FeatureStyle; import de.blau.android.util.GeoMath; import de.blau.android.util.rtree.BoundedObject; public class Way extends OsmElement implements BoundedObject { private static final String DEBUG_TAG = "Way"; /** * */ private static final long serialVersionUID = 1104911642016294266L; private static final String[] importantHighways; final ArrayList<Node> nodes; public static final String NAME = "way"; public static final String NODE = "nd"; public static int maxWayNodes = 2000; // if API has a different value it will replace this private transient FeatureStyle featureProfile = null; // FeatureProfile is currently not serializable static { importantHighways = ( "motorway,motorway_link,trunk,trunk_link,primary,primary_link,"+ "secondary,secondary_link,tertiary,residential,unclassified,living_street" ).split(","); } Way(final long osmId, final long osmVersion, final byte status) { super(osmId, osmVersion, status); nodes = new ArrayList<Node>(); } /** * Add node at end of way * @param node */ void addNode(final Node node) { int size = nodes.size(); if ((size > 0) && (nodes.get(size - 1) == node)) { Log.i(DEBUG_TAG, "addNode attempt to add same node " + node.getOsmId() + " to " + getOsmId()); return; } nodes.add(node); } /** * Return list of all nodes in a way * @return */ public List<Node> getNodes() { return nodes; } /** * Be careful to leave at least 2 nodes! * * @return list of nodes allowing {@link Iterator#remove()}. */ Iterator<Node> getRemovableNodes() { return nodes.iterator(); } @Override public String getName() { return NAME; } @Override public String toString() { String res = super.toString(); if (tags != null) { for (Map.Entry<String, String> tag : tags.entrySet()) { res += "\t" + tag.getKey() + "=" + tag.getValue(); } } return res; } @Override public void toXml(final XmlSerializer s, final Long changeSetId) throws IllegalArgumentException, IllegalStateException, IOException { s.startTag("", "way"); s.attribute("", "id", Long.toString(osmId)); if (changeSetId != null) s.attribute("", "changeset", Long.toString(changeSetId)); s.attribute("", "version", Long.toString(osmVersion)); if (nodes != null) { for (Node node : nodes) { s.startTag("", "nd"); s.attribute("", "ref", Long.toString(node.getOsmId())); s.endTag("", "nd"); } } else { Log.i(DEBUG_TAG, "Way without nodes"); throw new IllegalArgumentException("Way " + getOsmId() + " has no nodes"); } tagsToXml(s); s.endTag("", "way"); } @Override public void toJosmXml(final XmlSerializer s) throws IllegalArgumentException, IllegalStateException, IOException { s.startTag("", "way"); s.attribute("", "id", Long.toString(osmId)); if (state == OsmElement.STATE_DELETED) { s.attribute("", "action", "delete"); } else if (state == OsmElement.STATE_CREATED || state == OsmElement.STATE_MODIFIED) { s.attribute("", "action", "modify"); } s.attribute("", "version", Long.toString(osmVersion)); s.attribute("", "visible", "true"); if (nodes != null) { for (Node node : nodes) { s.startTag("", "nd"); s.attribute("", "ref", Long.toString(node.getOsmId())); s.endTag("", "nd"); } } else { Log.i(DEBUG_TAG, "Way without nodes"); throw new IllegalArgumentException("Way " + getOsmId() + " has no nodes"); } tagsToXml(s); s.endTag("", "way"); } /** * Returns true if "node" is a way node of this way * @param node * @return */ public boolean hasNode(final Node node) { return nodes.contains(node); } /** * Returns true if this way has a common node with "way" * @param way * @return */ public boolean hasCommonNode(final Way way) { for (Node n : this.nodes) { if (way.hasNode(n)) { return true; } } return false; } /** * Returns the first found common node with "way" or null if their are none * @param way * @return */ public Node getCommonNode(Way way) { for (Node n : this.nodes) { if (way.hasNode(n)) { return n; } } return null; } void removeNode(final Node node) { int index = nodes.lastIndexOf(node); if (index > 0 && index < (nodes.size()-1)) { // not the first or last node if (nodes.get(index-1) == nodes.get(index+1)) { nodes.remove(index-1); Log.i(DEBUG_TAG, "removeNode removed duplicate node"); } } while (nodes.remove(node)) { } } /** * return true if first == last node, will not work for broken geometries * @return */ public boolean isClosed() { return nodes.get(0).equals(nodes.get(nodes.size() - 1)); } void appendNode(final Node refNode, final Node newNode) { if (refNode == newNode) { // user error Log.i(DEBUG_TAG, "appendNode attempt to add same node"); return; } if (nodes.get(0) == refNode) { nodes.add(0, newNode); } else if (nodes.get(nodes.size() - 1) == refNode) { nodes.add(newNode); } } void addNodeAfter(final Node nodeBefore, final Node newNode) { if (nodeBefore == newNode) { // user error Log.i(DEBUG_TAG, "addNodeAfter attempt to add same node"); return; } nodes.add(nodes.indexOf(nodeBefore) + 1, newNode); } /** * Adds multiple nodes to the way in the order in which they appear in the list. * They can be either prepended or appended to the existing nodes. * @param newNodes a list of new nodes * @param atBeginning if true, nodes are prepended, otherwise, they are appended */ void addNodes(List<Node> newNodes, boolean atBeginning) { if (atBeginning) { if ((nodes.size() > 0) && nodes.get(0) == newNodes.get(newNodes.size()-1)) { // user error Log.i(DEBUG_TAG, "addNodes attempt to add same node"); if (newNodes.size() > 1) { Log.i(DEBUG_TAG, "retrying addNodes"); newNodes.remove(newNodes.size()-1); addNodes(newNodes, atBeginning); } return; } nodes.addAll(0, newNodes); } else { if ((nodes.size() > 0) && newNodes.get(0) == nodes.get(nodes.size()-1)) { // user error Log.i(DEBUG_TAG, "addNodes attempt to add same node"); if (newNodes.size() > 1) { Log.i(DEBUG_TAG, "retrying addNodes"); newNodes.remove(0); addNodes(newNodes, atBeginning); } return; } nodes.addAll(newNodes); } } /** * Reverses the direction of the way */ void reverse() { Collections.reverse(nodes); } /** * Replace an existing node in a way with a different node. * @param existing The existing node to be replaced. * @param newNode The new node. */ void replaceNode(Node existing, Node newNode) { int idx; while ((idx = nodes.indexOf(existing)) != -1) { nodes.set(idx, newNode); // check for duplicates if (idx > 0 && nodes.get(idx-1).equals(newNode)) { Log.i(DEBUG_TAG, "replaceNode node would duplicate preceeding node"); nodes.remove(idx); } if (idx >= 0 && idx < nodes.size()-1 && nodes.get(idx+1).equals(newNode)) { Log.i(DEBUG_TAG, "replaceNode node would duplicate following node"); nodes.remove(idx); } } } /** * Checks if a node is an end node of the way (i.e. either the first or the last one) * @param node a node to check * @return */ public boolean isEndNode(final Node node) { return getFirstNode() == node || getLastNode() == node; } public Node getFirstNode() { return nodes.get(0); } public Node getLastNode() { return nodes.get(nodes.size() - 1); } /** * Checks if this way is tagged as oneway * @return 1 if this is a regular oneway-way (oneway:yes, oneway:true or oneway:1), * -1 if this is a reverse oneway-way (oneway:-1 or oneway:reverse), * 0 if this is not a oneway-way (no oneway tag or tag with none of the specified values) */ public int getOneway() { String oneway = getTagWithKey("oneway"); if ("yes".equalsIgnoreCase(oneway) || "true".equalsIgnoreCase(oneway) || "1".equals(oneway)) { return 1; } else if ("-1".equals(oneway) || "reverse".equalsIgnoreCase(oneway)) { return -1; } return 0; } /** * There is a set of tags which lead to a way not being reversible, this is EXTREMLY stupid and should be depreciated immediately. * * natural=cliff * natural=coastline * barrier=retaining_wall * barrier=kerb * barrier=guard_rail * man_made=embankment * barrier=city_wall if two_sided != yes * waterway=* * * @return true if somebody added the brain dead tags */ public boolean notReversable() { boolean brainDead = false; String waterway = getTagWithKey(Tags.KEY_WATERWAY); if (waterway != null) { brainDead = true; // IHMO } else { String natural = getTagWithKey(Tags.KEY_NATURAL); if ((natural != null) && (natural.equals(Tags.VALUE_CLIFF) || natural.equals(Tags.VALUE_COASTLINE))) { brainDead = true; // IHMO } else { String barrier = getTagWithKey(Tags.KEY_BARRIER); if ((barrier != null) && barrier.equals(Tags.VALUE_RETAINING_WALL)) { brainDead = true; // IHMO } else if ((barrier != null) && barrier.equals(Tags.VALUE_KERB)) { brainDead = true; // } else if ((barrier != null) && barrier.equals(Tags.VALUE_GUARD_RAIL)) { brainDead = true; // } else if ((barrier != null) && barrier.equals(Tags.VALUE_CITY_WALL) && ((getTagWithKey(Tags.KEY_TWO_SIDED) == null) || !getTagWithKey(Tags.KEY_TWO_SIDED).equals(Tags.VALUE_YES))) { brainDead = true; // IMHO } else { String man_made = getTagWithKey(Tags.KEY_MAN_MADE); if ((man_made != null) && man_made.equals(Tags.VALUE_EMBANKMENT)) { brainDead = true; // IHMO } } } } return brainDead; } private boolean hasTagWithValue(String tag, String value) { String tagValue = getTagWithKey(tag); return tagValue != null && tagValue.equalsIgnoreCase(value); } /** * Test if the way has a problem. * @return true if the way has a problem, false if it doesn't. */ @Override protected boolean calcProblem() { String highway = getTagWithKey(Tags.KEY_HIGHWAY); // cache frequently accessed key if (Tags.VALUE_ROAD.equalsIgnoreCase(highway)) { // unsurveyed road return true; } if ((getTagWithKey(Tags.KEY_NAME) == null) && (getTagWithKey(Tags.KEY_REF) == null) && !(hasTagWithValue(Tags.KEY_NONAME,Tags.VALUE_YES) || hasTagWithValue(Tags.KEY_VALIDATE_NO_NAME,Tags.VALUE_YES))) { // unnamed way - only the important ones need names for (String h : importantHighways) { if (h.equalsIgnoreCase(highway)) { return true; } } } return super.calcProblem(); } @Override public String describeProblem() { String superProblem = super.describeProblem(); String wayProblem = ""; String highway = getTagWithKey(Tags.KEY_HIGHWAY); if (Tags.VALUE_ROAD.equalsIgnoreCase(highway)) { wayProblem = App.resources().getString(R.string.toast_unsurveyed_road); } if ((getTagWithKey(Tags.KEY_NAME) == null) && (getTagWithKey(Tags.KEY_REF) == null) && !(hasTagWithValue(Tags.KEY_NONAME,Tags.VALUE_YES) || hasTagWithValue(Tags.KEY_VALIDATE_NO_NAME,Tags.VALUE_YES))) { boolean isImportant = false; for (String h : importantHighways) { if (h.equalsIgnoreCase(highway)) { isImportant = true; break; } } if (isImportant) { wayProblem = !wayProblem.equals("") ? wayProblem +", " : App.resources().getString(R.string.toast_noname); } } if (!superProblem.equals("")) return superProblem + (!wayProblem.equals("") ? "\n" + wayProblem : ""); else return wayProblem; } @Override public ElementType getType() { if (nodes.size() < 2) return ElementType.WAY; // should not happen if (getFirstNode().equals(getLastNode())) { return ElementType.CLOSEDWAY; } else { return ElementType.WAY; } } @Override public ElementType getType(Map<String,String> tags) { return getType(); } @Override void updateState(final byte newState) { featureProfile = null; // force recalc of style super.updateState(newState); } @Override void setState(final byte newState) { featureProfile = null; // force recalc of style super.setState(newState); } public FeatureStyle getFeatureProfile() { return featureProfile; } public void setFeatureProfile(FeatureStyle fp) { featureProfile = fp; } /** * return the number of nodes in the is way * @return */ public int nodeCount() { return nodes == null ? 0 : nodes.size(); } /** * return the length in m * @return */ public double length() { double result = 0d; if (nodes != null) { for (int i = 0; i < (nodes.size() - 1); i++) { result = result + GeoMath.haversineDistance(nodes.get(i).getLon()/1E7D, nodes.get(i).getLat()/1E7D, nodes.get(i+1).getLon()/1E7D, nodes.get(i+1).getLat()/1E7D); } } return result; } /** * Note this is only useful for sorting given that the result is returned in WGS84 °*1E7 or so * @param location * @return the minimum distance of this way to the given location */ public double getDistance(final int[] location) { double distance = Double.MAX_VALUE; if (location != null) { Node n1 = null; for (Node n2 : getNodes()) { // distance to nodes of way if (n1 != null) { // distance to lines of way distance = Math.min(distance, GeoMath.getLineDistance( location[0], location[1], n1.getLat(), n1.getLon(), n2.getLat(), n2.getLon())); } n1 = n2; } } return distance; } /** * Returns a bounding box covering the way * FIXME results should be cached in some intelligent way * * @return the bounding box of the way */ public BoundingBox getBounds() { BoundingBox result = null; boolean first = true; for (Node n : getNodes()) { if (first) { result = new BoundingBox(n.lon,n.lat); first = false; } else { result.union(n.lon,n.lat); } } return result; } /** * Returns a bounding box covering the way * FIXME results should be cached in some intelligent way * * @param result a bounding box to use for producing the result, avoids creating an object instance * @return the bounding box of the way */ public BoundingBox getBounds(BoundingBox result) { boolean first = true; for (Node n : getNodes()) { if (first) { result.resetTo(n.lon,n.lat); first = false; } else { result.union(n.lon,n.lat); } } return result; } /** * Set the maximum number of nodes allowed in one way * @param max */ public static void setMaxWayNodes(int max) { maxWayNodes = max; } }