package de.blau.android.osm; import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; import java.net.MalformedURLException; import java.net.ProtocolException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import org.acra.ACRA; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlSerializer; import com.drew.lang.annotations.NotNull; import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import de.blau.android.App; import de.blau.android.R; import de.blau.android.exception.OsmException; import de.blau.android.exception.OsmIllegalOperationException; import de.blau.android.exception.OsmServerException; import de.blau.android.exception.StorageException; import de.blau.android.filter.Filter; import de.blau.android.util.GeoMath; import de.blau.android.util.SavingHelper; import de.blau.android.util.SavingHelper.Exportable; import de.blau.android.util.Snack; import de.blau.android.util.Util; import de.blau.android.util.collections.LongOsmElementMap; public class StorageDelegator implements Serializable, Exportable { private static final long serialVersionUID = 9L; private Storage currentStorage; private Storage apiStorage; private UndoStorage undo; private ClipboardStorage clipboard; private ArrayList<String> imagery; /** * when reading state lockout writing/reading */ private transient ReentrantLock readingLock = new ReentrantLock(); /** * Indicates whether changes have been made since the last save to disk. * Since a newly created storage is not saved, the constructor sets it to true. * After a successful save or load, it is set to false. * If it is false, save does nothing. */ private transient boolean dirty; /** * if false we need to check if the current imagery has been recorded */ private transient boolean imageryRecorded = false; private final static String DEBUG_TAG = StorageDelegator.class.getSimpleName(); public final static String FILENAME = "lastActivity.res"; private transient SavingHelper<StorageDelegator> savingHelper = new SavingHelper<StorageDelegator>(); /** * A OsmElementFactory that is used to create new elements. * Needs to be persisted together with currentStorage/apiStorage to avoid duplicate IDs * when the application is restarted after some elements have been created. */ private OsmElementFactory factory; public void setCurrentStorage(final Storage currentStorage) { dirty = true; apiStorage = new Storage(); clipboard = new ClipboardStorage(); this.currentStorage = currentStorage; undo = new UndoStorage(currentStorage, apiStorage); } public StorageDelegator() { reset(false); // don't set dirty on instantiation } public void reset(boolean dirty) { this.dirty = dirty; apiStorage = new Storage(); currentStorage = new Storage(); clipboard = new ClipboardStorage(); undo = new UndoStorage(currentStorage, apiStorage); factory = new OsmElementFactory(); imagery = new ArrayList<String>(); } public boolean isDirty() { return dirty; } /** * set dirty to true */ public void dirty() { dirty = true; Log.d("StorageDelegator", "setting delegator to dirty"); } /** * Get the current undo instance. * For immediate use only - DO NOT CACHE THIS. * @return the UndoStorage, allowing operations like creation of checkpoints and undo/redo. */ public UndoStorage getUndo() { return undo; } /** * Clears the undo storage. */ public void clearUndo() { undo = new UndoStorage(currentStorage, apiStorage); } /** * Get the current OsmElementFactory instance used by this delegator. * Use only the factory returned by this to create new element IDs for insertion into this delegator! * For immediate use only - DO NOT CACHE THIS. * @return the OsmElementFactory for creating nodes/ways with new IDs */ public OsmElementFactory getFactory() { return factory; } public void insertElementSafe(final OsmElement elem) { dirty = true; undo.save(elem); try { currentStorage.insertElementSafe(elem); apiStorage.insertElementSafe(elem); onElementChanged(null, null); } catch (StorageException e) { // TODO handle OOMk e.printStackTrace(); } } /** * Sets the tags of the element, replacing all existing ones * @param elem the element to tag * @param tags the new tags */ public void setTags(@NonNull final OsmElement elem, @Nullable final Map<String, String> tags) { dirty = true; undo.save(elem); if (elem.setTags(tags)) { // OsmElement tags have changed elem.updateState(OsmElement.STATE_MODIFIED); try { apiStorage.insertElementSafe(elem); onElementChanged(null, null); } catch (StorageException e) { // TODO handle OOM e.printStackTrace(); } } } private void insertElementUnsafe(final OsmElement elem) { dirty = true; undo.save(elem); try { currentStorage.insertElementUnsafe(elem); apiStorage.insertElementUnsafe(elem); onElementChanged(null, null); } catch (StorageException e) { // TODO handle OOMk e.printStackTrace(); } } /** * Called after an element has been changed * * As it may be fairly expensive to determine all changes pre and/or post may be null * @param pre list of changed elements before the operation or null * @param post list of changed elements after the operation or null */ private void onElementChanged(@Nullable List<OsmElement> pre, @Nullable List<OsmElement> post) { Filter filter = App.getLogic().getFilter(); if (filter != null) { filter.onElementChanged(pre, post); } } /** * Store the currently used imagery */ public void recordImagery(@Nullable de.blau.android.Map map) { if (!imageryRecorded) { // flag is reset when we change imagery try { if (map != null) { // currently we only modify data when the map exists ArrayList<String>currentImagery = map.getImageryNames(); for (String i:currentImagery) { if (!imagery.contains(i) && !"None".equalsIgnoreCase(i)) { imagery.add(i); } } imageryRecorded = true; } } catch (Exception ignored) { // never fail on anything here } catch (Error ignored) { } } } public void setImageryRecorded(boolean recorded) { imageryRecorded = recorded; } /** * Create apiStorage (aka the changes to the original data) based on state field of the elements. * Assumes that apiStorage is empty. As a side effect it updates the id sequences for the creation of new elements. */ public synchronized void fixupApiStorage() { try { long minNodeId = 0; long minWayId = 0; long minRelationId = 0; List<Node> nl = new ArrayList<Node>(currentStorage.getNodes()); for (Node n:nl) { if (n.getState()!=OsmElement.STATE_UNCHANGED) { apiStorage.insertElementUnsafe(n); if (n.getOsmId() < minNodeId) { minNodeId = n.getOsmId(); } } if (n.getState()==OsmElement.STATE_DELETED) { currentStorage.removeElement(n); } } List<Way> wl = new ArrayList<Way>(currentStorage.getWays()); for (Way w:wl) { if (w.getState()!=OsmElement.STATE_UNCHANGED) { apiStorage.insertElementUnsafe(w); if (w.getOsmId() < minWayId) { minWayId = w.getOsmId(); } } if (w.getState()==OsmElement.STATE_DELETED) { currentStorage.removeElement(w); } } List<Relation> rl = new ArrayList<Relation>(currentStorage.getRelations()); for (Relation r:rl) { if (r.getState()!=OsmElement.STATE_UNCHANGED) { apiStorage.insertElementUnsafe(r); if (r.getOsmId() < minRelationId) { minRelationId = r.getOsmId(); } } if (r.getState()==OsmElement.STATE_DELETED) { currentStorage.removeElement(r); } } getFactory().setIdSequences(minNodeId, minWayId, minRelationId); } catch (StorageException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * Create empty relation * @param members * @return */ public Relation createAndInsertRelation(List<OsmElement> members) { // undo - nothing done here, way gets saved/marked on insert dirty = true; Relation relation = factory.createRelationWithNewId(); insertElementUnsafe(relation); if (members != null) { for (OsmElement e:members) { undo.save(e); RelationMember rm = new RelationMember("", e); relation.addMember(rm); e.addParentRelation(relation); } } return relation; } /** * @param firstWayNode * @return */ public Way createAndInsertWay(final Node firstWayNode) { // undo - nothing done here, way gets saved/marked on insert dirty = true; Way way = factory.createWayWithNewId(); way.addNode(firstWayNode); insertElementUnsafe(way); return way; } public void addNodeToWay(final Node node, final Way way) throws OsmIllegalOperationException { dirty = true; undo.save(way); try { if (way.nodeCount() + 1 > Way.maxWayNodes) throw new OsmIllegalOperationException(App.resources().getString(R.string.exception_too_many_nodes)); apiStorage.insertElementSafe(way); way.addNode(node); way.updateState(OsmElement.STATE_MODIFIED); onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } public void addNodeToWayAfter(final Node nodeBefore, final Node newNode, final Way way) throws OsmIllegalOperationException { dirty = true; undo.save(way); try { if (way.nodeCount() + 1 > Way.maxWayNodes) throw new OsmIllegalOperationException(App.resources().getString(R.string.exception_too_many_nodes)); apiStorage.insertElementSafe(way); way.addNodeAfter(nodeBefore, newNode); way.updateState(OsmElement.STATE_MODIFIED); onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } public void appendNodeToWay(final Node refNode, final Node nextNode, final Way way) throws OsmIllegalOperationException { dirty = true; undo.save(way); try { if (way.nodeCount() + 1 > Way.maxWayNodes) throw new OsmIllegalOperationException(App.resources().getString(R.string.exception_too_many_nodes)); apiStorage.insertElementSafe(way); way.appendNode(refNode, nextNode); way.updateState(OsmElement.STATE_MODIFIED); onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } public void updateLatLon(final Node node, final int latE7, final int lonE7) { dirty = true; undo.save(node); try { apiStorage.insertElementSafe(node); node.setLat(latE7); node.setLon(lonE7); node.updateState(OsmElement.STATE_MODIFIED); onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } /** * Mode all nodes in a way, since the nodes keep their ids, the way itself doesn't change and doesn't need to be saved * apply translation only once to the first node if way is closed * @param way * @param deltaLatE7 * @param deltaLonE7 */ public void moveWay(final Way way, final int deltaLatE7, final int deltaLonE7) { if (way.getNodes() == null) { Log.d("StorageDelegator", "moveWay way " + way.getOsmId() + " has no nodes!"); return; } dirty = true; try { HashSet<Node> nodes = new HashSet<Node>(way.getNodes()); // Guarantee uniqueness for (Node nd:nodes) { undo.save(nd); apiStorage.insertElementSafe(nd); nd.setLat(nd.getLat() + deltaLatE7); nd.setLon(nd.getLon() + deltaLonE7); nd.updateState(OsmElement.STATE_MODIFIED); } onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } /** * Move a list of nodes apply translation only once * @param allNodes * @param deltaLatE7 * @param deltaLonE7 */ public void moveNodes(final List<Node> allNodes, final int deltaLatE7, final int deltaLonE7) { if (allNodes == null) { Log.d("StorageDelegator", "moveNodes no nodes!"); return; } dirty = true; try { HashSet<Node> nodes = new HashSet<Node>(allNodes); // Guarantee uniqueness for (Node nd:nodes) { undo.save(nd); apiStorage.insertElementSafe(nd); nd.setLat(nd.getLat() + deltaLatE7); nd.setLon(nd.getLon() + deltaLonE7); nd.updateState(OsmElement.STATE_MODIFIED); } onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } /** * Arrange way nodes in a circle * * @param map current map view * @param center center of the circle * @param way way to circulize */ public void circulizeWay(@NotNull de.blau.android.Map map, int[] c, @NotNull Way way) { if ((way.getNodes() == null) || (way.getNodes().size()<3)) { Log.d("StorageDelegator", "circulize way " + way.getOsmId() + " has no nodes or less than 3!"); return; } dirty = true; try { HashSet<Node> nodes = new HashSet<Node>(way.getNodes()); // Guarantee uniqueness int width = map.getWidth(); int height = map.getHeight(); BoundingBox box = map.getViewBox(); Coordinates coords[] = nodeListToCooardinateArray(width, height, box, new ArrayList<Node>(nodes)); // save nodes for undo for (Node nd:nodes) { undo.save(nd); } Coordinates center = new Coordinates(GeoMath.lonE7ToX(width, box, c[1]), GeoMath.latE7ToY(height,width, box, c[0])); // caclulate average radius double r = 0.0f; for (Coordinates p:coords) { Log.d("StorageDelegator","r="+Math.sqrt((p.x-center.x)*(p.x-center.x)+(p.y-center.y)*(p.y-center.y))); r = r + Math.sqrt((p.x-center.x)*(p.x-center.x)+(p.y-center.y)*(p.y-center.y)); } r = r / coords.length; for (Coordinates p:coords) { double ratio = r/Math.sqrt((p.x-center.x)*(p.x-center.x)+(p.y-center.y)*(p.y-center.y)); p.x = (float) ((p.x-center.x) * ratio)+center.x; p.y = (float) ((p.y-center.y) * ratio)+center.y; } int i=0; for (Node nd:nodes) { nd.setLon(GeoMath.xToLonE7(width, box, coords[i].x)); nd.setLat(GeoMath.yToLatE7(height, width, box, coords[i].y)); apiStorage.insertElementSafe(nd); nd.updateState(OsmElement.STATE_MODIFIED); i++; } onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } /** * Build groups of ways that have common nodes * There must be a better way to do this, but they likely all fall afoul of our current data model * @param ways * @return */ private ArrayList<ArrayList<Way>> groupWays(List<Way> ways) { ArrayList<ArrayList<Way>> groups = new ArrayList<ArrayList<Way>>(); int group = 0; int index = 0; int groupIndex = 1; groups.add(new ArrayList<Way>()); Way startWay = ways.get(index); groups.get(group).add(startWay); do { do { for (Node nd:startWay.getNodes()) { for (Way w:ways) { if (w.getNodes().contains(nd) && !groups.get(group).contains(w)) { groups.get(group).add(w); } } } if (groupIndex < groups.get(group).size()) { startWay = groups.get(group).get(groupIndex); groupIndex++; } } while (groupIndex < groups.get(group).size()); // repeat until no new ways are added in the loop // find the next way that is not in a group and start a new one for (;index<ways.size();index++) { Way w = ways.get(index); boolean found = false; for (ArrayList<Way>list:groups) { found = found || list.contains(w); } if (!found) { group++; groups.add(new ArrayList<Way>()); startWay = w; groupIndex = 1; break; } } } while (index<ways.size()); Log.d(DEBUG_TAG,"number of groups found " + groups.size()); return groups; } /** * "square" a way/polygon, based on the algorithm used by iD and before that by P2, originally written by Matt Amos * If multiple ways are selected the ways are grouped in groups that share nodes and the groups individually squared. * * @param map current map view * @param way way to square */ public void orthogonalizeWay(@NotNull de.blau.android.Map map, @NotNull List<Way> ways) { final int threshold = 10; // degrees within right or straight to alter final double lowerThreshold = Math.cos((90 - threshold) * Math.PI / 180); final double upperThreshold = Math.cos(threshold * Math.PI / 180); final double epsilon = 1e-4; dirty = true; try { // save nodes for undo // adding to a Set first removes duplication HashSet<Node> save = new HashSet<Node>(); for (Way way:ways) { if (way.getNodes() != null) { save.addAll(way.getNodes()); } } for (Node nd:save) { undo.save(nd); } List<ArrayList<Way>> groups = groupWays(ways); int width = map.getWidth(); int height = map.getHeight(); BoundingBox box = map.getViewBox(); for (ArrayList<Way> wayList:groups) { // Coordinates coords[] = nodeListToCooardinateArray(nodes); ArrayList<Coordinates[]> coordsArray = new ArrayList<Coordinates[]>(); int totalNodes = 0; for (Way w:wayList) { coordsArray.add(nodeListToCooardinateArray(width, height, box, w.getNodes())); totalNodes += w.getNodes().size(); } Coordinates a, b, c, p, q; double loopEpsilon = epsilon*(totalNodes/4D); //NOTE the original algorithm didn't take the number of corners in to account // iterate until score is low enough for (int iteration = 0; iteration < 1000; iteration++) { for (int coordIndex=0;coordIndex<coordsArray.size();coordIndex++) { Coordinates[] coords = coordsArray.get(coordIndex); int start = 0; int end = coords.length; if (!wayList.get(coordIndex).isClosed()) { start = 1; end = end-1; } Coordinates motions[] = new Coordinates[coords.length]; for (int i=start;i<end;i++) { a = coords[(i - 1 + coords.length) % coords.length]; b = coords[i]; c = coords[(i + 1) % coords.length]; p = a.subtract(b); q = c.subtract(b); double scale = 2 * Math.min(Math.hypot(p.x,p.y), Math.hypot(q.x,q.y)); p = normalize(p, 1.0); q = normalize(q, 1.0); double dotp = filter((p.x * q.x + p.y * q.y), lowerThreshold, upperThreshold); // nasty hack to deal with almost-straight segments (angle is closer to 180 than to 90/270). if (dotp < -0.707106781186547) { dotp += 1.0; } motions[i] = normalize(p.add(q), 0.1 * dotp * scale); } // apply position changes for (int i=start;i<end;i++) { coords[i] = coords[i].add(motions[i]); } } // calculate score double score = 0.0; for (int coordIndex=0;coordIndex<coordsArray.size();coordIndex++) { Coordinates[] coords = coordsArray.get(coordIndex); int start = 0; int end = coords.length; if (!wayList.get(coordIndex).isClosed()) { start = 1; end = end-1; } for (int i=start;i<end;i++) { // yes I know that this -nearly- duplicates the code above a = coords[(i - 1 + coords.length) % coords.length]; b = coords[i]; c = coords[(i + 1) % coords.length]; p = a.subtract(b); q = c.subtract(b); p = normalize(p, 1.0); q = normalize(q, 1.0); double dotp = filter((p.x * q.x + p.y * q.y), lowerThreshold, upperThreshold); score = score + 2.0 * Math.min(Math.abs(dotp-1.0), Math.min(Math.abs(dotp), Math.abs(dotp+1.0))); } } // Log.d("StorageDelegator", "orthogonalize way iteration/score " + iteration + "/" + score); if (score < loopEpsilon) break; } // prepare updated nodes for upload for (int wayIndex=0;wayIndex<wayList.size();wayIndex++) { List<Node> nodes = wayList.get(wayIndex).getNodes(); Coordinates[] coords = coordsArray.get(wayIndex); for (int i = 0; i < nodes.size(); i++) { Node nd = nodes.get(i); // if (i == 0 || !nd.equals(firstNode)) { nd.setLon(GeoMath.xToLonE7(width, box, coords[i].x)); nd.setLat(GeoMath.yToLatE7(height, width, box, coords[i].y)); apiStorage.insertElementSafe(nd); nd.updateState(OsmElement.STATE_MODIFIED); // } } } } onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } // TODO move this and following code somewhere else and generalize private Coordinates normalize(Coordinates p, double scale) { Coordinates result = p; double length = p.length(); if (length != 0) { result = p.divide(length); } return result.multiply(scale); } private double filter(double v, double lower, double upper) { return (lower > Math.abs(v)) || (Math.abs(v) > upper) ? v : 0.0; } private class Coordinates { float x; float y; Coordinates (float x, float y) { this.x = x; this.y = y; } Coordinates subtract(Coordinates s) { return new Coordinates(this.x-s.x,this.y-s.y); } Coordinates add(Coordinates p) { return new Coordinates(this.x+p.x,this.y+p.y); } Coordinates multiply(double m) { return new Coordinates((float)(this.x*m),(float)(this.y*m)); } Coordinates divide(double d) { return new Coordinates((float)(this.x/d),(float)(this.y/d)); } float length() { return (float)Math.hypot(x, y); } } private Coordinates[] nodeListToCooardinateArray(int width, int height, BoundingBox box, List<Node>nodes) { Coordinates points[] = new Coordinates[nodes.size()]; //loop over all nodes for (int i=0;i<nodes.size();i++) { points[i] = new Coordinates(0.0f,0.0f); points[i].x = GeoMath.lonE7ToX(width, box, nodes.get(i).getLon()); points[i].y = GeoMath.latE7ToY(height, width, box, nodes.get(i).getLat()); } return points; } /** * Rotate all nodes in a way, since the nodes keep their ids, the way itself doesn't change and doesn't need to be saved * apply translation only once to the first node if way is closed. Rotation is done in screen coords * @param way * @param v * @param k * @param j * @param deltaLatE7 * @param deltaLonE7 */ public void rotateWay(final Way way, final float angle, final int direction, final float pivotX, final float pivotY, int w, int h, BoundingBox v) { if (way.getNodes() == null) { Log.d("StorageDelegator", "rotateWay way " + way.getOsmId() + " has no nodes!"); return; } // Log.d("StorageDelegator","Roating " + angle + " around " + pivotY + " " + pivotX ); dirty = true; try { HashSet<Node> nodes = new HashSet<Node>(way.getNodes()); // Guarantee uniqness for (Node nd:nodes) { undo.save(nd); apiStorage.insertElementSafe(nd); float nodeX = GeoMath.lonE7ToX(w, v, nd.getLon()); float nodeY = GeoMath.latE7ToY(h, w, v, nd.getLat()); float newX = pivotX + (nodeX-pivotX)*(float)Math.cos(angle) - direction * (nodeY-pivotY)*(float)Math.sin(angle); float newY = pivotY + direction * (nodeX-pivotX)*(float)Math.sin(angle) + (nodeY-pivotY)*(float)Math.cos(angle); int lat = GeoMath.yToLatE7(h, w, v, newY); int lon = GeoMath.xToLonE7(w, v, newX); nd.setLat(lat); nd.setLon(lon); nd.updateState(OsmElement.STATE_MODIFIED); } onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } /** * updated for relation support * @param node */ public void removeNode(final Node node) { // undo - node saved here, affected ways saved in removeWayNodes dirty = true; if (node.state == OsmElement.STATE_DELETED) { Log.d("StorageDelegator", "removeNode: nore already deleted " + node.getOsmId()); return; // node was already deleted } undo.save(node); try { if (node.state == OsmElement.STATE_CREATED) { apiStorage.removeElement(node); } else { apiStorage.insertElementSafe(node); } removeWayNodes(node); removeElementFromRelations(node); currentStorage.removeNode(node); node.updateState(OsmElement.STATE_DELETED); onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } public void splitAtNode(final Node node) { Log.d("StorageDelegator", "splitAtNode for all ways"); // undo - nothing done here, everything done in splitAtNode dirty = true; List<Way> ways = currentStorage.getWays(node); for (Way way : ways) { splitAtNode(way, node); } } /** * split a (closed) way at two points * @param way * @param node1 * @param node2 * @param createPolygons split in to two polygons * @return null if split failed or wasn't possible, the two resulting ways otherwise */ public Way[] splitAtNodes(Way way, Node node1, Node node2, boolean createPolygons) { Log.d("StorageDelegator", "splitAtNodes way " + way.getOsmId() + " node1 " + node1.getOsmId() + " node2 " + node2.getOsmId()); // undo - old way is saved here, new way is saved at insert dirty = true; undo.save(way); List<Node> nodes = way.getNodes(); if (nodes.size() < 3) { return null; } /* convention iterate over list, copy everything between first split node found and 2nd split node found * if 2nd split node found first the same */ List<Node> nodesForNewWay = new LinkedList<Node>(); List<Node> nodesForOldWay1 = new LinkedList<Node>(); List<Node> nodesForOldWay2 = new LinkedList<Node>(); boolean found1 = false; boolean found2 = false; for (Iterator<Node> it = way.getRemovableNodes(); it.hasNext();) { Node wayNode = it.next(); if (!found1 && wayNode.getOsmId() == node1.getOsmId()) { found1 = true; nodesForNewWay.add(wayNode); if (!found2) nodesForOldWay1.add(wayNode); else nodesForOldWay2.add(wayNode); } else if (!found2 && wayNode.getOsmId() == node2.getOsmId()) { found2 = true; nodesForNewWay.add(wayNode); if (!found1) nodesForOldWay1.add(wayNode); else nodesForOldWay2.add(wayNode); } else if ((found1 && !found2) || (!found1 && found2)) { nodesForNewWay.add(wayNode); } else if (!found1 && !found2) { nodesForOldWay1.add(wayNode); } else if (found1 && found2) { nodesForOldWay2.add(wayNode); } } // shuffle the nodes around for the original way so that they are in sequence and the way isn't closed Log.d("StorageDelegator","nodesForNewWay " + nodesForNewWay.size() + " oldNodes1 " + nodesForOldWay1.size() + " oldNodes2 " + nodesForOldWay2.size()); List<Node> oldNodes = way.getNodes(); oldNodes.clear(); if (nodesForOldWay1.size() == 0) { oldNodes.addAll(nodesForOldWay2); } else if (nodesForOldWay2.size() == 0) { oldNodes.addAll(nodesForOldWay1); } else if (nodesForOldWay1.get(0) == nodesForOldWay2.get(nodesForOldWay2.size()-1)) { oldNodes.addAll(nodesForOldWay2); nodesForOldWay1.remove(0); oldNodes.addAll(nodesForOldWay1); } else { oldNodes.addAll(nodesForOldWay1); nodesForOldWay2.remove(0); oldNodes.addAll(nodesForOldWay2); } try { if (createPolygons && way.length() > 2) { // close the original way now way.addNode(way.getFirstNode()); } way.updateState(OsmElement.STATE_MODIFIED); apiStorage.insertElementSafe(way); // create the new way Way newWay = factory.createWayWithNewId(); newWay.addTags(way.getTags()); newWay.addNodes(nodesForNewWay, false); if (createPolygons && newWay.length() > 2) { // close the new way now newWay.addNode(newWay.getFirstNode()); } insertElementUnsafe(newWay); // check for relation membership if (way.hasParentRelations()) { ArrayList<Relation> relations = new ArrayList<Relation>(way.getParentRelations()); // copy ! dirty = true; /* iterate through relations, add the new way to the relation, for now simply after the old way */ for (Relation r : relations) { Log.d("StorageDelegator", "splitAtNode processing relation (#" + r.getOsmId() + "/" + relations.size() + ") " + r.getDescription()); RelationMember rm = r.getMember(way); undo.save(r); // no role specific code for now RelationMember newMember = new RelationMember(rm.getRole(), newWay); r.addMemberAfter(rm, newMember); newWay.addParentRelation(r); r.updateState(OsmElement.STATE_MODIFIED); apiStorage.insertElementSafe(r); } } onElementChanged(null, null); Way[] result = new Way[2]; result[0] = way; result[1] = newWay; return result; } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } return null; } /** * split way at node with relation support * @param way * @param node */ public Way splitAtNode(final Way way, final Node node) { Log.d("StorageDelegator", "splitAtNode way " + way.getOsmId() + " node " + node.getOsmId()); // undo - old way is saved here, new way is saved at insert dirty = true; undo.save(way); List<Node> nodes = way.getNodes(); int occurances = Collections.frequency(way.getNodes(), node); // the following condition is fairly obscure and should likely be replaced by checking for position of the node in the way if (nodes.size() < 3 || (way.isEndNode(node) && (way.isClosed()?occurances==2:occurances==1))) { // protect against producing single node ways FIXME give feedback that this is not good Log.d("StorageDelegator", "splitAtNode can't split " + nodes.size() + " node long way at this node"); return null; } // we assume this node is only contained in the way once. // else the user needs to split the remaining way again. List<Node> nodesForNewWay = new LinkedList<Node>(); boolean found = false; boolean first = true; // node to split at can't be the first one for (Iterator<Node> it = way.getRemovableNodes(); it.hasNext();) { Node wayNode = it.next(); if (!found && wayNode.getOsmId() == node.getOsmId() && !first) { found = true; nodesForNewWay.add(wayNode); } else if (found) { nodesForNewWay.add(wayNode); it.remove(); } first = false; } if (nodesForNewWay.size() <= 1) { Log.d("StorageDelegator", "splitAtNode can't split, new way would have " + nodesForNewWay.size() + " node(s)"); return null; // do not create 1-node way } try { way.updateState(OsmElement.STATE_MODIFIED); apiStorage.insertElementSafe(way); // create the new way Way newWay = factory.createWayWithNewId(); newWay.addTags(way.getTags()); newWay.addNodes(nodesForNewWay, false); insertElementUnsafe(newWay); // check for relation membership if (way.getParentRelations() != null) { ArrayList<Relation> relations = new ArrayList<Relation>(way.getParentRelations()); // copy ! dirty = true; /* iterate through relations, for all except restrictions add the new way to the relation, for now simply after the old way */ for (Relation r : relations) { Log.d("StorageDelegator", "splitAtNode processing relation (#" + r.getOsmId() + "/" + relations.size() + ") " + r.getDescription()); RelationMember rm = r.getMember(way); if (rm == null) { Log.d("StorageDelegator", "Unconsistent state detected way " + way.getOsmId() + " should be relation member" ); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(null); continue; } undo.save(r); String type = r.getTagWithKey(Tags.KEY_TYPE); if (type != null){ // attempt to handle turn restrictions correctly, if element is the via way, copying relation membership to both is ok if (type.equals(Tags.VALUE_RESTRICTION) && !rm.getRole().equals(Tags.VALUE_VIA)) { // check if the old way has a node in common with the via relation member, if no assume the new way has ArrayList<RelationMember> rl = r.getMembersWithRole(Tags.VALUE_VIA); boolean foundVia=false; for (int j=0;j<rl.size();j++) { RelationMember viaRm = rl.get(j); OsmElement viaE = viaRm.getElement(); Log.d("StorageDelegator", "splitAtNode " + viaE.getOsmId()); if (viaE instanceof Node) { if (((Way)rm.getElement()).hasNode((Node)viaE)) { foundVia = true; } } else if (viaE instanceof Way) { if (((Way)rm.getElement()).hasCommonNode((Way)viaE)) { foundVia = true; } } } Log.d("StorageDelegator", "splitAtNode foundVia " + foundVia); if (!foundVia) { // remove way from relation, add newWay to it RelationMember newMember = new RelationMember(rm.getRole(), newWay); r.replaceMember(rm, newMember); way.removeParentRelation(r); // way is dirty and will be changes anyway newWay.addParentRelation(r); } } else { RelationMember newMember = new RelationMember(rm.getRole(), newWay); r.addMemberAfter(rm, newMember); newWay.addParentRelation(r); } } else { RelationMember newMember = new RelationMember(rm.getRole(), newWay); r.addMemberAfter(rm, newMember); newWay.addParentRelation(r); } r.updateState(OsmElement.STATE_MODIFIED); apiStorage.insertElementSafe(r); } } onElementChanged(null, null); return newWay; } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); return null; } } /** * Merge two nodes into one. * Updated for relation support * @param mergeInto The node to merge into. Tags are combined. * @param mergeFrom The node to merge from. Is deleted. */ public boolean mergeNodes(Node mergeInto, Node mergeFrom) { boolean mergeOK = true; dirty = true; // first determine if one of the nodes already has a valid id, if it is not and other node has valid id swap // else check version numbers this helps preserve history if (((mergeInto.getOsmId() < 0) && (mergeFrom.getOsmId() > 0)) || mergeInto.getOsmVersion() < mergeFrom.getOsmVersion()) { // swap Log.d("StorageDelegator", "swap into #" + mergeInto.getOsmId() + " with from #" + mergeFrom.getOsmId()); Node tmpNode = mergeInto; mergeInto = mergeFrom; mergeFrom = tmpNode; Log.d("StorageDelegator", "now into #" + mergeInto.getOsmId() + " from #" + mergeFrom.getOsmId()); } mergeOK = !roleConflict(mergeInto, mergeFrom); // need to do this before we remove objects from relations. // merge tags setTags(mergeInto, OsmElement.mergedTags(mergeInto, mergeFrom)); // if merging the tags creates multiple-value tags, mergeOK should be set to false for (String v:mergeInto.getTags().values()) { if (v.indexOf(";") >= 0) { mergeOK = false; break; } } // replace references to mergeFrom node in ways with mergeInto for (Way way : currentStorage.getWays(mergeFrom)) { replaceNodeInWay(mergeFrom, mergeInto, way); } // belt and suspenders not really necessary for (Way way : apiStorage.getWays(mergeFrom)) { replaceNodeInWay(mergeFrom, mergeInto, way); } mergeElementsRelations(mergeInto, mergeFrom); // delete mergeFrom node removeNode(mergeFrom); onElementChanged(null, null); return mergeOK; } /** * Merges two ways by prepending/appending all nodes from the second way to the first one, then deleting the second one. * * Updated for relation support if roles are not the same the merge will fail. * @param mergeInto Way to merge the other way into. This way will be kept if it has a valid id. * @param mergeFrom Way to merge into the other. * @return false if we had tag conflicts * @throws OsmIllegalOperationException */ public boolean mergeWays(Way mergeInto, Way mergeFrom) throws OsmIllegalOperationException { boolean mergeOK = true; if ((mergeInto.nodeCount() + mergeFrom.nodeCount()) > Way.maxWayNodes) throw new OsmIllegalOperationException(App.resources().getString(R.string.exception_too_many_nodes)); // first determine if one of the nodes already has a valid id, if it is not and other node has valid id swap // else check version numbers this helps preserve history if (((mergeInto.getOsmId() < 0) && (mergeFrom.getOsmId() > 0)) || mergeInto.getOsmVersion() < mergeFrom.getOsmVersion()) { // swap Log.d("StorageDelegator", "swap into #" + mergeInto.getOsmId() + " with from #" + mergeFrom.getOsmId()); Way tmpWay = mergeInto; mergeInto = mergeFrom; mergeFrom = tmpWay; Log.d("StorageDelegator", "now into #" + mergeInto.getOsmId() + " from #" + mergeFrom.getOsmId()); } mergeOK = !roleConflict(mergeInto, mergeFrom); // need to do this before we remove ways from relations. // undo - mergeInto way saved here, mergeFrom way will not be changed directly and will be saved in removeWay dirty = true; undo.save(mergeInto); removeWay(mergeFrom); // have to do this here because otherwise the way will be saved with potentially reversed tags List<Node> newNodes = new ArrayList<Node>(mergeFrom.getNodes()); boolean atBeginning; if (mergeInto.getFirstNode().equals(mergeFrom.getFirstNode())) { // Result: f3 f2 f1 (f0=)i0 i1 i2 i3 (f0 = 0th node of mergeFrom, i1 = 1st node of mergeInto) atBeginning = true; //check for direction dependent tags Map<String, String> dirTags = Reverse.getDirectionDependentTags(mergeFrom); if (dirTags != null) { Reverse.reverseDirectionDependentTags(mergeFrom,dirTags, true); } mergeOK = !mergeFrom.notReversable(); Collections.reverse(newNodes); newNodes.remove(newNodes.size()-1); // remove "last" (originally first) node after reversing reverseWayNodeTags(newNodes); } else if (mergeInto.getLastNode().equals(mergeFrom.getFirstNode())) { // Result: i0 i1 i2 i3(=f0) f1 f2 f3 atBeginning = false; newNodes.remove(0); } else if (mergeInto.getFirstNode().equals(mergeFrom.getLastNode())) { // Result: f0 f1 f2 (f3=)i0 i1 i2 i3 atBeginning = true; newNodes.remove(newNodes.size()-1); } else if (mergeInto.getLastNode().equals(mergeFrom.getLastNode())) { // Result: i0 i1 i2 i3(=f3) f2 f1 f0 atBeginning = false; //check for direction dependent tags Map<String, String> dirTags = Reverse.getDirectionDependentTags(mergeFrom); if (dirTags != null) { Reverse.reverseDirectionDependentTags(mergeFrom, dirTags, true); } mergeOK = !mergeFrom.notReversable(); newNodes.remove(newNodes.size()-1); // remove last node before reversing reverseWayNodeTags(newNodes); Collections.reverse(newNodes); } else { throw new RuntimeException("attempted to merge non-mergeable nodes. this is a bug."); } // merge tags (after any reversal has been done) setTags(mergeInto, OsmElement.mergedTags(mergeInto, mergeFrom)); // if merging the tags creates multiple-value tags, mergeOK should be set to false for (String v:mergeInto.getTags().values()) { if (v.indexOf(";") >= 0) { mergeOK = false; break; } } mergeInto.addNodes(newNodes, atBeginning); mergeInto.updateState(OsmElement.STATE_MODIFIED); insertElementSafe(mergeInto); mergeElementsRelations(mergeInto, mergeFrom); return mergeOK; } /** * reverse any direction dependent tags on the way nodes * @param nodes */ private void reverseWayNodeTags(List<Node> nodes) { for (Node n:nodes) { Map<String,String> nodeDirTags = Reverse.getDirectionDependentTags(n); if (nodeDirTags!=null) { undo.save(n); Reverse.reverseDirectionDependentTags(n,nodeDirTags, true); n.updateState(OsmElement.STATE_MODIFIED); try { apiStorage.insertElementSafe(n); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } } } /** * return true if elements have different roles in the same relation * @param o1 * @param o2 * @return */ private boolean roleConflict(OsmElement o1, OsmElement o2) { ArrayList<Relation> r1 = o1.getParentRelations() != null ? o1.getParentRelations() : new ArrayList<Relation>(); ArrayList<Relation> r2 = o2.getParentRelations() != null ? o2.getParentRelations() : new ArrayList<Relation>(); for (Relation r : r1) { if (r2.contains(r)) { RelationMember rm1 = r.getMember(o1); RelationMember rm2 = r.getMember(o2); if (rm1 != null && rm2 != null) { // if either of these are null something is broken String role1 = rm1.getRole(); String role2 = rm2.getRole(); //noinspection StringEquality if ((role1 != null && role2 == null) || (role1 == null && role2 != null) || (role1 != role2 && !role1.equals(role2))) { Log.d(DEBUG_TAG,"role conflict between " + o1.getDescription() + " role " + role1 + " and " + o2.getDescription() + " role " + role2); return true; } } else { Log.d(DEBUG_TAG,"inconsistent relation membership in " + r.getOsmId() + " for " + o1.getOsmId() + " and " + o2.getOsmId()); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(null); return true; } } } return false; } /** * Unjoins ways connected at the given node. * Updated for relation support * @param node The node connecting ways that are to be unjoined. */ /** * @param node */ public void unjoinWays(final Node node) { List<Way> ways = currentStorage.getWays(node); try { if (ways.size() > 1) { boolean first = true; for (Way way : ways) { if (first) { // first way doesn't need to be changed first = false; } else { // subsequent ways dirty = true; // create a new node that duplicates the given node Node newNode = factory.createNodeWithNewId(node.lat, node.lon); newNode.addTags(node.getTags()); insertElementUnsafe(newNode); // replace the given node in the way with the new node undo.save(way); List<Node> nodes = way.getNodes(); nodes.set(nodes.indexOf(node), newNode); way.updateState(OsmElement.STATE_MODIFIED); apiStorage.insertElementSafe(way); // check if node is in a relation, if yes, add to new node // should probably check for restrictions if (node.hasParentRelations()) { ArrayList<Relation> relations = node.getParentRelations(); /* iterate through relations, for all except restrictions add the new node to the relation, for now simply after the old node */ for (Relation r : relations) { RelationMember rm = r.getMember(node); undo.save(r); String type = r.getTagWithKey(Tags.KEY_TYPE); if (type != null){ if (type.equals(Tags.VALUE_RESTRICTION)) { // doing nothing for now at least gives a chance of being right :-) } else { RelationMember newMember = new RelationMember(rm.getRole(), newNode); r.addMemberAfter(rm, newMember); newNode.addParentRelation(r); } } else { RelationMember newMember = new RelationMember(rm.getRole(), newNode); r.addMemberAfter(rm, newMember); newNode.addParentRelation(r); } r.updateState(OsmElement.STATE_MODIFIED); apiStorage.insertElementSafe(r); } } } } onElementChanged(null, null); } } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } /** * Replace the given node in any ways it is member of. * @param node The node to be replaced. * @return null if node was not member of a way, the replacement node if it was */ public Node replaceNode(final Node node) { List<Way> ways = currentStorage.getWays(node); if (ways.size() > 0) { Node newNode = factory.createNodeWithNewId(node.lat, node.lon); insertElementUnsafe(newNode); dirty = true; for (Way way : ways) { replaceNodeInWay(node, newNode, way); } return newNode; } return null; } /** * Reverses a way * @param way * @return true is way had tags that needed to be reversed */ public boolean reverseWay(final Way way) { dirty = true; undo.save(way); //check for direction dependent tags Map<String, String> dirTags = Reverse.getDirectionDependentTags(way); //TODO inform user about the tags if (dirTags != null) { Reverse.reverseDirectionDependentTags(way, dirTags, false); // assume he only wants to change the oneway direction for now } reverseWayNodeTags(way.getNodes()); way.reverse(); List<Relation>relations = Reverse.getRelationsWithDirectionDependentRoles(way); if (relations != null) { Reverse.reverseRoleDirection(way,relations); for (Relation r:relations) { r.updateState(OsmElement.STATE_MODIFIED); try { apiStorage.insertElementSafe(r); } catch (StorageException e) { // TODO handle OOM e.printStackTrace(); } } } way.updateState(OsmElement.STATE_MODIFIED); try { apiStorage.insertElementSafe(way); onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } return ((dirTags != null) && dirTags.containsKey(Tags.KEY_ONEWAY)); } private void replaceNodeInWay(final Node existingNode, final Node newNode, final Way way) { dirty = true; undo.save(way); way.replaceNode(existingNode, newNode); way.updateState(OsmElement.STATE_MODIFIED); try { apiStorage.insertElementSafe(way); onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } private int removeWayNodes(final Node node) { // undo - node is not changed, affected way(s) are stored below dirty = true; int deleted = 0; try { List<Way> ways = currentStorage.getWays(node); for (Way way:ways) { undo.save(way); if (way.isClosed() && way.isEndNode(node) && way.getNodes().size() > 1) { // note protection against degenerate closed ways way.removeNode(node); way.addNode(way.getFirstNode()); } else { way.removeNode(node); } //remove way when less than two waynodes exist if (way.getNodes().size() < 2) { removeWay(way); } else { way.updateState(OsmElement.STATE_MODIFIED); apiStorage.insertElementSafe(way); } deleted++; } onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } return deleted; } /** * Deletes a way * updated for relations * @param way */ public void removeWay(final Way way) { dirty = true; undo.save(way); try { currentStorage.removeWay(way); if (apiStorage.contains(way)) { if (way.getState() == OsmElement.STATE_CREATED) { apiStorage.removeElement(way); } } else { apiStorage.insertElementSafe(way); } removeElementFromRelations(way); way.updateState(OsmElement.STATE_DELETED); onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } /** * updated for relation support * @param node */ public void removeRelation(final Relation relation) { // undo - node saved here, affected ways saved in removeWayNodes dirty = true; undo.save(relation); try { if (relation.state == OsmElement.STATE_CREATED) { apiStorage.removeElement(relation); } else { apiStorage.insertElementSafe(relation); } removeElementFromRelations(relation); removeRelationFromMembers(relation); currentStorage.removeRelation(relation); relation.updateState(OsmElement.STATE_DELETED); onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } /** * Remove backlinks in elements * @param relation */ private void removeRelationFromMembers(final Relation relation) { for (RelationMember rm: relation.getMembers()) { OsmElement e = rm.getElement(); if (e != null) { // if null the element wasn't downloaded undo.save(e); e.removeParentRelation(relation); } } } /** * Note the element does not need to have its state changed of be stored in the API sotrage since the * parent relation back link is just internal. * @param element */ private void removeElementFromRelations(final OsmElement element) { try { if (element.hasParentRelations()) { ArrayList<Relation> relations = new ArrayList<Relation>(element.getParentRelations()); // need copy! for (Relation r : relations) { Log.i("StorageDelegator", "removing " + element.getName() + " #" + element.getOsmId() + " from relation #" + r.getOsmId()); dirty = true; undo.save(r); r.removeMember(r.getMember(element)); r.updateState(OsmElement.STATE_MODIFIED); apiStorage.insertElementSafe(r); undo.save(element); element.removeParentRelation(r); Log.i("StorageDelegator", "... done"); } onElementChanged(null, null); } } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } /** * Note the element does not need to have its state changed of be stored in the API storage since the * parent relation back link is just internal. * @param element */ private void removeElementFromRelation(final OsmElement element, Relation r) { Log.i("StorageDelegator", "removing " + element.getName() + " #" + element.getOsmId() + " from relation #" + r.getOsmId()); dirty = true; undo.save(r); try { r.removeMember(r.getMember(element)); r.updateState(OsmElement.STATE_MODIFIED); apiStorage.insertElementSafe(r); undo.save(element); element.removeParentRelation(r); Log.i("StorageDelegator", "... done"); onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } } /* * remove non-downloaded element from relation */ public void removeElementFromRelation(String type, final Long elementId, Relation r) { Log.i("StorageDelegator", "removing #" + elementId + " from relation #" + r.getOsmId()); dirty = true; undo.save(r); r.removeMember(r.getMember(type, elementId)); r.updateState(OsmElement.STATE_MODIFIED); try { apiStorage.insertElementSafe(r); onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } // Log.i("StorageDelegator", "... done"); } /** * add element to relation at a specific position * @param e * @param pos * @param role * @param rel */ private void addElementToRelation(final OsmElement e, final int pos, final String role, final Relation rel) { dirty = true; undo.save(rel); undo.save(e); RelationMember newMember = new RelationMember(role, e); rel.addMember(pos, newMember); e.addParentRelation(rel); rel.updateState(OsmElement.STATE_MODIFIED); try { apiStorage.insertElementSafe(rel); onElementChanged(null, null); } catch (StorageException sex) { //TODO handle OOM sex.printStackTrace(); } } /** * add element to relation at end * @param e * @param role * @param rel */ public void addElementToRelation(final OsmElement e, final String role, final Relation rel) { dirty = true; undo.save(rel); undo.save(e); RelationMember newMember = new RelationMember(role, e); rel.addMember(newMember); e.addParentRelation(rel); rel.updateState(OsmElement.STATE_MODIFIED); try { apiStorage.insertElementSafe(rel); onElementChanged(null, null); } catch (StorageException sex) { //TODO handle OOM sex.printStackTrace(); } } /** * add element to relation at end * @param e * @param role * @param rel */ public void addElementToRelation(final RelationMember newMember, final Relation rel) { OsmElement e = newMember.getElement(); if (e == null) { Log.e(DEBUG_TAG, "addElementToRelation element not found"); return; } dirty = true; undo.save(rel); undo.save(e); rel.addMember(newMember); e.addParentRelation(rel); rel.updateState(OsmElement.STATE_MODIFIED); try { apiStorage.insertElementSafe(rel); onElementChanged(null, null); } catch (StorageException sex) { //TODO handle OOM sex.printStackTrace(); } } /** * set role for e in relation rel to new value role * @param e * @param role * @param rel */ private void setRole(final OsmElement e, final String role, final Relation rel) { dirty = true; undo.save(rel); RelationMember oldRm = rel.getMember(e); RelationMember rm = new RelationMember(oldRm); // necessary or else we will overwrite the role string in undo storage rm.setRole(role); rel.replaceMember(oldRm, rm); rel.updateState(OsmElement.STATE_MODIFIED); try { apiStorage.insertElementSafe(rel); onElementChanged(null, null); } catch (StorageException sex) { //TODO handle OOM sex.printStackTrace(); } Log.w("StorageDelegator", "set role for #" + e.getOsmId() + " to " + role + " in relation #" + rel.getOsmId()); } /** * set role for e in relation rel to new value role * @param e * @param role * @param rel */ public void setRole(final String type, final long elementId, final String role, final Relation rel) { dirty = true; undo.save(rel); RelationMember oldRm = rel.getMember(type, elementId); RelationMember rm = new RelationMember(oldRm); // necessary or else we will overwrite the role string in undo storage rm.setRole(role); rel.replaceMember(oldRm, rm); rel.updateState(OsmElement.STATE_MODIFIED); try { apiStorage.insertElementSafe(rel); onElementChanged(null, null); } catch (StorageException e) { //TODO handle OOM e.printStackTrace(); } Log.w("StorageDelegator", "set role for #" + elementId+ " to " + role + " in relation #" + rel.getOsmId()); } /** * compare current relations e is a member of to new state parents and make it so * @param e * @param parents */ public void updateParentRelations(final OsmElement e, final HashMap<Long, String> parents) { Log.d(DEBUG_TAG,"updateParentRelations new parents size " + parents.size()); ArrayList<Relation> origParents = e.getParentRelations() != null ? new ArrayList<Relation>(e.getParentRelations()) : new ArrayList<Relation>(); for (Relation o: origParents) { // find changes to existing memberships if (!parents.containsKey(Long.valueOf(o.getOsmId()))) { removeElementFromRelation(e, o); // saves undo state continue; } if (parents.containsKey(Long.valueOf(o.getOsmId()))){ String newRole = parents.get(Long.valueOf(o.getOsmId())); if (!o.getMember(e).getRole().equals(newRole)) { setRole(e, newRole, o); } } } // add as new member to relation for (Long l : parents.keySet()) { Log.d(DEBUG_TAG,"updateParentRelations new parent " + l.longValue()); if (l.longValue() != -1) { // Relation r = (Relation) currentStorage.getOsmElement(Relation.NAME, l.longValue()); if (!origParents.contains(r)) { Log.d(DEBUG_TAG,"updateParentRelations adding " + e.getDescription() + " to " + r.getDescription()); addElementToRelation(e, -1, parents.get(l), r); // append for now only } } } } /** * compare current list of relations members to new list and apply the necessary changes * currently doesn't handle additions or changes in sequence * @param r the relation * @param members new list of members */ public void updateRelation(Relation r, ArrayList<RelationMemberDescription> members) { dirty = true; undo.save(r); boolean changed = false; ArrayList<RelationMember> origMembers = new ArrayList<RelationMember>(r.getMembers()); LinkedHashMap<String,RelationMember> membersHash = new LinkedHashMap<String,RelationMember>(); for (RelationMember rm: r.getMembers()) { membersHash.put(rm.getType()+"-"+rm.getRef(),rm); } ArrayList<RelationMember> newMembers = new ArrayList<RelationMember>(); for (int i = 0; i < members.size(); i++) { RelationMemberDescription rmd = members.get(i); String key = rmd.getType()+"-"+rmd.getRef(); OsmElement e = rmd.getElement(); RelationMember rm = membersHash.get(key); if (rm != null) { int origPos = origMembers.indexOf(rm); String newRole = rmd.getRole(); if (!rm.getRole().equals(newRole)) { changed = true; rm = new RelationMember(rm); // allocate new element rm.setRole(newRole); } newMembers.add(rm); // existing member simply add to list if (origPos != i) { changed = true; } membersHash.remove(key); } else { // new member changed = true; RelationMember newMember = null; if (e != null) { // downloaded newMember = new RelationMember(rmd.getRole(), e); } else { newMember = new RelationMember(rmd.getType(), rmd.getRef(), rmd.getRole()); } newMembers.add(newMember); } } for (RelationMember rm: membersHash.values()) { changed = true; OsmElement e = rm.getElement(); if (e != null) { undo.save(e); e.removeParentRelation(r); } } if (changed) { r.replaceMembers(newMembers); r.updateState(OsmElement.STATE_MODIFIED); try { apiStorage.insertElementSafe(r); onElementChanged(null, null); } catch (StorageException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else { // FIXME remove relation from undo storage } } /** * Add further members without role to an existing relation * @param relation * @param members */ public void addMembersToRelation(Relation relation, ArrayList<OsmElement> members) { dirty = true; for (OsmElement e:members) { undo.save(e); RelationMember rm = new RelationMember("", e); relation.addMember(rm); e.addParentRelation(relation); } relation.updateState(OsmElement.STATE_MODIFIED); insertElementSafe(relation); } /** * Assumes mergeFrom will deleted by caller and doesn't update back refs * @param mergeInto * @param mergeFrom */ private void mergeElementsRelations(final OsmElement mergeInto, final OsmElement mergeFrom) { ArrayList<Relation> fromRelations = mergeFrom.getParentRelations() != null ? new ArrayList<Relation>(mergeFrom.getParentRelations()) : new ArrayList<Relation>(); // copy just to be safe ArrayList<Relation> toRelations = mergeInto.getParentRelations() != null ? mergeInto.getParentRelations() : new ArrayList<Relation>(); try { for (Relation r : fromRelations) { if (!toRelations.contains(r)) { dirty = true; undo.save(r); RelationMember rm = r.getMember(mergeFrom); // create new member with same role RelationMember newRm = new RelationMember(rm.getRole(), mergeInto); // insert at same place r.replaceMember(rm, newRm); r.updateState(OsmElement.STATE_MODIFIED); apiStorage.insertElementSafe(r); mergeInto.addParentRelation(r); mergeInto.updateState(OsmElement.STATE_MODIFIED); apiStorage.insertElementSafe(mergeInto); } } onElementChanged(null, null); } catch (StorageException sex) { //TODO handle OOM } } /** * Make a copy of the element and store it in the clipboard * @param e element to copy * @param lat latitude where it was located * @param lon longitude where it was located */ public void copyToClipboard(OsmElement e, int lat, int lon) { dirty = true; // otherwise clipboard will not get saved without other changes if (e instanceof Node) { Node newNode = factory.createNodeWithNewId(((Node) e).getLat(), ((Node) e).getLon()); newNode.setTags(e.getTags()); clipboard.copyTo(newNode, lat, lon); } else if (e instanceof Way) { Way newWay = factory.createWayWithNewId(); newWay.setTags(e.getTags()); for (Node nd: ((Way)e).getNodes()) { Node newNode = factory.createNodeWithNewId(nd.getLat(), nd.getLon()); newNode.setTags(nd.getTags()); newWay.addNode(nd); } clipboard.copyTo(newWay, lat, lon); } } /** * Cut original element to clipboard, does -not- preserve relation memberships * @param e element to copy * @param lat latitude where it was located * @param lon longitude where it was located */ public void cutToClipboard(OsmElement e, int lat, int lon) { dirty = true; // otherwise clipboard will not get saved without other changes if (e instanceof Node) { clipboard.cutTo(e, lat, lon); removeNode((Node)e); } else if (e instanceof Way) { // clone all nodes that are members of other ways ArrayList<Node> nodes = new ArrayList<Node>(((Way)e).getNodes()); for (Node nd: nodes) { if (currentStorage.getWays(nd).size() > 1) { // 1 is expected (our way will be deleted later) Log.d("StorageDelegator","Duplicating node"); Node newNode = factory.createNodeWithNewId(nd.getLat(), nd.getLon()); newNode.setTags(nd.getTags()); ((Way)e).replaceNode(nd, newNode); } } clipboard.cutTo(e, lat, lon); removeWay((Way)e); nodes = new ArrayList<Node>(((Way)e).getNodes()); for (Node nd: nodes) { removeNode(nd); // } } } public boolean pasteFromClipboard(int lat, int lon) { OsmElement e = clipboard.pasteFrom(); // if the clipboard isn't empty now we need to clone the element if (!clipboard.isEmpty()) { // paste from copy if (e instanceof Node) { Node newNode = factory.createNodeWithNewId(lat, lon); newNode.setTags(e.getTags()); insertElementSafe(newNode); } else if (e instanceof Way) { Way newWay = factory.createWayWithNewId(); newWay.setTags(e.getTags()); int deltaLat = lat - clipboard.getSelectionLat(); int deltaLon = lon - clipboard.getSelectionLon(); List<Node>nodeList = ((Way)e).getNodes(); // this is slightly complicated because we need to handle cases with potentially broken geometry // allocate and set the position of the new nodes Set<Node> nodes = new HashSet<Node>(nodeList); HashMap<Node,Node>newNodes = new HashMap<Node,Node>(); for (Node nd:nodes) { Node newNode = factory.createNodeWithNewId(nd.getLat() + deltaLat, nd.getLon() + deltaLon); newNode.setTags(nd.getTags()); newNode.setLat(nd.getLat() + deltaLat); newNode.setLon(nd.getLon() + deltaLon); newNode.updateState(OsmElement.STATE_MODIFIED); insertElementSafe(newNode); newNodes.put(nd, newNode); } // now add them to the new way for (Node nd:nodeList) { newWay.addNode(newNodes.get(nd)); } insertElementSafe(newWay); } } else { // paste from cut if (e instanceof Node) { ((Node)e).setLat(lat); ((Node)e).setLon(lon); } else if (e instanceof Way) { int deltaLat = lat - clipboard.getSelectionLat(); int deltaLon = lon - clipboard.getSelectionLon(); Set<Node> nodes = new HashSet<Node>(((Way)e).getNodes()); for (Node nd:nodes) { nd.setLat(nd.getLat() + deltaLat); nd.setLon(nd.getLon() + deltaLon); nd.updateState(OsmElement.STATE_MODIFIED); insertElementSafe(nd); } } e.updateState(OsmElement.STATE_MODIFIED); insertElementSafe(e); } return e != null; } public boolean clipboardIsEmpty() { return clipboard.isEmpty(); } public Storage getApiStorage() { return apiStorage; } public Storage getCurrentStorage() { return currentStorage; } // public BoundingBox getOriginalBox() { // return currentStorage.getBoundingBox().copy(); // } public List<BoundingBox> getBoundingBoxes() { // TODO make a copy? return currentStorage.getBoundingBoxes(); } public void setOriginalBox(final BoundingBox box) { dirty = true; currentStorage.setBoundingBox(box); } public void addBoundingBox(BoundingBox box) { dirty = true; currentStorage.addBoundingBox(box); } public void deleteBoundingBox(BoundingBox box) { dirty = true; currentStorage.deleteBoundingBox(box); } public int getApiNodeCount() { return apiStorage.getNodes().size(); } public int getApiWayCount() { return apiStorage.getWays().size(); } public int getApiRelationCount() { return apiStorage.getRelations().size(); } /** * Get the total number of elements in API storage * <p> * Returns the total number of elements to be created, modified or deleted * @return the element count */ public int getApiElementCount() { return apiStorage.getRelations().size() + apiStorage.getWays().size() + apiStorage.getNodes().size(); } public OsmElement getOsmElement(final String type, final long osmId) { OsmElement elem = apiStorage.getOsmElement(type, osmId); if (elem == null) { elem = currentStorage.getOsmElement(type, osmId); } return elem; } public boolean hasChanges() { return !apiStorage.isEmpty(); } public boolean isEmpty() { return currentStorage.isEmpty() && apiStorage.isEmpty(); } /** * Stores the current storage data to the default storage file * @param ctx TODO * @throws IOException */ public void writeToFile(Context ctx) throws IOException { if (apiStorage == null || currentStorage == null) { // don't write empty state files Log.i("StorageDelegator", "storage delegator empty, skipping save"); return; } if (!dirty) { // dirty flag should only be set if we have actually read/loaded/changed something Log.i("StorageDelegator", "storage delegator not dirty, skipping save"); return; } if (readingLock.tryLock()) { // TODO this doesn't really help with error conditions need to throw exception if (savingHelper.save(ctx, FILENAME, this, true)) { dirty = false; } else { // this is essentially catastrophic and can only happen if something went really wrong // running out of memory or disk, or HW failure if (ctx != null && ctx instanceof Activity) { try { Snack.barError((Activity)ctx, R.string.toast_statesave_failed); } catch (Exception ignored) { Log.e(DEBUG_TAG,"Emergency toast failed with " + ignored.getMessage()); } catch (Error ignored) { Log.e(DEBUG_TAG,"Emergency toast failed with " + ignored.getMessage()); } } SavingHelper.asyncExport(ctx, this); // ctx == null is checked in method Log.d("StorageDelegator", "save of state file failed, written emergency change file" ); } readingLock.unlock(); } else { Log.i("StorageDelegator", "storage delegator state being read, skipping save"); } } /** * Read save data from standard file * Loads the storage data from the default storage file * NOTE: lock is acquired in logic before this is called */ public boolean readFromFile(Context context) { return readFromFile(context, FILENAME); } /** * Read save data from file * @param filename * @return */ public boolean readFromFile(Context context, String filename) { try{ lock(); StorageDelegator newDelegator = savingHelper.load(context, filename, true); if (newDelegator != null) { Log.d("StorageDelegator", "read saved state"); currentStorage = newDelegator.currentStorage; if (currentStorage.getBoundingBoxes() == null) { // can happen if data was added before load try { currentStorage.setBoundingBox(currentStorage.calcBoundingBoxFromData()); } catch (OsmException e) { // TODO Auto-generated catch block e.printStackTrace(); } } apiStorage = newDelegator.apiStorage; undo = newDelegator.undo; clipboard = newDelegator.clipboard; factory = newDelegator.factory; dirty = false; // data was just read, i.e. memory and file are in sync return true; } else { Log.d("StorageDelegator", "saved state null"); return false; } } finally { unlock(); } } /** * Return a localized list of strings describing the changes we would upload on {@link #uploadToServer(Server)}. * * @param aResources the translations * @return the changes */ public List<String> listChanges(final Resources aResources) { List<String> retval = new ArrayList<String>(); for (Node node : new ArrayList<Node>(apiStorage.getNodes())) { retval.add(node.getStateDescription(aResources)); } for (Way way : new ArrayList<Way>(apiStorage.getWays())) { retval.add(way.getStateDescription(aResources)); } for (Relation relation : new ArrayList<Relation>(apiStorage.getRelations())) { retval.add(relation.getStateDescription(aResources)); } return retval; } /** * * @param server Server to upload changes to. * @param comment Changeset comment. * @param source * @param closeChangeset * @throws MalformedURLException * @throws ProtocolException * @throws OsmServerException * @throws IOException */ public synchronized void uploadToServer(final Server server, final String comment, String source, boolean closeChangeset) throws MalformedURLException, ProtocolException, OsmServerException, IOException { dirty = true; // storages will get modified as data is uploaded, these changes need to be saved to file // upload methods set dirty flag too, in case the file is saved during an upload boolean split = getApiElementCount() > server.getCapabilities().maxElementsInChangeset; int part = 1; while (getApiElementCount() > 0) { String tmpSource = source; if (split) { tmpSource = source + " [" + part + "]"; } server.openChangeset(comment, tmpSource, Util.listToOsmList(imagery)); server.diffUpload(this); if (closeChangeset || split) { // always close when splitting server.closeChangeset(); } part++; } // yes, again, just to be sure dirty = true; // reset imagery recording for next upload imagery = new ArrayList<String>(); setImageryRecorded(false); // sanity check if (!apiStorage.isEmpty()) { Log.d(DEBUG_TAG, "apiStorage not empty"); } } /** * Exports changes as a OsmChange file. */ @Override public void export(OutputStream outputStream) throws Exception { writeOsmChange(outputStream, null, Integer.MAX_VALUE); } @Override public String exportExtension() { return "osc"; } /** * Writes created/changed/deleted data to outputStream in OsmChange format * http://wiki.openstreetmap.org/wiki/OsmChange * @param outputStream stream to write to * @param changeSetId the allocated changeset id or null if non * @param maxChanges maximum number of changes to write * @throws IllegalArgumentException * @throws IllegalStateException * @throws IOException * @throws XmlPullParserException */ public void writeOsmChange(OutputStream outputStream, @Nullable Long changeSetId, int maxChanges) throws IllegalArgumentException, IllegalStateException, IOException, XmlPullParserException { int count = 0; Log.d(DEBUG_TAG, "writing osm change with changesetid " + changeSetId); XmlSerializer serializer = XmlPullParserFactory.newInstance().newSerializer(); serializer.setOutput(outputStream, "UTF-8"); serializer.startDocument("UTF-8", null); serializer.startTag(null, "osmChange"); serializer.attribute(null, "generator", App.userAgent); serializer.attribute(null, "version", "0.6"); ArrayList<OsmElement> createdNodes = new ArrayList<OsmElement>(); ArrayList<OsmElement> modifiedNodes = new ArrayList<OsmElement>(); ArrayList<OsmElement> deletedNodes = new ArrayList<OsmElement>(); ArrayList<OsmElement> createdWays = new ArrayList<OsmElement>(); ArrayList<OsmElement> modifiedWays = new ArrayList<OsmElement>(); ArrayList<OsmElement> deletedWays = new ArrayList<OsmElement>(); ArrayList<Relation> createdRelations = new ArrayList<Relation>(); ArrayList<Relation> modifiedRelations = new ArrayList<Relation>(); ArrayList<Relation> deletedRelations = new ArrayList<Relation>(); for (OsmElement elem : apiStorage.getNodes()) { Log.d("StorageDelegator", "node added to list for upload, id " + elem.osmId); switch (elem.state) { case OsmElement.STATE_CREATED: createdNodes.add(elem); break; case OsmElement.STATE_MODIFIED: modifiedNodes.add(elem); break; case OsmElement.STATE_DELETED: deletedNodes.add(elem); break; } count++; if (count>=maxChanges) { break; } } if (count < maxChanges) { for (OsmElement elem : apiStorage.getWays()) { Log.d("StorageDelegator", "way added to list for upload, id " + elem.osmId); switch (elem.state) { case OsmElement.STATE_CREATED: createdWays.add(elem); break; case OsmElement.STATE_MODIFIED: modifiedWays.add(elem); break; case OsmElement.STATE_DELETED: deletedWays.add(elem); break; } count++; if (count>=maxChanges) { break; } } } if (count < maxChanges) { for (OsmElement elem : apiStorage.getRelations()) { Log.d("StorageDelegator", "relation added to list for upload, id " + elem.osmId); switch (elem.state) { case OsmElement.STATE_CREATED: createdRelations.add((Relation) elem); break; case OsmElement.STATE_MODIFIED: modifiedRelations.add((Relation) elem); break; case OsmElement.STATE_DELETED: deletedRelations.add((Relation) elem); break; } count++; if (count>=maxChanges) { break; } } } Comparator<Relation> relationOrder = new Comparator<Relation>() { @Override public int compare(Relation r1, Relation r2) { if (r1.hasParentRelation(r2)) { return -1; } if (r2.hasParentRelation(r1)) { return 1; } return 0; }}; if (!createdRelations.isEmpty()) { // sort the relations so that childs come first, will not handle loops and similar brokenness Collections.sort(createdRelations, relationOrder); } if (!modifiedRelations.isEmpty()) { // sort the relations so that childs come first, will not handle loops and similar brokenness Collections.sort(modifiedRelations, relationOrder); } if (!deletedRelations.isEmpty()) { // sort the relations so that parents come first, will not handle loops and similar brokenness Collections.sort(deletedRelations, new Comparator<Relation>() { @Override public int compare(Relation r1, Relation r2) { if (r1.hasParentRelation(r2)) { return 1; } if (r2.hasParentRelation(r1)) { return -1; } return 0; }}); } if (!createdNodes.isEmpty() || !createdWays.isEmpty() || !createdRelations.isEmpty()) { serializer.startTag(null, "create"); for (OsmElement elem : createdNodes) elem.toXml(serializer, changeSetId); for (OsmElement elem : createdWays) elem.toXml(serializer, changeSetId); for (OsmElement elem : createdRelations) elem.toXml(serializer, changeSetId); serializer.endTag(null, "create"); } if (!modifiedNodes.isEmpty() || !modifiedWays.isEmpty() || !modifiedRelations.isEmpty()) { serializer.startTag(null, "modify"); for (OsmElement elem : modifiedNodes) elem.toXml(serializer, changeSetId); for (OsmElement elem : modifiedWays) elem.toXml(serializer, changeSetId); for (OsmElement elem : modifiedRelations) elem.toXml(serializer, changeSetId); serializer.endTag(null, "modify"); } // delete in opposite order if (!deletedNodes.isEmpty() || !deletedWays.isEmpty() || !deletedRelations.isEmpty()) { serializer.startTag(null, "delete"); for (OsmElement elem : deletedRelations) elem.toXml(serializer, changeSetId); for (OsmElement elem : deletedWays) elem.toXml(serializer, changeSetId); for (OsmElement elem : deletedNodes) elem.toXml(serializer, changeSetId); serializer.endTag(null, "delete"); } serializer.endTag(null, "osmChange"); serializer.endDocument(); } /** * Writes currentStorage + deleted objects to an outputstream in JOSM format. * Note: currently does not sort output except by OSM object type * @param outputStream the stream we are writing to * @throws XmlPullParserException * @throws IllegalArgumentException * @throws IllegalStateException * @throws IOException */ public void save(OutputStream outputStream) throws XmlPullParserException, IllegalArgumentException, IllegalStateException, IOException { XmlSerializer serializer = XmlPullParserFactory.newInstance().newSerializer(); serializer.setOutput(outputStream, "UTF-8"); serializer.startDocument("UTF-8", null); serializer.startTag(null, "osm"); serializer.attribute(null, "generator", App.userAgent); serializer.attribute(null, "version", "0.6"); serializer.attribute(null, "upload", "true"); ArrayList<Node> saveNodes = new ArrayList<Node>(currentStorage.getNodes()); ArrayList<Way> saveWays = new ArrayList<Way>(currentStorage.getWays()); ArrayList<Relation> saveRelations = new ArrayList<Relation>(currentStorage.getRelations()); for (Node elem : apiStorage.getNodes()) { if (elem.state == OsmElement.STATE_DELETED) { Log.d("StorageDelegator", "deleted node added to list for save, id " + elem.osmId); saveNodes.add(elem); } } for (Way elem : apiStorage.getWays()) { if (elem.state == OsmElement.STATE_DELETED) { Log.d("StorageDelegator", "deleted way added to list for save, id " + elem.osmId); saveWays.add(elem); } } for (Way elem : apiStorage.getWays()) { if (elem.state == OsmElement.STATE_DELETED) { Log.d("StorageDelegator", "deleted way added to list for save, id " + elem.osmId); saveWays.add(elem); } } // for (BoundingBox b:currentStorage.getBoundingBoxes()) { b.toJosmXml(serializer); } //TODO sort arrays here if (!saveNodes.isEmpty()) { for (OsmElement elem : saveNodes) elem.toJosmXml(serializer); } if (!saveWays.isEmpty()) { for (OsmElement elem : saveWays) elem.toJosmXml(serializer); } if (!saveRelations.isEmpty()) { for (OsmElement elem : saveRelations) elem.toJosmXml(serializer); } serializer.endTag(null, "osm"); serializer.endDocument(); } /** * Merge additional data with existing, copy to a new storage because this may fail * @param storage */ synchronized public boolean mergeData(Storage storage, PostMergeHandler postMerge) { Log.d("StorageDelegator","mergeData called"); // make temp copy of current storage (we may have to abort Storage temp = new Storage(currentStorage); // retrieve the maps LongOsmElementMap<Node> nodeIndex = temp.getNodeIndex(); LongOsmElementMap<Way> wayIndex = temp.getWayIndex(); LongOsmElementMap<Relation> relationIndex = temp.getRelationIndex(); Log.d("StorageDelegator","mergeData finished init"); try { // add nodes for (Node n:storage.getNodes()) { Node apiNode = apiStorage.getNode(n.getOsmId()); // can contain deleted elements if (!nodeIndex.containsKey(n.getOsmId()) && apiNode == null) { // new node no problem temp.insertNodeUnsafe(n); if (postMerge != null) { postMerge.handler(n); } } else { if (apiNode != null && apiNode.getState() == OsmElement.STATE_DELETED) { if (apiNode.getOsmVersion() >= n.getOsmVersion()) { continue; // can use node we already have } else { return false; // can't resolve conflicts, upload first } } Node existingNode = nodeIndex.get(n.getOsmId()); if (existingNode.getOsmVersion() >= n.getOsmVersion()) { // larger just to be on the safe side continue; // can use node we already have } else { if (existingNode.isUnchanged()) { temp.insertNodeUnsafe(n); if (postMerge != null) { postMerge.handler(n); } } else { return false; // can't resolve conflicts, upload first } } } } Log.d("StorageDelegator","mergeData added nodes"); // add ways for (Way w:storage.getWays()) { Way apiWay = apiStorage.getWay(w.getOsmId()); // can contain deleted elements if (!wayIndex.containsKey(w.getOsmId()) && apiWay == null) { // new way no problem temp.insertWayUnsafe(w); if (postMerge != null) { postMerge.handler(w); } } else { if (apiWay != null && apiWay.getState() == OsmElement.STATE_DELETED) { if (apiWay.getOsmVersion() >= w.getOsmVersion()) { continue; // can use way we already have } else { return false; // can't resolve conflicts, upload first } } Way existingWay = wayIndex.get(w.getOsmId()); if (existingWay != null) { if (existingWay.getOsmVersion() >= w.getOsmVersion()) {// larger just to be on the safe side continue; // can use way we already have } else { if (existingWay.isUnchanged()) { temp.insertWayUnsafe(w); if (postMerge != null) { postMerge.handler(w); } } else { return false; // can't resolve conflicts, upload first } } } else { // this shouldn't be able to happen Log.e("StorageDelegator","mergeData null existing way " + w.getOsmId()); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(null); return false; } } } Log.d("StorageDelegator","mergeData added ways"); // fix up way nodes // all nodes should be in storage now, however new ways will have references to copies not in storage for (Way w:wayIndex) { List<Node> nodes = w.getNodes(); for (int i=0;i<nodes.size();i++) { Node wayNode = nodes.get(i); long wayNodeId = wayNode.getOsmId(); Node n = nodeIndex.get(wayNodeId); if (n != null) { nodes.set(i,n); } else { // node might have been deleted, aka somebody deleted nodes outside of the down loaded data bounding box // that belonged to a not downloaded way Node apiNode = apiStorage.getNode(wayNodeId); if (apiNode != null && apiNode.getState() == OsmElement.STATE_DELETED) { // attempt to fix this up, reinstate the original node so that any existing references remain // FIXME undoing the original delete will likely cause havoc Log.e("StorageDelegator","mergeData null undeleting node " + wayNodeId); if (apiNode.getOsmVersion() == wayNode.getOsmVersion() && (apiNode.isTagged() && apiNode.getTags().equals(wayNode.getTags())) && apiNode.getLat() == wayNode.getLat() && apiNode.getLon() == wayNode.getLon()) { apiNode.setState(OsmElement.STATE_UNCHANGED); apiStorage.removeNode(apiNode); } else { apiNode.setState(OsmElement.STATE_MODIFIED); } temp.insertNodeUnsafe(apiNode); nodes.set(i,apiNode); } else { Log.e("StorageDelegator","mergeData null way node for way " + w.getOsmId() + " v" + w.getOsmVersion() + " node " + wayNodeId + " v" + wayNode.getOsmVersion()); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(null); return false; } } } } Log.d("StorageDelegator","mergeData fixup way nodes nodes"); // add relations for (Relation r:storage.getRelations()) { Relation apiRelation = apiStorage.getRelation(r.getOsmId()); // can contain deleted elements if (!relationIndex.containsKey(r.getOsmId()) && apiRelation == null) { // new relation no problem temp.insertRelationUnsafe(r); if (postMerge != null) { postMerge.handler(r); } } else { if (apiRelation != null && apiRelation.getState() == OsmElement.STATE_DELETED) { if (apiRelation.getOsmVersion() >= r.getOsmVersion()) continue; // can use relation we already have else return false; // can't resolve conflicts, upload first } Relation existingRelation = relationIndex.get(r.getOsmId()); if (existingRelation.getOsmVersion() >= r.getOsmVersion()) { // larger just to be on the safe side continue; // can use relation we already have } else { if (existingRelation.isUnchanged()) { temp.insertRelationUnsafe(r); if (postMerge != null) { postMerge.handler(r); } } else return false; // can't resolve conflicts, upload first } } } Log.d("StorageDelegator","mergeData added relations"); // fixup relation back links and memberships for (Relation r:temp.getRelations()) { for (RelationMember rm:r.getMembers()) { if (rm.getType().equals(Node.NAME)) { if (nodeIndex.containsKey(rm.getRef())) { // if node is downloaded always re-set it Node n = nodeIndex.get(rm.getRef()); rm.setElement(n); if (n.hasParentRelation(r.getOsmId())) { n.removeParentRelation(r.getOsmId()); // this removes based on id } // net effect is to remove the old rel n.addParentRelation(r); // and add the updated one } else { // check if deleted Node apiNode = apiStorage.getNode(rm.getRef()); if (apiNode != null && apiNode.getState() == OsmElement.STATE_DELETED) { Log.e("StorageDelegator","mergeData deleted node in downloaded relation " + r.getOsmId()); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(null); return false; // can't resolve conflicts, upload first } } } else if (rm.getType().equals(Way.NAME)) { // same logic as for nodes if (wayIndex.containsKey(rm.getRef())) { Way w = wayIndex.get(rm.getRef()); rm.setElement(w); if (w.hasParentRelation(r.getOsmId())) { w.removeParentRelation(r.getOsmId()); } w.addParentRelation(r); } else { // check if deleted Way apiWay = apiStorage.getWay(rm.getRef()); if (apiWay != null && apiWay.getState() == OsmElement.STATE_DELETED) { Log.e("StorageDelegator","mergeData deleted way in downloaded relation"); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(null); return false; // can't resolve conflicts, upload first } } } else if (rm.getType().equals(Relation.NAME)) { // same logic as for nodes if (relationIndex.containsKey(rm.getRef())) { Relation r2 = relationIndex.get(rm.getRef()); rm.setElement(r2); if (r2.hasParentRelation(r.getOsmId())) { r2.removeParentRelation(r.getOsmId()); } r2.addParentRelation(r); } else { // check if deleted Relation apiRel = apiStorage.getRelation(rm.getRef()); if (apiRel != null && apiRel.getState() == OsmElement.STATE_DELETED) { Log.e("StorageDelegator","mergeData deleted relation in downloaded relation"); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(null); return false; // can't resolve conflicts, upload first } } } } } Log.d("StorageDelegator","mergeData fixuped relations"); } catch (StorageException sex) { // ran of memory return false; } currentStorage = temp; undo.setCurrentStorage(temp); return true; // Success } /** * This is only used when trying to fix conflicts * @param element */ public void removeFromUpload(OsmElement element) { apiStorage.removeElement(element); element.setState(OsmElement.STATE_UNCHANGED); } /** * This is only used when trying to fix conflicts * @param element * @param version */ public void setOsmVersion(OsmElement element, long version) { element.setOsmVersion(version); element.setState(OsmElement.STATE_MODIFIED); insertElementSafe(element); } /** * Return true if coordinates were in the original bboxes from downloads, needs a more efficient implementation * @param lat * @param lon * @return */ public boolean isInDownload(int lat, int lon) { for (BoundingBox bb:new ArrayList<BoundingBox>(currentStorage.getBoundingBoxes())) { // make shallow copy if (bb.isIn(lat, lon)) return true; } return false; } public BoundingBox getLastBox() { int s = getBoundingBoxes().size(); if (s > 0) { return currentStorage.getBoundingBoxes().get(getBoundingBoxes().size()-1); } Log.e(DEBUG_TAG,"Bounding box list empty"); return new BoundingBox(); // empty box } /** * for debugging only */ public void logStorage() { Log.d("StorageDelegator","storage dirty? " + isDirty()); Log.d("StorageDelegator","currentStorage"); currentStorage.logStorage(); Log.d("StorageDelegator","apiStorage"); apiStorage.logStorage(); } public void lock() { readingLock.lock(); } public void unlock() { readingLock.unlock(); } }