package de.blau.android; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.ProtocolException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.net.ssl.SSLProtocolException; import javax.xml.parsers.ParserConfigurationException; import org.acra.ACRA; import org.xml.sax.SAXException; import org.xmlpull.v1.XmlPullParserException; import com.drew.lang.annotations.NotNull; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.net.Uri; import android.os.AsyncTask; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.FragmentActivity; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.util.DisplayMetrics; import android.util.Log; import android.view.View; import de.blau.android.contract.Urls; import de.blau.android.dialogs.AttachedObjectWarning; import de.blau.android.dialogs.ErrorAlert; import de.blau.android.dialogs.ForbiddenLogin; import de.blau.android.dialogs.InvalidLogin; import de.blau.android.dialogs.Progress; import de.blau.android.dialogs.ProgressDialog; import de.blau.android.dialogs.UploadConflict; 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.osm.BoundingBox; import de.blau.android.osm.Node; import de.blau.android.osm.OsmElement; import de.blau.android.osm.OsmParser; import de.blau.android.osm.PostMergeHandler; import de.blau.android.osm.Relation; import de.blau.android.osm.RelationMember; import de.blau.android.osm.RelationMemberDescription; import de.blau.android.osm.Server; import de.blau.android.osm.Server.UserDetails; import de.blau.android.osm.Server.Visibility; import de.blau.android.osm.StorageDelegator; import de.blau.android.osm.Tags; import de.blau.android.osm.Track; import de.blau.android.osm.UndoStorage; import de.blau.android.osm.Way; import de.blau.android.prefs.Preferences; import de.blau.android.resources.DataStyle; import de.blau.android.resources.TileLayerServer; import de.blau.android.tasks.Note; import de.blau.android.tasks.Task; import de.blau.android.util.EditState; import de.blau.android.util.FileUtil; import de.blau.android.util.GeoMath; import de.blau.android.util.Offset; import de.blau.android.util.SavingHelper; import de.blau.android.util.Snack; import de.blau.android.util.Util; import de.blau.android.util.collections.MRUList; /** * Logic is the gatekeeper to actual object storage and provides higher level operations. * <ul> * <li> hold selected objects * <li> wrap operations with undo checkpoints * <li> hold current mode * <li> save and load state * </ul> * * @author mb */ public class Logic { private static final String DEBUG_TAG = Logic.class.getSimpleName(); /** * Enums for directions. Used for translation via cursor-pad. */ public enum CursorPaddirection { DIRECTION_LEFT, DIRECTION_DOWN, DIRECTION_RIGHT, DIRECTION_UP } /** * Enums for zooming. */ public static final boolean ZOOM_IN = true; public static final boolean ZOOM_OUT = false; /** * Minimum width of the viewBox for showing the tolerance. When the viewBox is wider, no element selection is * possible. */ private static final int TOLERANCE_MIN_VIEWBOX_WIDTH = 40000*2; /** * In MODE_EDIT this value is used for the padding of the display border. */ private static final int PADDING_ON_BORDER_TOUCH = 5; /** * In MODE_EDIT, when the user moves a node {@link PADDING_ON_BORDER_TOUCH} pixel to a border, the map will be * translated by this factor. */ private static final double BORDER_TOCH_TRANSLATION_FACTOR = 0.02; /** * Translation factor used by cursor-pad. */ private static final float TRANSLATION_FACTOR = 0.15f; /** * Global factor for all nodes and lines. */ public static final float STROKE_FACTOR = 100000; /** * Filename of file containing the currently edit state */ private static final String EDITSTATE_FILENAME = "edit.state"; /** Sorter instance for sorting nodes by distance */ private static final DistanceSorter<OsmElement, Node> nodeSorter = new DistanceSorter<OsmElement, Node>(); /** Sorter instance for sorting ways by distance */ private static final DistanceSorter<Way, Way> waySorter = new DistanceSorter<Way, Way>(); /** * maximum number of nodes in a way for it still to be moveable, arbitrary number for now */ private static final int MAX_NODES_FOR_MOVE = 100; /** * Stores the {@link Preferences} as soon as they are available. */ private Preferences prefs; /** * The user-selected node. */ private List<Node> selectedNodes; /** * The user-selected way. */ private List<Way> selectedWays; /** * The user-selected relation. */ private List<Relation> selectedRelations; /* The following are lists because elements could be add multiple times * adding them once per selected relation and the same for deletion avoids * having to maintain a count */ /** * ways belonging to a selected relation */ private List<Way> selectedRelationWays = null; /** * nodes belonging to a selected relation */ private List<Node> selectedRelationNodes = null; /** * relations belonging to a selected relation */ private List<Relation> selectedRelationRelations = null; /** * The user-selected bug. */ private Task selectedBug; private final static int MRULIST_SIZE = 10; /** * last changeset comment */ private MRUList<String> lastComments = new MRUList<String>(MRULIST_SIZE); /** * last changeset source */ private MRUList<String> lastSources = new MRUList<String>(MRULIST_SIZE); /** * Are we currently dragging a node? * Set by {@link #handleTouchEventDown(float, float)} */ private boolean draggingNode = false; /** * Are we currently dragging a way? * Set by {@link #handleTouchEventDown(float, float)} */ private boolean draggingWay = false; private int startLat; private int startLon; private float startY; private float startX; private float centroidY; private float centroidX; /** * Are we currently dragging a handle? */ private boolean draggingHandle = false; private Node handleNode = null; /** * */ private boolean rotatingWay = false; /** * Current mode. */ private Mode mode; /** * Screen locked or not */ private boolean locked; /** * The viewBox for the map. All changes on this Object are made in here or in {@link Tracker}. */ private final BoundingBox viewBox; /** * An instance of the map. Value set by Main via constructor. */ private Map map; private Set<OsmElement> clickableElements; /** * add relations to result of clicks/touches */ private boolean returnRelations = true; /** * The currently selected handle to be dragged to create a new node in a way. */ private Handle selectedHandle = null; /** * Filter to apply if any */ private Filter filter = null; /** * Should we show a warning if hidden/filtered objects are manipulated not persisted in edit state for now */ private boolean attachedObjectWarning = true; /** * Initiate all needed values. Starts Tracker and delegate the first values for the map. * */ Logic() { viewBox = getDelegator().getLastBox(); mode = Mode.MODE_EASYEDIT; setLocked(true); } /** * Set all {@link Preferences} and delegates them to {@link Tracker} and {@link Map}. The AntiAlias-Flag will be set * to {@link Paints}. Map gets repainted. * * @param prefs the new Preferences. */ void setPrefs(final Preferences prefs) { this.prefs = prefs; DataStyle.switchTo(prefs.getMapProfile()); } /** * Informs the current drawing profile of the user preferences affecting * drawing, the current screen properties, and clears the way cache. */ public void updateProfile() { DataStyle.switchTo(prefs.getMapProfile()); DataStyle.updateStrokes(strokeWidth(viewBox.getWidth())); DataStyle.setAntiAliasing(prefs.isAntiAliasingEnabled()); // zap the cached style for all ways for (Way w:getDelegator().getCurrentStorage().getWays()) { w.setFeatureProfile(null); } } /** * @return locked status */ public boolean isLocked() { return locked; } /** * @param locked set locked status */ public void setLocked(boolean locked) { this.locked = locked; } /** * Sets new mode. * If the new mode is different from the current one, * all selected Elements will be nulled, the Map gets repainted, * and the action bar will be reset. * * @param main instance of main that is calling this * @param mode mode to set */ public void setMode(@NotNull final Main main, @NotNull final Mode mode) { Log.d(DEBUG_TAG,"current mode " + this.mode + " new mode " + mode); if (this.mode == mode) { return; } Mode oldMode = this.mode; this.mode = mode; Main.onEditModeChanged(); setSelectedBug(null); deselectAll(); oldMode.teardown(main, this); mode.setup(main, this); invalidateMap(); } /** * Returns the current mode that the program is in. * @return the mode */ public Mode getMode() { return mode; } /** * Checks for changes in the API-Storage. * * @return {@link StorageDelegator#hasChanges()} */ public boolean hasChanges() { return getDelegator().hasChanges(); } /** * 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 getDelegator().getUndo(); } /** * Wrapper to ensure the dirty flag is set * @return checkpoint name or null if none available */ @Nullable public String undo() { String name = getDelegator().getUndo().undo(); getDelegator().dirty(); return name; } /** * Wrapper to ensure the dirty flag is set * @return checkpoint name or null if none available */ @Nullable public String redo() { String name = getDelegator().getUndo().redo(); getDelegator().dirty(); return name; } /** * Checks if the viewBox is close enough to the viewBox to be in the ability to edit something. * * @return true, if viewBox' width is smaller than {@link #TOLERANCE_MIN_VIEWBOX_WIDTH}. */ public boolean isInEditZoomRange() { return (viewBox.getWidth() < TOLERANCE_MIN_VIEWBOX_WIDTH) && (viewBox.getHeight() < TOLERANCE_MIN_VIEWBOX_WIDTH) && !map.tooManyNodes(); } /** * Translates the viewBox into the given direction by {@link #TRANSLATION_FACTOR} and sets GPS-Following to false. * Map will be repainted. * * @param direction the direction of the translation. */ public void translate(final CursorPaddirection direction) { float translation = viewBox.getWidth() * TRANSLATION_FACTOR; try { switch (direction) { case DIRECTION_LEFT: viewBox.translate(map, (int) -translation, 0); break; case DIRECTION_DOWN: viewBox.translate(map, 0, -(GeoMath.latE7ToMercatorE7(viewBox.getTop())-(int)(viewBox.getBottomMercator()*1E7D))); break; case DIRECTION_RIGHT: viewBox.translate(map, (int) translation, 0); break; case DIRECTION_UP: viewBox.translate(map, 0, GeoMath.latE7ToMercatorE7(viewBox.getTop())-(int)(viewBox.getBottomMercator()*1E7D)); break; } } catch (OsmException e) { // TODO Auto-generated catch block e.printStackTrace(); } invalidateMap(); } /** * Test if the requested zoom operation can be performed. * * @param zoomIn The zoom operation: ZOOM_IN or ZOOM_OUT. * @return true if the zoom operation can be performed, false if it can't. */ public boolean canZoom(final boolean zoomIn) { return zoomIn ? viewBox.canZoomIn() : viewBox.canZoomOut(); } /** * Zooms in or out. Checks if the new viewBox is close enough for editing * and sends this value to map. Strokes will be updated and map will be repainted. * * @param zoomIn * true for zooming in. */ public void zoom(final boolean zoomIn) { if (zoomIn) { viewBox.zoomIn(); } else { viewBox.zoomOut(); } DataStyle.updateStrokes(strokeWidth(viewBox.getWidth())); if (rotatingWay) { showCrosshairsForCentroid(); } map.postInvalidate(); } /** * Zooms the map in or out by the given factor and updates the map view after zooming. * * @param zoomFactor The factor to zoom by, negative values zoom out, positive zooms in. */ public void zoom(final float zoomFactor) { viewBox.zoom(zoomFactor); DataStyle.updateStrokes(strokeWidth(viewBox.getWidth())); if (rotatingWay) { showCrosshairsForCentroid(); } map.postInvalidate(); } /** * Set the zoom to a specific tile zoom level. * * @param z The TMS zoom level to zoom to (from 0 for the whole world to about 19 for small areas). */ public void setZoom(Map map,int z) { viewBox.setZoom(map, z); if (rotatingWay) { showCrosshairsForCentroid(); } } /** * Return a stroke width value that increases with zoom and is capped at a configurable value * * @param width screenwidth in 10e7 deg. * @return stroke width */ private float strokeWidth(long width){ // prefs may not have been initialized if (prefs != null ) return Math.min(prefs.getMaxStrokeWidth(), STROKE_FACTOR / width); return STROKE_FACTOR / width; } /** * Create an undo checkpoint using a resource string as the name, records imagery used at the same time * * @param activity that we were called from for access to the resources, if null we will use the resources from App * @param stringId the resource id of the string representing the checkpoint name */ private void createCheckpoint(@Nullable Activity activity, int stringId) { Resources r = activity != null ? activity.getResources() : App.resources(); boolean firstCheckpoint = !getDelegator().getUndo().canUndo(); getDelegator().getUndo().createCheckpoint(r.getString(stringId)); getDelegator().recordImagery(map); if (firstCheckpoint && activity instanceof AppCompatActivity) { ((AppCompatActivity)activity).supportInvalidateOptionsMenu(); } } /** * Remove an undo checkpoint using a resource string as the name * * @param activity that we were called from for access to the resources, if null we will use the resources from App * @param stringId the resource id of the string representing the checkpoint name */ private void removeCheckpoint(@Nullable Activity activity, int stringId) { Resources r = activity != null ? activity.getResources() : App.resources(); getDelegator().getUndo().removeCheckpoint(r.getString(stringId)); } /** * Delegates the setting of the Tag-list to {@link StorageDelegator}. * All existing tags will be replaced. * * @param activity activity we were called from * @param e element to change the tags on * @param tags Tag-List to be set. * @return false if the element wasn't in storage and the tags were not applied * @throws OsmIllegalOperationException if the e isn't in storage */ public synchronized void setTags(@Nullable Activity activity, @NonNull final OsmElement e, @Nullable final java.util.Map<String, String> tags) throws OsmIllegalOperationException { setTags(activity, e.getName(), e.getOsmId(), tags); } /** * Delegates the setting of the Tag-list to {@link StorageDelegator}. * All existing tags will be replaced. * * @param activity activity we were called from * @param type type of the element for the Tag-list. * @param osmId OSM-ID of the element. * @param tags Tag-List to be set. * @return false if no element exists for the given osmId/type. */ public synchronized void setTags(@Nullable Activity activity, final String type, final long osmId, @Nullable final java.util.Map<String, String> tags) throws OsmIllegalOperationException { OsmElement osmElement = getDelegator().getOsmElement(type, osmId); if (osmElement == null) { Log.e(DEBUG_TAG, "Attempted to setTags on a non-existing element " + type + " #" + osmId); throw new OsmIllegalOperationException("Element " + osmElement + " not in storage"); } else { createCheckpoint(activity, R.string.undo_action_set_tags); getDelegator().setTags(osmElement, tags); } } /** * Delegates the setting of the Tag-list to {@link StorageDelegator}. * All existing tags will be replaced. * * @param activity activity we were called from * @param type type of the element for the Tag-list. * @param osmId OSM-ID of the element. * @param tags Tag-List to be set. * @return false if no element exists for the given osmId/type. */ public synchronized boolean updateParentRelations(@Nullable Activity activity, final String type, final long osmId, final HashMap<Long, String> parents) { OsmElement osmElement = getDelegator().getOsmElement(type, osmId); if (osmElement == null) { Log.e(DEBUG_TAG, "Attempted to update relations on a non-existing element"); return false; } else { createCheckpoint(activity, R.string.undo_action_update_relations); getDelegator().updateParentRelations(osmElement, parents); return true; } } /** * Updates the list of members in the selected relation. * Actual work is delegated out to {@link StorageDelegator}. * * @param activity activity we were called from * @param osmId The OSM ID of the relation to change. * @param members The new list of members to set for the given relation. */ public synchronized boolean updateRelation(@Nullable Activity activity, long osmId, ArrayList<RelationMemberDescription> members) { OsmElement osmElement = getDelegator().getOsmElement(Relation.NAME, osmId); if (osmElement == null) { Log.e(DEBUG_TAG, "Attempted to update non-existing relation #" + osmId); return false; } else { createCheckpoint(activity, R.string.undo_action_update_relations); getDelegator().updateRelation((Relation)osmElement, members); return true; } } /** * Prepares the screen for an empty map. Strokes will be updated and map will be repainted. * * @param activity activity that called us * @param box the new empty map-box. Don't mess up with the viewBox! */ void newEmptyMap(@NotNull FragmentActivity activity, @NotNull BoundingBox box) { Log.d(DEBUG_TAG, "newEmptyMap"); if (box == null) { // probably should do a more general check if the BB is valid box = BoundingBox.getMaxMercatorExtent(); } // not checking will zap edits, given that this method will only be called when we are not downloading, not a good thing if (!getDelegator().isDirty()) { getDelegator().reset(false); // delegator.setOriginalBox(box); not needed IMHO } else { //TODO show warning Log.e(DEBUG_TAG, "newEmptyMap called on dirty storage"); } // if the map view isn't drawn use an approximation for the aspect ratio of the display ... this is a hack DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); float ratio = (float)metrics.widthPixels / (float)metrics.heightPixels; if (map.getHeight() != 0) { ratio = (float) map.getWidth() / map.getHeight(); } viewBox.setBorders(map, box, ratio, false); map.setViewBox(box); DataStyle.updateStrokes(strokeWidth(viewBox.getWidth())); invalidateMap(); activity.supportInvalidateOptionsMenu(); } /** * Searches for all Ways and Nodes at x,y plus the shown node-tolerance. Nodes have to lie in the mapBox. * * @param x display-coordinate. * @param y display-coordinate. * @return a List of all OsmElements (Nodes and Ways) within the tolerance */ public List<OsmElement> getClickedNodesAndWays(final float x, final float y) { ArrayList<OsmElement> result = new ArrayList<OsmElement>(); result.addAll(getClickedNodes(x, y)); result.addAll(getClickedWays(x, y)); if (returnRelations) { // add any relations that the elements are members of result.addAll(getParentRelations(result)); } return result; } /** * * @param elements * @return */ private ArrayList<OsmElement> getParentRelations(ArrayList<OsmElement> elements) { ArrayList<OsmElement> relations = new ArrayList<OsmElement>(); for (OsmElement e: elements) { if (e.getParentRelations() != null) { for (Relation r: e.getParentRelations()) { if (!relations.contains(r)) { // not very efficient relations.add(r); //FIXME add one level of parent relations of relations // we could do this recursively but would need to add loop protection if (r.getParentRelations() != null) { for (Relation p: r.getParentRelations()) { if (!relations.contains(p)) { relations.add(p); } } } } } } } return relations; } /** * Returns all ways within way tolerance from the given coordinates, and their distances from them. * * @param x x display coordinate * @param y y display coordinate * @return a hash map mapping Ways to distances */ private HashMap<Way, Double> getClickedWaysWithDistances(final float x, final float y) { return getClickedWaysWithDistances(true, x, y); } /** * Returns all ways within way tolerance from the given coordinates, and their distances from them. * * @param includeClosed include closed ways in the result if true * @param x x display coordinate * @param y y display coordinate * @return a hash map mapping Ways to distances */ private HashMap<Way, Double> getClickedWaysWithDistances(boolean includeClosed, final float x, final float y) { HashMap<Way, Double> result = new HashMap<Way, Double>(); boolean showWayIcons = prefs.getShowWayIcons(); List<Way> ways = filter != null ? filter.getVisibleWays() : getDelegator().getCurrentStorage().getWays(map.getViewBox()); for (Way way : ways) { if (way.isClosed() && !includeClosed) { continue; } boolean added = false; List<Node> wayNodes = way.getNodes(); if (clickableElements != null && !clickableElements.contains(way)) continue; double A = 0; double Y = 0; double X = 0; float node1X = Float.MAX_VALUE; float node1Y = Float.MAX_VALUE; //Iterate over all WayNodes, but not the last one. for (int k = 0, wayNodesSize = wayNodes.size(); k < wayNodesSize - 1; ++k) { Node node1 = wayNodes.get(k); Node node2 = wayNodes.get(k + 1); if (node1X == Float.MAX_VALUE) { node1X = lonE7ToX(node1.getLon()); node1Y = latE7ToY(node1.getLat()); } float node2X = lonE7ToX(node2.getLon()); float node2Y = latE7ToY(node2.getLat()); double distance = isPositionOnLine(x, y, node1X, node1Y, node2X, node2Y); if (distance >= 0) { result.put(way, distance); added = true; break; } // calculations for centroid double d = node1X*node2Y - node2X*node1Y; A = A + d; X = X + (node1X+node2X)*d; Y = Y + (node1Y+node2Y)*d; node1X = node2X; node1Y = node2Y; } if (Util.notZero(A) && showWayIcons && !added && way.isClosed() && (way.hasTagKey(Tags.KEY_BUILDING) || way.hasTag(Tags.KEY_INDOOR, Tags.VALUE_ROOM))) { Y = Y/(3*A); X = X/(3*A); double distance = Math.hypot(x-X, y-Y); if (distance < DataStyle.getCurrent().nodeToleranceValue) { result.put(way, distance); } } } return result; } /** * The small mid segment 'x' handles that allow dragging to easily add a new node to a way. */ class Handle { float x; float y; Handle(float x, float y) { this.x = x; this.y = y; } } /** * Returns all ways with a mid-way segment handle tolerance from the given coordinates, and their distances from them. * * @param x x display coordinate * @param y y display coordinate * @return a hash map mapping Ways to distances */ private Handle getClickedWayHandleWithDistances(final float x, final float y) { Handle result = null; double bestDistance = Double.MAX_VALUE; for (Way way : getDelegator().getCurrentStorage().getWays()) { List<Node> wayNodes = way.getNodes(); if (clickableElements != null && !clickableElements.contains(way)) continue; //Iterate over all WayNodes, but not the last one. for (int k = 0, wayNodesSize = wayNodes.size(); k < wayNodesSize - 1; ++k) { Node node1 = wayNodes.get(k); Node node2 = wayNodes.get(k + 1); // TODO only project once per node float node1X = lonE7ToX(node1.getLon()); float node1Y = latE7ToY(node1.getLat()); float xDelta = lonE7ToX(node2.getLon()) - node1X; float yDelta = latE7ToY(node2.getLat()) - node1Y; float handleX = node1X + xDelta/2; float handleY = node1Y + yDelta/2; float differenceX = Math.abs(handleX - x); float differenceY = Math.abs(handleY - y); if ((differenceX > DataStyle.getCurrent().wayToleranceValue) && (differenceY > DataStyle.getCurrent().wayToleranceValue)) continue; if (Math.hypot(xDelta,yDelta) <= DataStyle.getCurrent().minLenForHandle) continue; double dist = Math.hypot(differenceX, differenceY); // TODO better choice for tolerance if ((dist <= DataStyle.getCurrent().wayToleranceValue) && (dist < bestDistance)) { if (filter != null) { if (filter.include(way, isSelected(way))) { bestDistance = dist; result = new Handle(handleX, handleY); } } else { bestDistance = dist; result = new Handle(handleX, handleY); } } } } return result; } /** * Calculates the on-screen distance between a node and the screen coordinate of a click. * Returns null if the node was outside the click tolerance. * * @param node the node * @param x the x coordinate of the clicked point * @param y the y coordinate of the clicked point * @return The distance between the clicked point and the node in px if the node was within the tolerance value, * null otherwise */ private Double clickDistance(Node node, final float x, final float y) { return clickDistance(node, x, y, node.isTagged() ? DataStyle.getCurrent().nodeToleranceValue : DataStyle.getCurrent().wayToleranceValue/2); } private Double clickDistance(Node node, final float x, final float y, float tolerance) { float differenceX = Math.abs(lonE7ToX(node.getLon()) - x); float differenceY = Math.abs(latE7ToY(node.getLat()) - y); if ((differenceX > tolerance) && (differenceY > tolerance)) return null; double dist = Math.hypot(differenceX, differenceY); return (dist > tolerance) ? null : dist; } /** * Returns all nodes within node tolerance from the given coordinates, and their distances from them. * * @param x x display coordinate * @param y y display coordinate * @param inDownloadOnly if true the node has to be new or in one of the downloaded bounding boxes * @return a hash map mapping Nodes to distances */ private HashMap<Node, Double> getClickedNodesWithDistances(final float x, final float y, boolean inDownloadOnly) { HashMap<Node, Double> result = new HashMap<Node, Double>(); List<Node> nodes = filter != null ? filter.getVisibleNodes() : getDelegator().getCurrentStorage().getNodes(map.getViewBox()); for (Node node : nodes) { if (clickableElements != null && !clickableElements.contains(node)) { continue; } int lat = node.getLat(); int lon = node.getLon(); if (!inDownloadOnly || node.getState() != OsmElement.STATE_UNCHANGED || getDelegator().isInDownload(lat, lon)) { Double dist = clickDistance(node, x, y); if (dist != null) { result.put(node, dist); } } } return result; } /** * Searches for a Node at x,y plus the shown node-tolerance. The Node has to lay in the mapBox. * * @param x display-coordinate. * @param y display-coordinate. * @return all nodes within tolerance found in the currentStorage node-list, ordered ascending by distance. */ public List<OsmElement> getClickedNodes(final float x, final float y) { return nodeSorter.sort(getClickedNodesWithDistances(x, y, true)); } /** * Searches for a way end node at x,y plus the shown node-tolerance. The Node has to lay in the mapBox. * * @param x display-coordinate. * @param y display-coordinate. * @return all end nodes within tolerance found in the currentStorage node-list, ordered ascending by distance. */ public List<OsmElement> getClickedEndNodes(final float x, final float y) { List<OsmElement> result = new ArrayList<OsmElement>(); List<OsmElement> allNodes = getClickedNodes(x, y); for (OsmElement osmElement : allNodes) { if (getDelegator().getCurrentStorage().isEndNode((Node) osmElement)) result.add(osmElement); } return result; } /** * Searches for a Node at x,y plus the shown node-tolerance. The Node has to lay in the mapBox. * * @param x display-coordinate. * @param y display-coordinate. * @return the nearest node found in the current-Storage node-list. null, when no node was found. */ private Node getClickedNode(final float x, final float y) { Node bestNode = null; Double bestDistance = Double.MAX_VALUE; HashMap<Node, Double> candidates = getClickedNodesWithDistances(x, y, false); for (Entry<Node, Double> candidate : candidates.entrySet()) { if (candidate.getValue() < bestDistance) { bestNode = candidate.getKey(); bestDistance = candidate.getValue(); } } return bestNode; } /** * Returns all ways within click tolerance from the given coordinate * * @param x x display-coordinate. * @param y y display-coordinate. * @return the ways */ private List<Way> getClickedWays(final float x, final float y) { return getClickedWays(true, x, y); } /** * Returns all ways within click tolerance from the given coordinate * * @param includeClosed include closed ways in the result if true * @param x x display-coordinate. * @param y y display-coordinate. * @return the ways */ public List<Way> getClickedWays(boolean includeClosed, final float x, final float y) { return waySorter.sort(getClickedWaysWithDistances(includeClosed, x, y)); } /** * Returns the closest way (within tolerance) to the given coordinates * * @param x the x display-coordinate. * @param y the y display-coordinate. * @return the closest way, or null if no way is found within the tolerance */ private Way getClickedWay(final float x, final float y) { Way bestWay = null; Double bestDistance = Double.MAX_VALUE; HashMap<Way, Double> candidates = getClickedWaysWithDistances(x, y); for (Entry<Way, Double> candidate : candidates.entrySet()) { if (candidate.getValue() < bestDistance) { bestWay = candidate.getKey(); bestDistance = candidate.getValue(); } } return bestWay; } /** * Returns a list of all the clickable OSM elements in storage (does not * restrict to the current screen). Before returning the list is "pruned" to * remove any elements on the exclude list. * * @param excludes The list of OSM elements to exclude from the results. * @return */ public Set<OsmElement> findClickableElements(List<OsmElement> excludes) { Set<OsmElement> result = new HashSet<OsmElement>(); result.addAll(getDelegator().getCurrentStorage().getNodes()); result.addAll(getDelegator().getCurrentStorage().getWays()); for (OsmElement e:excludes) result.remove(e); return result; } /** * Get a list of all the Ways connected to the given Node. * * @param node The Node. * @return A list of all Ways connected to the Node. */ public List<Way> getWaysForNode(final Node node) { return getDelegator().getCurrentStorage().getWays(node); } /** * Get a list of all the filtered Ways connected to the given Node. * * @param node The Node. * @return A list of all Ways connected to the Node. */ public List<Way> getFilteredWaysForNode(final Node node) { List<Way> ways = new ArrayList<Way>(); for (Way w:getDelegator().getCurrentStorage().getWays(node)) { if (getFilter() == null || filter.include(w, false)) { ways.add(w); } } return ways; } /** * Test if the given Node is an end node of a Way. Isolated nodes not part * of a way are not considered an end node. * * @param node Node to test. * @return true if the Node is an end node of a Way, false otherwise. */ public boolean isEndNode(final Node node) { return getDelegator().getCurrentStorage().isEndNode(node); } /** * Check all nodes in way to see if they are in the downloaded data. * * @param way the way whose nodes should be checked * @return true if the above is the case */ public boolean isInDownload(Way way) { for (Node n:way.getNodes()) { if (!getDelegator().isInDownload(n.getLat(), n.getLon())){ return false; } } return true; } /** * Check if node is in the downloaded data * * @param node * @return true if the above is the case */ private boolean isInDownload(Node n) { return getDelegator().isInDownload(n.getLat(), n.getLon()); } /** * Handles the event when user begins to touch the display. When the viewBox is close enough for editing and the * user is in edit-mode a touched node will bet set to selected. draggingNode will be set if a node is to be moved. * A eventual movement of this node will be done in {@link #handleTouchEventMove(float, float, float, float, boolean)}. * * @param x display-coord. * @param y display-coord. */ synchronized void handleTouchEventDown(Activity activity, final float x, final float y) { boolean draggingMultiselect = false; if (!isLocked() && isInEditZoomRange() && mode.elementsGeomEditiable()) { draggingNode = false; draggingWay = false; draggingHandle = false; if (selectedNodes != null && selectedNodes.size() == 1 && selectedWays == null && clickDistance(selectedNodes.get(0), x, y, prefs.largeDragArea() ? DataStyle.getCurrent().largDragToleranceRadius : DataStyle.getCurrent().nodeToleranceValue) != null) { draggingNode = true; if (prefs.largeDragArea()) { startX = lonE7ToX(selectedNodes.get(0).getLon()); startY = latE7ToY(selectedNodes.get(0).getLat()); } } else { if (selectedWays != null && selectedWays.size() == 1 && selectedNodes == null) { if (!rotatingWay) { Handle handle = getClickedWayHandleWithDistances(x, y); if (handle != null) { Log.d(DEBUG_TAG,"start handle drag"); selectedHandle = handle; draggingHandle = true; } else { Way clickedWay = getClickedWay(x, y); if (clickedWay != null && (clickedWay.getOsmId() == selectedWays.get(0).getOsmId())) { if (selectedWays.get(0).getNodes().size() <= MAX_NODES_FOR_MOVE) { startLat = yToLatE7(y); startLon = xToLonE7(x); draggingWay = true; } else { Snack.barError(activity, R.string.toast_too_many_nodes_for_move); } } } } else { startX = x; startY = y; } } else { // check for multi-select if ((selectedWays != null && selectedWays.size() > 1) || (selectedNodes != null && selectedNodes.size() > 1) || ((selectedWays != null && selectedWays.size() >= 1) && (selectedNodes != null && selectedNodes.size() >= 1))) { Log.d(DEBUG_TAG, "Multi select detected"); boolean foundSelected = false; if (selectedWays != null) { List<Way> clickedWays = getClickedWays(x, y); for (Way w:clickedWays) { if (selectedWays.contains(w)) { foundSelected = true; break; } } } if (!foundSelected && selectedNodes != null) { List<OsmElement> clickedNodes = getClickedNodes(x,y); for (OsmElement n:clickedNodes) { if (selectedNodes.contains((Node)n)) { foundSelected = true; break; } } } if (foundSelected) { startLat = yToLatE7(y); startLon = xToLonE7(x); startX = x; startY = y; draggingMultiselect = true; draggingWay = true; } } else { if (rotatingWay) { rotatingWay = false; hideCrosshairs(); } // at last really nothing selected or special going on } } } } else { draggingNode = false; draggingWay = false; rotatingWay = false; draggingHandle = false; } Log.d(DEBUG_TAG,"handleTouchEventDown creating checkpoints"); if (draggingNode || draggingWay) { if (draggingMultiselect) { createCheckpoint(activity, R.string.undo_action_moveobjects); } else { createCheckpoint(activity, draggingNode ? R.string.undo_action_movenode : R.string.undo_action_moveway); } } else if (rotatingWay) { createCheckpoint(activity, R.string.undo_action_rotateway); } } synchronized void handleTouchEventUp(final float x, final float y) { handleNode = null; draggingHandle = false; } /** * Calculates the coordinates for the center of the screen and displays a crosshair there. */ public void showCrosshairsForCentroid() { if (selectedWays == null) { return; } synchronized(selectedWays) { float centroid[] = centroidXY(map.getWidth(), map.getHeight(), viewBox, selectedWays.get(0)); if (centroid==null) { return; } centroidX = centroid[0]; centroidY = centroid[1]; showCrosshairs(centroidX,centroidY); } } /** * Handles a finger-movement on the touchscreen. * Moves a node when draggingNode was set by {@link #handleTouchEventDown(float, float)}. * Otherwise the movement will be interpreted as map-translation. * Map will be repainted. * * @param absoluteX The absolute display-coordinate. * @param absoluteY The absolute display-coordinate. * @param relativeX The difference to the last absolute display-coordinate. * @param relativeY The difference to the last absolute display-coordinate. * @throws OsmIllegalOperationException */ synchronized void handleTouchEventMove(Main main, final float absoluteX, final float absoluteY, final float relativeX, final float relativeY) { if (draggingNode || draggingWay || draggingHandle) { int lat; int lon; // checkpoint created where draggingNode is set if ((draggingNode && selectedNodes != null && selectedNodes.size() == 1 && selectedWays == null) || draggingHandle) { if (draggingHandle) { // create node only if we are really dragging try { if (handleNode == null && selectedHandle != null && selectedWays != null) { Log.d(DEBUG_TAG,"creating node at handle position"); handleNode = performAddOnWay(main, selectedWays, selectedHandle.x, selectedHandle.y, true); selectedHandle = null; } if (handleNode != null) { setSelectedNode(null); // performAddOnWay sets this, need to undo getDelegator().updateLatLon(handleNode, yToLatE7(absoluteY), xToLonE7(absoluteX)); } } catch (OsmIllegalOperationException e) { Snack.barError(main, e.getMessage()); return; } } else { displayAttachedObjectWarning(main, selectedNodes.get(0)); if (prefs.largeDragArea()) { startY = startY + relativeY; startX = startX - relativeX; lat = yToLatE7(startY); lon = xToLonE7(startX); } else { lat = yToLatE7(absoluteY); lon = xToLonE7(absoluteX); } getDelegator().updateLatLon(selectedNodes.get(0), lat, lon); } } else { // way dragging and multi-select lat = yToLatE7(absoluteY); lon = xToLonE7(absoluteX); ArrayList<Node> nodes = new ArrayList<Node>(); if (selectedWays != null && selectedWays.size() > 0) { // shouldn't happen but might be a race condition for (Way w:selectedWays) { nodes.addAll(w.getNodes()); } } if (selectedNodes != null && selectedNodes.size() > 0) { for (Node n:selectedNodes) { nodes.add(n); } } displayAttachedObjectWarning(main, nodes); getDelegator().moveNodes(nodes, lat - startLat, lon - startLon); // update startLat = lat; startLon = lon; } translateOnBorderTouch(absoluteX, absoluteY); main.easyEditManager.invalidate(); // if we are in an action mode update menubar } else if (rotatingWay) { double aSq = (startY-absoluteY)*(startY-absoluteY) + (startX-absoluteX)*(startX-absoluteX); double bSq = (absoluteY-centroidY)*(absoluteY-centroidY) + (absoluteX-centroidX)*(absoluteX-centroidX); double cSq = (startY-centroidY)*(startY-centroidY) + (startX-centroidX)*(startX-centroidX); double cosAngle = (bSq + cSq -aSq)/(2*Math.sqrt(bSq)*Math.sqrt(cSq)); int direction = 1; // 1 clockwise, -1 anti-clockwise // not perfect but works good enough if ((startY <= centroidY) && (absoluteY <= centroidY)) { direction = (startX > absoluteX) ? -1 : 1; } else if ((startX >= centroidX) && (absoluteX >= centroidX)) { direction = (startY > absoluteY) ? -1 : 1; } else if ((startY >= centroidY) && (absoluteY >= centroidY)) { direction = (startX < absoluteX) ? -1 : 1; } else if ((startX < centroidX) && (absoluteX < centroidX)) { direction = (startY < absoluteY) ? -1 : 1; } else if ((startY < startX) && (absoluteY < absoluteX)) { direction = (startY > absoluteY) ? -1: 1; } else if ((startY >= startX) && (absoluteY >= absoluteX)) { direction = (startY < absoluteY) ? -1: 1; } displayAttachedObjectWarning(main, selectedWays.get(0)); getDelegator().rotateWay(selectedWays.get(0), (float)Math.acos(cosAngle), direction, centroidX, centroidY, map.getWidth(), map.getHeight(), viewBox); startY = absoluteY; startX = absoluteX; main.easyEditManager.invalidate(); // if we are in an action mode update menubar } else { if (mode == Mode.MODE_ALIGN_BACKGROUND) performBackgroundOffset(relativeX, relativeY); else performTranslation(map, relativeX, relativeY); } invalidateMap(); } /** * @return is we should show warnings when filtered attached objects are being changed */ private boolean showAttachedObjectWarning() { return attachedObjectWarning; } /** * Determine if we should show warnings when filtered attached objects are being changed * @param show */ public void setAttachedObjectWarning(boolean show) { attachedObjectWarning = show; } /** * Puts the editor into the mode where the selected way will be rotated by * the handleTouchEventMove function on move events. * @param on new state */ public void setRotationMode(boolean on) { rotatingWay = on; } /** * Converts screen-coords to gps-coords and delegates translation to {@link BoundingBox#translate(int, int)}. * GPS-Following will be disabled. * * @param map current map view * @param screenTransX Movement on the screen. * @param screenTransY Movement on the screen. */ private void performTranslation(Map map, final float screenTransX, final float screenTransY) { // Log.d(DEBUG_TAG,"performTranslation " + screenTransX + " " + screenTransY); int height = map.getHeight(); int lon = xToLonE7(screenTransX); int lat = yToLatE7(height - screenTransY); int relativeLon = lon - viewBox.getLeft(); int relativeLat = lat - viewBox.getBottom(); try { viewBox.translate(map, relativeLon, relativeLat); } catch (OsmException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * Converts screen-coords to gps-coords and offests background layer. * * @param screenTransX Movement on the screen. * @param screenTransY Movement on the screen. */ private void performBackgroundOffset(final float screenTransX, final float screenTransY) { int height = map.getHeight(); int lon = xToLonE7(screenTransX); int lat = yToLatE7(height - screenTransY); int relativeLon = lon - viewBox.getLeft(); int relativeLat = lat - viewBox.getBottom(); TileLayerServer osmts = map.getOpenStreetMapTilesOverlay().getRendererInfo(); double lonOffset = 0d; double latOffset = 0d; Offset o = osmts.getOffset(map.getZoomLevel()); if (o != null) { lonOffset = o.lon; latOffset = o.lat; } osmts.setOffset(map.getZoomLevel(),lonOffset - relativeLon/1E7d, latOffset - relativeLat/1E7d); } /** * Executes an add-command for x,y. Adds new nodes and ways to storage. When more than one Node were * created/selected then a new way will be created. * * @param activity activity this was called from, if null no warnings will be displayed * @param x screen-coordinate * @param y screen-coordinate * @throws OsmIllegalOperationException */ public synchronized void performAdd(@Nullable final Activity activity, final float x, final float y) throws OsmIllegalOperationException { Log.d(DEBUG_TAG,"performAdd"); createCheckpoint(activity, R.string.undo_action_add); Node nextNode; Node lSelectedNode = selectedNodes != null && selectedNodes.size() > 0 ? selectedNodes.get(0) : null; Way lSelectedWay = selectedWays != null && selectedWays.size() > 0 ? selectedWays.get(0) : null; if (lSelectedNode == null) { //This will be the first node. lSelectedNode = getClickedNodeOrCreatedWayNode(x, y); if (lSelectedNode == null) { //A complete new Node... int lat = yToLatE7(y); int lon = xToLonE7(x); lSelectedNode = getDelegator().getFactory().createNodeWithNewId(lat, lon); getDelegator().insertElementSafe(lSelectedNode); if (!getDelegator().isInDownload(lat, lon)) { Log.d(DEBUG_TAG,"Outside of download"); Snack.barWarningShort(activity, R.string.toast_outside_of_download); } } } else { //this is not the first node nextNode = getClickedNodeOrCreatedWayNode(x, y); if (nextNode == null) { //clicked on empty space -> create a new Node if (lSelectedWay == null) { //This is the second Node, so we create a new Way and add the previous selected node to this way lSelectedWay = getDelegator().createAndInsertWay(lSelectedNode); } int lat = yToLatE7(y); int lon = xToLonE7(x); lSelectedNode = getDelegator().getFactory().createNodeWithNewId(lat, lon); getDelegator().addNodeToWay(lSelectedNode, lSelectedWay); getDelegator().insertElementSafe(lSelectedNode); if (!getDelegator().isInDownload(lat, lon)) { Log.d(DEBUG_TAG,"Outside of download"); Snack.barWarningShort(activity, R.string.toast_outside_of_download); } } else { //User clicks an existing Node if (nextNode == lSelectedNode) { //User clicks the last Node -> end here with adding removeCheckpoint(activity, R.string.undo_action_add); lSelectedNode = null; lSelectedWay = null; } else { //Create a new way with the existing node, which was clicked. if (lSelectedWay == null) { lSelectedWay = getDelegator().createAndInsertWay(lSelectedNode); } //Add the new Node. getDelegator().addNodeToWay(nextNode, lSelectedWay); lSelectedNode = nextNode; } } } setSelectedNode(lSelectedNode); setSelectedWay(lSelectedWay); } /** * Simplified version of creating a new node that takes geo coords and doesn't try to merge with existing features * @activity activity this was called from, if null no warnings will be displayed * @param lonD * @param latD * @return the create node */ public synchronized Node performAddNode(@Nullable final Activity activity, Double lonD, Double latD) { //A complete new Node... Log.d(DEBUG_TAG,"performAddNode"); createCheckpoint(activity, R.string.undo_action_add); int lon = (int)(lonD*1E7D); int lat = (int)(latD*1E7D); Node newNode = getDelegator().getFactory().createNodeWithNewId(lat, lon); getDelegator().insertElementSafe(newNode); if (!getDelegator().isInDownload(lat, lon)) { // warning toast Log.d(DEBUG_TAG,"Outside of download"); Snack.barWarningShort(activity, R.string.toast_outside_of_download); } setSelectedNode(newNode); return newNode; } /** * Executes an add node operation for x,y but only if on a way. Adds new node to storage and will select it. * * @param activity activity we were called from * @param ways candidate ways if null all ways will be considered * @param x screen-coordinate * @param y screen-coordinate * @param forceNew ignore nearby existing nodes * @return the new node or null if none was created * @throws OsmIllegalOperationException */ public synchronized Node performAddOnWay(@Nullable Activity activity, List<Way>ways,final float x, final float y, boolean forceNew) throws OsmIllegalOperationException { createCheckpoint(activity, R.string.undo_action_add); Node savedSelectedNode = selectedNodes != null && selectedNodes.size() > 0 ? selectedNodes.get(0) : null; Node newSelectedNode = getClickedNodeOrCreatedWayNode(ways,x, y, forceNew); if (newSelectedNode == null) { newSelectedNode = savedSelectedNode; return null; } setSelectedNode(newSelectedNode); return newSelectedNode; } /** * Catches the first node at the given position and delegates the deletion to {@link #delegator}. * @param activity activity this was called from, if null no warnings will be displayed * @param createCheckpoint create and undo checkpoint if true * @param x screen-coordinate. * @param y screen-coordinate. */ public synchronized void performEraseNode(@Nullable final FragmentActivity activity, final Node node, boolean createCheckpoint) { if (node != null) { if (createCheckpoint) { createCheckpoint(activity, R.string.undo_action_deletenode); } displayAttachedObjectWarning(activity, node); // needs to be done before removal getDelegator().removeNode(node); invalidateMap(); if (!isInDownload(node)) { // warning toast Snack.barWarning(activity, R.string.toast_outside_of_download); } } } /** * set new coordinates and center viewbox on them * * @param activity activity this was called from, if null no warnings will be displayed * @param node node to set position on * @param lon longitude * @param lat latitude */ public void performSetPosition(@Nullable final FragmentActivity activity, Node node, double lon, double lat) { if (node != null) { createCheckpoint(activity, R.string.undo_action_movenode); int lonE7 = (int)(lon*1E7d); int latE7 = (int)(lat*1E7d); getDelegator().updateLatLon(node, latE7, lonE7); viewBox.moveTo(map, lonE7, latE7); invalidateMap(); displayAttachedObjectWarning(activity, node); } } /** * Deletes a way. * * @param activity activity this was called from, if null no warnings will be displayed * @param way the way to be deleted * @param deleteOrphanNodes if true, way nodes that have no tags and are in no other ways will be deleted too * @param createCheckpoint if true create an undo checkpoint */ public synchronized void performEraseWay(@Nullable final FragmentActivity activity, final Way way, final boolean deleteOrphanNodes, boolean createCheckpoint) { if (createCheckpoint) { createCheckpoint(activity, R.string.undo_action_deleteway); } displayAttachedObjectWarning(activity, way); // needs to be done before removal HashSet<Node> nodes = deleteOrphanNodes ? new HashSet<Node>(way.getNodes()) : null; // HashSet guarantees uniqueness getDelegator().removeWay(way); if (deleteOrphanNodes) { for (Node node : nodes) { if (getWaysForNode(node).isEmpty() && node.getTags().isEmpty()) getDelegator().removeNode(node); } } invalidateMap(); } /** * Catches the first relation at the given position and delegates the deletion to {@link #delegator}. * * @param activity activity this was called from, if null no warnings will be displayed * @param createCheckpoint TODO * @param x screen-coordinate. * @param y screen-coordinate. */ public synchronized void performEraseRelation(@Nullable final FragmentActivity activity, final Relation relation, boolean createCheckpoint) { if (relation != null) { if (createCheckpoint) { createCheckpoint(activity, R.string.undo_action_delete_relation); } displayAttachedObjectWarning(activity, relation); // needs to be done before removal getDelegator().removeRelation(relation); invalidateMap(); } } /** * Erase a list of objects * * @param activity activity this method was called from, if null no warnings will be displayed * @param selection objects to delete */ public synchronized void performEraseMultipleObjects(@Nullable final FragmentActivity activity, ArrayList<OsmElement> selection) { // need to make three passes createCheckpoint(activity, R.string.undo_action_delete_objects); displayAttachedObjectWarning(activity, selection); // needs to be done before removal for (OsmElement e:selection) { if (e instanceof Relation && e.getState() != OsmElement.STATE_DELETED) { performEraseRelation(activity, (Relation)e, false); } } for (OsmElement e:selection) { if (e instanceof Way && e.getState() != OsmElement.STATE_DELETED) { if (isInDownload((Way)e)) { performEraseWay(activity, (Way)e, true, false); // TODO maybe we don't want to delete the nodes } else { // TODO toast } } } for (OsmElement e:selection) { if (e instanceof Node && e.getState() != OsmElement.STATE_DELETED) { performEraseNode(activity, (Node)e, false); } } } /** * Splits all ways containing the node will be split at the nodes position * * @param activity activity this method was called from, if null no warnings will be displayed * @param node node to split at */ public synchronized void performSplit(@Nullable final FragmentActivity activity, final Node node) { if (node != null) { // setSelectedNode(node); createCheckpoint(activity, R.string.undo_action_split_ways); displayAttachedObjectWarning(activity, node); // needs to be done before split getDelegator().splitAtNode(node); invalidateMap(); } } /** * Splits a way at a given node * * @param activity activity this method was called from, if null no warnings will be displayed * @param way the way to split * @param node the node at which the way should be split * @return the new way or null if failed */ public synchronized Way performSplit(@Nullable final FragmentActivity activity, final Way way, final Node node) { // setSelectedNode(node); createCheckpoint(activity, R.string.undo_action_split_way); Way result = getDelegator().splitAtNode(way, node); invalidateMap(); return result; } /** * Split a closed way, needs two nodes * * @param activity activity we were called fron * @param way * @param node1 * @param node2 * @return null if split fails, the two ways otherwise */ public synchronized Way[] performClosedWaySplit(@Nullable Activity activity, Way way, Node node1, Node node2, boolean createPolygons) { createCheckpoint(activity, R.string.undo_action_split_way); Way[] result = getDelegator().splitAtNodes(way, node1, node2, createPolygons); invalidateMap(); return result; } /** * Merge two ways. * Ways must be valid (i.e. have at least two nodes) and mergeable * (i.e. have a common start/end node). * * @param activity activity this method was called from, if null no warnings will be displayed * @param mergeInto Way to merge the other way into. This way will be kept. * @param mergeFrom Way to merge into the other. This way will be deleted. * @throws OsmIllegalOperationException */ public synchronized boolean performMerge(@Nullable final FragmentActivity activity, Way mergeInto, Way mergeFrom) throws OsmIllegalOperationException { createCheckpoint(activity, R.string.undo_action_merge_ways); displayAttachedObjectWarning(activity, mergeInto, mergeFrom, true); // needs to be done before merge boolean mergeOK = getDelegator().mergeWays(mergeInto, mergeFrom); invalidateMap(); return mergeOK; } /** * Merge a sorted list of ways * * FIXME if sortedWays does not contain only ways things will crash with a cast exception * @param activity activity this was called from, if null no warnings will be displayed * @param sortedWays list of ways to be merged * @throws OsmIllegalOperationException */ public synchronized boolean performMerge(@Nullable FragmentActivity activity, List<OsmElement> sortedWays) throws OsmIllegalOperationException { createCheckpoint(activity, R.string.undo_action_merge_ways); displayAttachedObjectWarning(activity, sortedWays, true); // needs to be done before merge boolean mergeOK = true; Way previousWay = (Way) sortedWays.get(0); for (int i=1;i<sortedWays.size();i++) { Way nextWay = (Way) sortedWays.get(i); if (!getDelegator().mergeWays(previousWay, nextWay)) { Log.d(DEBUG_TAG,"ways " + previousWay.getDescription() + " and " + nextWay + " caused a merge conflict"); mergeOK = false; // signal that we had a problem somewhere } if (previousWay.getState() == OsmElement.STATE_DELETED) { previousWay = nextWay; } } return mergeOK; } /** * Orthogonalize a way (aka make angles 90°) * * @param activity activity this was called from, if null no warnings will be displayed * @param way way to square */ public void performOrthogonalize(@Nullable FragmentActivity activity, Way way) { if (way != null && way.getNodes().size() < 3) return; ArrayList<Way> ways = new ArrayList<Way>(1); ways.add(way); performOrthogonalize(activity, ways); } /** * Orthogonalize multiple ways at once (aka make angles 90°) * * FIXME this can take a long time and should likely be run async * @param activity activity this was called from, if null no warnings will be displayed * @param ways ways to square */ public synchronized void performOrthogonalize(@Nullable FragmentActivity activity, List<Way> ways) { if (ways==null || ways.size()==0) { return; } createCheckpoint(activity, R.string.undo_action_orthogonalize); getDelegator().orthogonalizeWay(map, ways); invalidateMap(); if (getFilter() != null && showAttachedObjectWarning()) { HashSet<Node> nodes = new HashSet<Node>(); for (Way w:ways) { nodes.addAll(w.getNodes()); } displayAttachedObjectWarning(activity, nodes); } } /** * Replace node in all ways it is a member of with a new node, * leaving node selected, if it already is. Note: relation memberships are not modified * * @param activity activity this was called from, if null no warnings will be displayed * @param node node to extract from any ways it is a member of * @return the new way node or null if the node was not a way node */ public synchronized Node performExtract(@Nullable FragmentActivity activity, final Node node) { if (node != null) { createCheckpoint(activity, R.string.undo_action_extract_node); displayAttachedObjectWarning(activity, node); // this needs to be done -before- we replace the node Node newNode = getDelegator().replaceNode(node); invalidateMap(); return newNode; } return null; } /** * If any ways are close to the node (within the tolerance), return the way. * * @param nodeToJoin * @return the closest way to the node */ public OsmElement findJoinableElement(Node nodeToJoin) { OsmElement closestElement = null; double closestDistance = Double.MAX_VALUE; float jx = lonE7ToX(nodeToJoin.getLon()); float jy = latE7ToY(nodeToJoin.getLat()); // start by looking for the closest nodes for (Node node : getDelegator().getCurrentStorage().getNodes()) { if (node != nodeToJoin) { Double distance = clickDistance(node, jx, jy); if (distance != null && distance < closestDistance && (filter == null || filter.include(node,false))) { closestDistance = distance; closestElement = node; } } } if (closestElement == null) { // fall back to closest ways for (Way way : getDelegator().getCurrentStorage().getWays()) { if (!way.hasNode(nodeToJoin)) { List<Node> wayNodes = way.getNodes(); for (int i = 1, wayNodesSize = wayNodes.size(); i < wayNodesSize; ++i) { Node node1 = wayNodes.get(i - 1); Node node2 = wayNodes.get(i); // TODO only project once per node float node1X = lonE7ToX(node1.getLon()); float node1Y = latE7ToY(node1.getLat()); float node2X = lonE7ToX( node2.getLon()); float node2Y = latE7ToY(node2.getLat()); double distance = isPositionOnLine(jx, jy, node1X, node1Y, node2X, node2Y); if (distance >= 0) { if (distance < closestDistance && (filter == null || filter.include(way,false))) { closestDistance = distance; closestElement = way; } } } } } } return closestElement; } /** * Join a node to a node or way at the point on the way closest to the node. * * @param activity activity this was called from, if null no warnings will be displayed * @param element Node or Way that the node will be joined to. * @param nodeToJoin Node to be joined to the way. * @throws OsmIllegalOperationException */ public synchronized boolean performJoin(@Nullable FragmentActivity activity, OsmElement element, Node nodeToJoin) throws OsmIllegalOperationException { boolean mergeOK = true; if (element instanceof Node) { Node node = (Node)element; createCheckpoint(activity, R.string.undo_action_join); displayAttachedObjectWarning(activity, node,nodeToJoin); // needs to be done before join mergeOK = getDelegator().mergeNodes(node, nodeToJoin); invalidateMap(); } else if (element instanceof Way) { Way way = (Way)element; List<Node> wayNodes = way.getNodes(); for (int i = 1, wayNodesSize = wayNodes.size(); i < wayNodesSize; ++i) { Node node1 = wayNodes.get(i - 1); Node node2 = wayNodes.get(i); float x = lonE7ToX(nodeToJoin.getLon()); float y = latE7ToY(nodeToJoin.getLat()); // TODO only project once per node float node1X = lonE7ToX(node1.getLon()); float node1Y = latE7ToY(node1.getLat()); float node2X = lonE7ToX(node2.getLon()); float node2Y = latE7ToY(node2.getLat()); double distance = isPositionOnLine(x, y, node1X, node1Y, node2X, node2Y); if (distance >= 0) { float[] p = GeoMath.closestPoint(x, y, node1X, node1Y, node2X, node2Y); int lat = yToLatE7(p[1]); int lon = xToLonE7(p[0]); createCheckpoint(activity, R.string.undo_action_join); Node node = null; if (node == null && lat == node1.getLat() && lon == node1.getLon()) { node = node1; } if (node == null && lat == node2.getLat() && lon == node2.getLon()) { node = node2; } if (node == null) { displayAttachedObjectWarning(activity, way,nodeToJoin); // needs to be done before join // move the existing node onto the way and insert it into the way getDelegator().updateLatLon(nodeToJoin, lat, lon); getDelegator().addNodeToWayAfter(node1, nodeToJoin, way); } else { displayAttachedObjectWarning(activity, node,nodeToJoin); // needs to be done before join // merge node into tgtNode mergeOK = getDelegator().mergeNodes(node, nodeToJoin); } invalidateMap(); break; // need to leave loop !!! } } } return mergeOK; } /** * Unjoin ways joined by the given node. * * @param activity activity this was called from, if null no warnings will be displayed * @param activity activity this was called from, if null no warnings will be displayed * @param node Node that is joining the ways to be unjoined. */ public synchronized void performUnjoin(@Nullable FragmentActivity activity,Node node) { createCheckpoint(activity, R.string.undo_action_unjoin_ways); displayAttachedObjectWarning(activity, node); // needs to be done before unjoin getDelegator().unjoinWays(node); invalidateMap(); } /** * Reverse a way * * @param activity activity we were called from * @param way the way to reverse * @return true if reverseWay returned true, implying that tags had to be reversed */ public synchronized boolean performReverse(@Nullable Activity activity, Way way) { createCheckpoint(activity, R.string.undo_action_reverse_way); boolean hadToReverse = getDelegator().reverseWay(way); invalidateMap(); return hadToReverse; } public synchronized void performAppendStart(Way way, Node node) { setSelectedNode(node); setSelectedWay(way); invalidateMap(); } public synchronized void performAppendStart(OsmElement element) { Way lSelectedWay = null; Node lSelectedNode = null; if (element != null) { if (element instanceof Node) { lSelectedNode = (Node) element; List<Way> ways = getDelegator().getCurrentStorage().getWays(lSelectedNode); // TODO Resolve possible multiple ways that end at the node for (Way way : ways) { if (way.isEndNode(lSelectedNode)) { lSelectedWay = way; break; } } if (lSelectedWay == null) { lSelectedNode = null; } } } performAppendStart(lSelectedWay, lSelectedNode); } /** * * @param activity activity this method was called from, if null no warnings will be displayed * @param x * @param y * @throws OsmIllegalOperationException */ public synchronized void performAppendAppend(@Nullable final Activity activity, final float x, final float y) throws OsmIllegalOperationException { Log.d(DEBUG_TAG,"performAppendAppend"); createCheckpoint(activity, R.string.undo_action_append); Node lSelectedNode = getSelectedNode(); Way lSelectedWay = getSelectedWay(); Node node = getClickedNodeOrCreatedWayNode(x, y); if (node == lSelectedNode) { lSelectedNode = null; lSelectedWay = null; } else if (lSelectedWay != null){ // may have been deselected before we got here if (node == null) { int lat = yToLatE7(y); int lon = xToLonE7(x); node = getDelegator().getFactory().createNodeWithNewId(lat, lon); getDelegator().insertElementSafe(node); if (!getDelegator().isInDownload(lat, lon)) { // warning toast Snack.barWarningShort(activity, R.string.toast_outside_of_download); } } try { getDelegator().appendNodeToWay(lSelectedNode, node, lSelectedWay); } catch (OsmIllegalOperationException e) { getDelegator().removeNode(node); throw new OsmIllegalOperationException(e); } lSelectedNode = node; } setSelectedNode(lSelectedNode); setSelectedWay(lSelectedWay); invalidateMap(); } /** * Tries to locate the selected node. If x,y lays on a way, a new node at this location will be created, stored in * storage and returned. * * @param x the x screen coordinate * @param y the y screen coordinate * @return the selected node or the created node, if x,y lays on a way. Null if any node or way was selected. * @throws OsmIllegalOperationException */ private synchronized Node getClickedNodeOrCreatedWayNode(final float x, final float y) throws OsmIllegalOperationException { return getClickedNodeOrCreatedWayNode(null,x,y, false); } /** * Tries to locate the selected node. If x,y lays on a way, a new node at this location will be created, stored in * storage and returned. * * @param ways list of candidate ways or null for all * @param x the x screen coordinate * @param y the y screen coordinate * @param forceNew do not return existing nodes in tolerance range * @return the selected node or the created node, if x,y lays on a way. Null if any node or way was selected. * @throws OsmIllegalOperationException */ private synchronized Node getClickedNodeOrCreatedWayNode(List<Way>ways,final float x, final float y, boolean forceNew) throws OsmIllegalOperationException { Node node = null; if (!forceNew) { node = getClickedNode(x, y); if (node != null) { return node; } } if (ways==null) { ways=getDelegator().getCurrentStorage().getWays(); } Node savedNode1 = null; Node savedNode2 = null; ArrayList<Way> savedWays = new ArrayList<Way>(); ArrayList<Boolean> savedWaysSameDirection = new ArrayList<Boolean>(); double savedDistance = Double.MAX_VALUE; //create a new node on a way for (Way way : ways) { if (filter != null) { if (!filter.include(way, isSelected(way))) { continue; } } List<Node> wayNodes = way.getNodes(); for (int k = 1, wayNodesSize = wayNodes.size(); k < wayNodesSize; ++k) { Node node1 = wayNodes.get(k - 1); Node node2 = wayNodes.get(k); // TODO only project once per node float node1X = lonE7ToX(node1.getLon()); float node1Y = latE7ToY(node1.getLat()); float node2X = lonE7ToX(node2.getLon()); float node2Y = latE7ToY(node2.getLat()); double distance = isPositionOnLine(x, y, node1X, node1Y, node2X, node2Y); if (distance >= 0) { if ((savedNode1 == null && savedNode2 == null) || distance < savedDistance) { savedNode1 = node1; savedNode2 = node2; savedDistance = distance; savedWays.clear(); savedWays.add(way); savedWaysSameDirection.clear(); savedWaysSameDirection.add(true); } else if ((node1==savedNode1 && node2==savedNode2)) { savedWays.add(way); savedWaysSameDirection.add(true); } else if ((node1==savedNode2 && node2==savedNode1)) { savedWays.add(way); savedWaysSameDirection.add(false); } } } } // way(s) found in tolerance range if (savedNode1 != null && savedNode2 != null) { node = createNodeOnWay(savedNode1, savedNode2, x, y); if (node != null) { getDelegator().insertElementSafe(node); try { for (int i=0;i<savedWays.size();i++) { if (savedWaysSameDirection.get(i)) { getDelegator().addNodeToWayAfter(savedNode1, node, savedWays.get(i)); } else { getDelegator().addNodeToWayAfter(savedNode2, node, savedWays.get(i)); } } } catch (OsmIllegalOperationException e) { getDelegator().removeNode(node); throw new OsmIllegalOperationException(e); } } } return node; } /** * Creates a new node at x,y between node1 and node2. When x,y does not lay on the line between node1 and node2 it * will return null. * * @param node1 the first node * @param node2 the second node * @param x screen coordinate where the new node shall be created. * @param y screen coordinate where the new node shall be created. * @return a new created node at lon/lat corresponding to x,y. When x,y does not lay on the line between node1 and * node2 it will return null. */ private synchronized Node createNodeOnWay(final Node node1, final Node node2, final float x, final float y) { //Nodes have to be converted to screen-coordinates, due to a better tolerance-check. float node1X = lonE7ToX(node1.getLon()); float node1Y = latE7ToY(node1.getLat()); float node2X = lonE7ToX(node2.getLon()); float node2Y = latE7ToY(node2.getLat()); //At first, we check if the x,y is in the bounding box clamping by node1 and node2. if (isPositionOnLine(x, y, node1X, node1Y, node2X, node2Y) >= 0) { float[] p = GeoMath.closestPoint(x, y, node1X, node1Y, node2X, node2Y); int lat = yToLatE7(p[1]); int lon = xToLonE7(p[0]); return getDelegator().getFactory().createNodeWithNewId(lat, lon); } return null; } /** * Checks if the x,y-position plus the tolerance is on a line between node1(x,y) and node2(x,y). * * To avoid the typical two time calculation of the distance we actually return it * @param x screen X coordinate of the position * @param y screen Y coordinate of the position * @param node1X screen X coordinate of node1 * @param node1Y screen Y coordinate of node1 * @param node2X screen X coordinate of node2 * @param node2Y screen Y coordinate of node2 * @return distance >= 0, when x,y plus way-tolerance lays on the line between node1 and node2. */ private double isPositionOnLine(final float x, final float y, final float node1X, final float node1Y, final float node2X, final float node2Y) { float tolerance = DataStyle.getCurrent().wayToleranceValue / 2f; //noinspection SuspiciousNameCombination if (GeoMath.isBetween(x, node1X, node2X, tolerance) && GeoMath.isBetween(y, node1Y, node2Y, tolerance)) { double distance = GeoMath.getLineDistance(x, y, node1X, node1Y, node2X, node2Y); return distance < tolerance ? distance : -1D; } return -1D; } /** * Translates the {@link #viewBox} in the direction of x/y's next border. * * @param x screen-coordinate * @param y screen-coordinate */ private void translateOnBorderTouch(final float x, final float y) { int translationOnBorderTouch = (int) (viewBox.getWidth() * BORDER_TOCH_TRANSLATION_FACTOR); try { if (x > map.getWidth() - PADDING_ON_BORDER_TOUCH) { viewBox.translate(map, translationOnBorderTouch, 0); } else if (x < PADDING_ON_BORDER_TOUCH) { viewBox.translate(map, -translationOnBorderTouch, 0); } if (y > map.getHeight() - PADDING_ON_BORDER_TOUCH) { viewBox.translate(map, 0, -translationOnBorderTouch); } else if (y < PADDING_ON_BORDER_TOUCH) { viewBox.translate(map, 0, translationOnBorderTouch); } } catch (OsmException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * Loads the area defined by mapBox from the OSM-Server. * * @param activity activity this was called from * @param mapBox Box defining the area to be loaded. * @param add if true add this data to existing * @param postLoadHandler handler to execute after successful download */ public synchronized void downloadBox(final FragmentActivity activity, final BoundingBox mapBox, final boolean add, final PostAsyncActionHandler postLoadHandler) { try { mapBox.makeValidForApi(); } catch (OsmException e1) { Log.e(DEBUG_TAG,"downloadBox invalid download box"); ErrorAlert.showDialog(activity,ErrorCodes.INVALID_BOUNDING_BOX); return; } final PostMergeHandler postMerge = new PostMergeHandler(){ @Override public void handler(OsmElement e) { e.hasProblem(activity); } }; new AsyncTask<Boolean, Void, Integer>() { @Override protected void onPreExecute() { Progress.showDialog(activity, Progress.PROGRESS_LOADING); } @Override protected Integer doInBackground(Boolean... arg) { int result = 0; try { Server server = prefs.getServer(); if (server.hasReadOnly()) { server.getReadOnlyCapabilities(); if (!(server.readOnlyApiAvailable() && server.readOnlyReadableDB())) { return ErrorCodes.API_OFFLINE; } // try to get write capabilities in any case FIXME unclear what we should do if the write server is not available server.getCapabilities(); } else { server.getCapabilities(); if (!(server.apiAvailable() && server.readableDB())) { return ErrorCodes.API_OFFLINE; } } final OsmParser osmParser = new OsmParser(); final InputStream in = prefs.getServer().getStreamForBox(activity, mapBox); try { long startTime = System.currentTimeMillis(); osmParser.start(in); Log.d(DEBUG_TAG,"downloadBox downloaded and parsed input in " + (System.currentTimeMillis()-startTime) + "ms"); if (arg[0]) { // incremental load if (!getDelegator().mergeData(osmParser.getStorage(),postMerge)) { result = ErrorCodes.DATA_CONFLICT; } else { if (mapBox != null) { // if we are simply expanding the area no need keep the old bounding boxes List<BoundingBox> origBbs = getDelegator().getBoundingBoxes(); List<BoundingBox> bbs = new ArrayList<BoundingBox>(origBbs); for (BoundingBox bb:bbs) { if (mapBox.contains(bb)) { origBbs.remove(bb); } } getDelegator().addBoundingBox(mapBox); } } } else { // replace data with new download getDelegator().reset(false); getDelegator().setCurrentStorage(osmParser.getStorage()); // this sets dirty flag if (mapBox != null) { Log.d(DEBUG_TAG,"downloadBox setting original bbox"); getDelegator().setOriginalBox(mapBox); } } Map map = activity instanceof Main ? ((Main)activity).getMap() : null; if (map != null) { viewBox.setBorders(map, mapBox != null ? mapBox : getDelegator().getLastBox()); // set to current or previous } } finally { SavingHelper.close(in); } } catch (SAXException e) { Log.e(DEBUG_TAG, "downloadBox problem parsing", e); Exception ce = e.getException(); if ((ce instanceof StorageException) && ((StorageException)ce).getCode() == StorageException.OOM) { result = ErrorCodes.OUT_OF_MEMORY; } else { result = ErrorCodes.INVALID_DATA_RECEIVED; } if (getDelegator().getBoundingBoxes().contains(mapBox)) { // remove if download failed getDelegator().deleteBoundingBox(mapBox); } } catch (ParserConfigurationException e) { // crash and burn // TODO this seems to happen when the API call returns text from a proxy or similar intermediate network device... need to display what we actually got Log.e(DEBUG_TAG, "downloadBox problem parsing", e); result = ErrorCodes.INVALID_DATA_RECEIVED; if (getDelegator().getBoundingBoxes().contains(mapBox)) { // remove if download failed getDelegator().deleteBoundingBox(mapBox); } } catch (OsmServerException e) { result = e.getErrorCode(); Log.e(DEBUG_TAG, "downloadBox problem downloading", e); if (getDelegator().getBoundingBoxes().contains(mapBox)) { // remove if download failed getDelegator().deleteBoundingBox(mapBox); } } catch (IOException e) { if (e instanceof SSLProtocolException) { result = ErrorCodes.SSL_HANDSHAKE; } else { result = ErrorCodes.NO_CONNECTION; } Log.e(DEBUG_TAG, "downloadBox problem downloading", e); if (getDelegator().getBoundingBoxes().contains(mapBox)) { // remove if download failed getDelegator().deleteBoundingBox(mapBox); } } return result; } @Override protected void onPostExecute(Integer result) { Progress.dismissDialog(activity, Progress.PROGRESS_LOADING); Map map = activity instanceof Main ? ((Main)activity).getMap() : null; if (map != null) { try { viewBox.setRatio(map, (float)map.getWidth() / (float)map.getHeight()); } catch (OsmException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (result != 0) { if (result == ErrorCodes.OUT_OF_MEMORY) { System.gc(); if (getDelegator().isDirty()) { result = ErrorCodes.OUT_OF_MEMORY_DIRTY; } } try { if (!activity.isFinishing()) { ErrorAlert.showDialog(activity,result); } } catch (Exception ex) { // now and then this seems to throw a WindowManager.BadTokenException, however report, don't crash ACRA.getErrorReporter().putCustomData("CAUSE",ex.getMessage()); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(ex); } if (postLoadHandler != null) { postLoadHandler.onError(); } } else { if (postLoadHandler != null) { postLoadHandler.onSuccess(); } } if (map != null) { DataStyle.updateStrokes(strokeWidth(viewBox.getWidth())); invalidateMap(); } activity.supportInvalidateOptionsMenu(); } }.execute(add); } /** * Loads the area defined by mapBox from the OSM-Server. Static version for auto download * FIXME try to reduce the code duplication here * @param context android context * @param mapBox Box defining the area to be loaded. * @param add if true add this data to existing * @param auto download is being done automatically, try not mess up/move the display */ public synchronized void autoDownloadBox(final Context context, final Server server, final BoundingBox mapBox) { try { mapBox.makeValidForApi(); } catch (OsmException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } // TODO remove this? and replace with better error messaging final PostMergeHandler postMerge = new PostMergeHandler(){ @Override public void handler(OsmElement e) { e.hasProblem(context); } }; new AsyncTask<Void, Void, Integer>() { @Override protected void onPreExecute() { } @Override protected Integer doInBackground(Void... arg) { int result = 0; try { final OsmParser osmParser = new OsmParser(); final InputStream in = server.getStreamForBox(context, mapBox); try { osmParser.start(in); if (!getDelegator().mergeData(osmParser.getStorage(),postMerge)) { result = ErrorCodes.DATA_CONFLICT; } else { if (mapBox != null) { // if we are simply expanding the area no need keep the old bounding boxes List<BoundingBox> origBbs = getDelegator().getBoundingBoxes(); if ( origBbs.size() == 1) { // replace original BB if still present if (getDelegator().isEmpty()) { origBbs.clear(); } } List<BoundingBox> bbs = new ArrayList<BoundingBox>(origBbs); for (BoundingBox bb:bbs) { if (mapBox.contains(bb)) { origBbs.remove(bb); } } getDelegator().addBoundingBox(mapBox); } } } finally { SavingHelper.close(in); } } catch (SAXException e) { Log.e("Vespucci", "Problem parsing", e); Exception ce = e.getException(); if ((ce instanceof StorageException) && ((StorageException)ce).getCode() == StorageException.OOM) { result = ErrorCodes.OUT_OF_MEMORY; } else { result = ErrorCodes.INVALID_DATA_RECEIVED; } if (getDelegator().getBoundingBoxes().contains(mapBox)) { // remove if download failed getDelegator().deleteBoundingBox(mapBox); } } catch (ParserConfigurationException e) { // crash and burn // TODO this seems to happen when the API call returns text from a proxy or similar intermediate network device... need to display what we actually got Log.e("Vespucci", "Problem parsing", e); result = ErrorCodes.INVALID_DATA_RECEIVED; if (getDelegator().getBoundingBoxes().contains(mapBox)) { // remove if download failed getDelegator().deleteBoundingBox(mapBox); } } catch (OsmServerException e) { result = e.getErrorCode(); Log.e("Vespucci", "Problem downloading", e); if (getDelegator().getBoundingBoxes().contains(mapBox)) { // remove if download failed getDelegator().deleteBoundingBox(mapBox); } } catch (IOException e) { result = ErrorCodes.NO_CONNECTION; Log.e("Vespucci", "Problem downloading", e); if (getDelegator().getBoundingBoxes().contains(mapBox)) { // remove if download failed getDelegator().deleteBoundingBox(mapBox); } } return result; } @Override protected void onPostExecute(Integer result) { } }.execute(); } /** * Calls the actual downloadBox function using the current map view as the * bounding box for the download. * * @param activity activity this was called from * @param add add if true add this data to existing * @see #downloadBox(activity, BoundingBox, boolean) */ void downloadCurrent(final FragmentActivity activity, boolean add) { Log.d(DEBUG_TAG,"viewBox: " + viewBox.getBottom() + " " + viewBox.getLeft() + " " + viewBox.getTop() + " " + viewBox.getRight()); downloadBox(activity, viewBox.copy(),add, null); } /** * Re-downloads the same area as last time * * @param activity activity this was called from * @see #downloadBox(activity, BoundingBox, boolean) */ void downloadLast(final FragmentActivity activity) { getDelegator().reset(false); for (BoundingBox box:getDelegator().getBoundingBoxes()) { if (box != null && box.isValidForApi()) { downloadBox(activity, box, true, null); } } } /** * Return a single element from the API, does not merge into storage, synchronous * * @param activity the activity that called us * @param type type of the element * @param id id of the element * @return element if successful, null if not */ public synchronized OsmElement getElement(@Nullable final Activity activity, final String type, final long id) { class GetElementTask extends AsyncTask<Void, Void, OsmElement> { int result = 0; @Override protected void onPreExecute() { } @Override protected OsmElement doInBackground(Void... arg) { OsmElement element = null; try { final OsmParser osmParser = new OsmParser(); final InputStream in = prefs.getServer().getStreamForElement(activity, Way.NAME.equals(type)?"full":null, type, id); try { osmParser.start(in); element = osmParser.getStorage().getOsmElement(type, id); } finally { SavingHelper.close(in); } } catch (SAXException e) { Log.e(DEBUG_TAG, "getElement problem parsing", e); Exception ce = e.getException(); if ((ce instanceof StorageException) && ((StorageException)ce).getCode() == StorageException.OOM) { result = ErrorCodes.OUT_OF_MEMORY; } else { result = ErrorCodes.INVALID_DATA_RECEIVED; } } catch (ParserConfigurationException e) { // crash and burn // TODO this seems to happen when the API call returns text from a proxy or similar intermediate network device... need to display what we actually got Log.e(DEBUG_TAG, "getElement problem parsing", e); result = ErrorCodes.INVALID_DATA_RECEIVED; } catch (OsmServerException e) { Log.e(DEBUG_TAG, "getElement problem downloading", e); } catch (IOException e) { result = ErrorCodes.NO_CONNECTION; Log.e(DEBUG_TAG, "getElement problem downloading", e); } return element; } @Override protected void onPostExecute(OsmElement result) { // potentially do something if there is an error } } GetElementTask loader = new GetElementTask(); loader.execute(); try { return loader.get(20, TimeUnit.SECONDS); } catch (InterruptedException e) { return null; } catch (ExecutionException e) { return null; } catch (TimeoutException e) { return null; } } /** * Download a single element from the API and merge * * @param ctx Android context * @param type type of the element * @param id OSM id of the element * @param relationFull if we are downloading a relation download with full option * @param withParents download parent relations * @param postLoadHandler callback to execute after download completes if null method waits for download to finish * @return an error code 0 for success */ public synchronized int downloadElement(@NonNull final Context ctx, @NonNull final String type, final long id, final boolean relationFull, final boolean withParents, @Nullable final PostAsyncActionHandler postLoadHandler) { class DownLoadElementTask extends AsyncTask<Void, Void, Integer> { @Override protected void onPreExecute() { } @Override protected Integer doInBackground(Void... arg) { int result = 0; try { final Server server = prefs.getServer(); final OsmParser osmParser = new OsmParser(); // TODO this currently does not retrieve ways the node may be a member of // we always retrieve ways with nodes, relations "full" is optional InputStream in = server.getStreamForElement(ctx, (type.equals(Relation.NAME) && relationFull) || type.equals(Way.NAME)? "full" : null, type, id); try { osmParser.start(in); } finally { SavingHelper.close(in); } if (withParents) { // optional retrieve relations the element is a member of in = server.getStreamForElement(ctx, "relations", type, id); try { osmParser.start(in); } finally { SavingHelper.close(in); } } if (!getDelegator().mergeData(osmParser.getStorage(),null)) { // FIXME need to check if providing a handler makes sense here result = ErrorCodes.DATA_CONFLICT; } } catch (SAXException e) { Log.e(DEBUG_TAG, "downloadElement problem parsing", e); Exception ce = e.getException(); if ((ce instanceof StorageException) && ((StorageException)ce).getCode() == StorageException.OOM) { result = ErrorCodes.OUT_OF_MEMORY; } else { result = ErrorCodes.INVALID_DATA_RECEIVED; } } catch (ParserConfigurationException e) { // crash and burn // TODO this seems to happen when the API call returns text from a proxy or similar intermediate network device... need to display what we actually got Log.e(DEBUG_TAG, "downloadElement problem parsing", e); result = ErrorCodes.INVALID_DATA_RECEIVED; } catch (OsmServerException e) { Log.e(DEBUG_TAG, "downloadElement problem downloading", e); } catch (IOException e) { result = ErrorCodes.NO_CONNECTION; Log.e(DEBUG_TAG, "downloadElement problem downloading", e); } return result; } @Override protected void onPostExecute(Integer result) { if (result == 0) { if (postLoadHandler != null) { postLoadHandler.onSuccess(); } } else { if (postLoadHandler != null) { postLoadHandler.onError(); } } } } DownLoadElementTask loader = new DownLoadElementTask(); loader.execute(); if (postLoadHandler == null) { try { return loader.get(20, TimeUnit.SECONDS); } catch (InterruptedException e) { return -1; } catch (ExecutionException e) { return -1; } catch (TimeoutException e) { return -1; } } else { return 0; } } /** * Return multiple elements of the same type from the API and merge them in to our data * Since this doesn't return way nodes this method probably doesn't make sense * * @param type * @param id * @return */ // void downloadElements(final String type, long[] ids) { // // class MyTask extends AsyncTask<Void, Void, Integer> { // int result = 0; // // @Override // protected void onPreExecute() { // Application.mainActivity.showDialog(DialogFactory.PROGRESS_LOADING); // } // // @Override // protected Integer doInBackground(Void... arg) { // try { // final OsmParser osmParser = new OsmParser(); // final InputStream in = prefs.getServer().getStreamForElements(type, ids); // try { // osmParser.start(in); // if (!getDelegator().mergeData(osmParser.getStorage())) { // result = DialogFactory.DATA_CONFLICT; // } // } finally { // SavingHelper.close(in); // } // } catch (SAXException e) { // Log.e("Vespucci", "Problem parsing", e); // Exception ce = e.getException(); // if ((ce instanceof StorageException) && ((StorageException)ce).getCode() == StorageException.OOM) { // result = DialogFactory.OUT_OF_MEMORY; // } else { // result = DialogFactory.INVALID_DATA_RECEIVED; // } // } catch (ParserConfigurationException e) { // // crash and burn // // TODO this seems to happen when the API call returns text from a proxy or similar intermediate network device... need to display what we actually got // Log.e("Vespucci", "Problem parsing", e); // result = DialogFactory.INVALID_DATA_RECEIVED; // } catch (OsmServerException e) { // Log.e("Vespucci", "Problem downloading", e); // } catch (IOException e) { // result = DialogFactory.NO_CONNECTION; // Log.e("Vespucci", "Problem downloading", e); // } // return result; // } // // @Override // protected void onPostExecute(Integer result) { // // try { // Application.mainActivity.dismissDialog(DialogFactory.PROGRESS_LOADING); // } catch (IllegalArgumentException e) { // // Avoid crash if dialog is already dismissed // Log.d("Logic", "", e); // } // // if (result != 0) { // if (result == DialogFactory.OUT_OF_MEMORY) { // System.gc(); // if (getDelegator().isDirty()) { // result = DialogFactory.OUT_OF_MEMORY_DIRTY; // } // } // try { // if (!Application.mainActivity.isFinishing()) { // Application.mainActivity.showDialog(result); // } // } catch (Exception ex) { // now and then this seems to throw a WindowManager.BadTokenException, however report, don't crash // ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); // ACRA.getErrorReporter().handleException(ex); // } // } // invalidateMap(); // // UndoStorage.updateIcon(); // } // // }; // MyTask loader = new MyTask(); // loader.execute(); // } /** * Remove an element if it is deleted on the server * <p> * Element is deleted on server, delete locally but don't upload * A bit iffy because of memberships in other objects * * @param activity activity we were called from * @param e element to delete */ public synchronized void updateToDeleted(@Nullable Activity activity, OsmElement e) { createCheckpoint(activity, R.string.undo_action_fix_conflict); if (e.getName().equals(Node.NAME)) { getDelegator().removeNode((Node)e); } else if (e.getName().equals(Way.NAME)) { getDelegator().removeWay((Way)e); } else if (e.getName().equals(Relation.NAME)) { getDelegator().removeRelation((Relation)e); } getDelegator().removeFromUpload(e); invalidateMap(); } /** * Read a file in (J)OSM format from device * * @param activity activity that called this * @param uri uri of file to load * @param add unused currently * @throws FileNotFoundException */ public void readOsmFile(@NotNull final FragmentActivity activity, final Uri uri, boolean add) throws FileNotFoundException { readOsmFile(activity, uri, add, null); } /** * Read a file in (J)OSM format from device * * @param activity activity that called this * @param uri uri of file to load * @param add unused currently * @param postLoad callback to execute once file is loaded * @throws FileNotFoundException */ public void readOsmFile(@NotNull final FragmentActivity activity, final Uri uri, boolean add, final PostAsyncActionHandler postLoad) throws FileNotFoundException { final InputStream is; if (uri.getScheme().equals("file")) { is = new FileInputStream(new File(uri.getPath())); } else { ContentResolver cr = activity.getContentResolver(); is = cr.openInputStream(uri); } readOsmFile(activity, is, add, postLoad); } /** * Read a stream in (J)OSM format * * @param activity activity that called this * @param is input * @param add unused currently * @param postLoad callback to execute once stream has been loaded * @throws FileNotFoundException */ public void readOsmFile(@NotNull final FragmentActivity activity, final InputStream is, boolean add, final PostAsyncActionHandler postLoad) { final Map map = activity instanceof Main ? ((Main)activity).getMap() : null; new AsyncTask<Boolean, Void, Integer>() { @Override protected void onPreExecute() { Progress.showDialog(activity, Progress.PROGRESS_LOADING); } @Override protected Integer doInBackground(Boolean... arg) { int result = 0; try { final OsmParser osmParser = new OsmParser(); osmParser.clearBoundingBoxes(); // this removes the default bounding box final InputStream in = new BufferedInputStream(is); try { osmParser.start(in); StorageDelegator sd = getDelegator(); sd.reset(false); sd.setCurrentStorage(osmParser.getStorage()); // this sets dirty flag sd.fixupApiStorage(); if (map != null) { viewBox.setBorders(map, getDelegator().getLastBox()); // set to current or previous } } finally { SavingHelper.close(in); } } catch (SAXException e) { Log.e(DEBUG_TAG, "Problem parsing", e); Exception ce = e.getException(); if ((ce instanceof StorageException) && ((StorageException)ce).getCode() == StorageException.OOM) { result = ErrorCodes.OUT_OF_MEMORY; } else { result = ErrorCodes.INVALID_DATA_READ; } } catch (ParserConfigurationException e) { // crash and burn Log.e(DEBUG_TAG, "Problem parsing", e); result = ErrorCodes.INVALID_DATA_READ; } catch (IOException e) { result = ErrorCodes.NO_CONNECTION; Log.e(DEBUG_TAG, "Problem reading", e); } return result; } @Override protected void onPostExecute(Integer result) { Progress.dismissDialog(activity, Progress.PROGRESS_LOADING); if (map != null) { try { viewBox.setRatio(map, (float)map.getWidth() / (float)map.getHeight()); } catch (OsmException e) { // TODO Auto-generated catch block e.printStackTrace(); } DataStyle.updateStrokes(strokeWidth(viewBox.getWidth())); } if (result != 0) { if (result == ErrorCodes.OUT_OF_MEMORY) { System.gc(); if (getDelegator().isDirty()) { result = ErrorCodes.OUT_OF_MEMORY_DIRTY; } } try { if (!activity.isFinishing()) { ErrorAlert.showDialog(activity,result); } } catch (Exception ex) { // now and then this seems to throw a WindowManager.BadTokenException, however report, don't crash ACRA.getErrorReporter().putCustomData("CAUSE",ex.getMessage()); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(ex); } if (postLoad != null) { postLoad.onError(); } } else { if (postLoad != null) { postLoad.onSuccess(); } } invalidateMap(); activity.supportInvalidateOptionsMenu(); } }.execute(add); } /** * Write data to a file in (J)OSM compatible format, * if fileName contains directories these are created, otherwise it is stored in the standard public dir * * @param fileName path of the file to save to * @param postSaveHandler if not null executes code after saving */ public void writeOsmFile(@NotNull final FragmentActivity activity, @NonNull final String fileName, @Nullable final PostAsyncActionHandler postSaveHandler) { new AsyncTask<Void, Void, Integer>() { @Override protected void onPreExecute() { Progress.showDialog(activity, Progress.PROGRESS_SAVING); } @Override protected Integer doInBackground(Void... arg) { int result = 0; try { File outfile = new File(fileName); String parent = outfile.getParent(); if (parent == null) { // no directory specified, save to standard location outfile = new File(FileUtil.getPublicDirectory(), fileName); } else { // ensure directory exists File outdir = new File(parent); //noinspection ResultOfMethodCallIgnored outdir.mkdirs(); if (!outdir.isDirectory()) { throw new IOException("Unable to create directory " + outdir.getPath()); } } Log.d(DEBUG_TAG,"Saving to " + outfile.getPath()); FileOutputStream fout = null; OutputStream out = null; try { fout = new FileOutputStream(outfile); out = new BufferedOutputStream(fout); getDelegator().save(out); } catch (IllegalArgumentException e) { result = ErrorCodes.FILE_WRITE_FAILED; Log.e(DEBUG_TAG, "Problem writing", e); } catch (IllegalStateException e) { result = ErrorCodes.FILE_WRITE_FAILED; Log.e(DEBUG_TAG, "Problem writing", e); } catch (XmlPullParserException e) { result = ErrorCodes.FILE_WRITE_FAILED; Log.e(DEBUG_TAG, "Problem writing", e); } finally { SavingHelper.close(out); SavingHelper.close(fout); } } catch (IOException e) { result = ErrorCodes.FILE_WRITE_FAILED; Log.e(DEBUG_TAG, "Problem writing", e); } return result; } @Override protected void onPostExecute(Integer result) { Progress.dismissDialog(activity, Progress.PROGRESS_SAVING); Map map = activity instanceof Main ? ((Main)activity).getMap() : null; if (map != null) { try { viewBox.setRatio(map, (float)map.getWidth() / (float)map.getHeight()); } catch (OsmException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (result != 0) { if (result == ErrorCodes.OUT_OF_MEMORY) { System.gc(); if (getDelegator().isDirty()) { result = ErrorCodes.OUT_OF_MEMORY_DIRTY; } } if (!activity.isFinishing()) { ErrorAlert.showDialog(activity,result); } if (postSaveHandler != null) { postSaveHandler.onError(); } } else { if (postSaveHandler != null) { postSaveHandler.onSuccess(); } } } }.execute(); } /** * Saves to a file (synchronously) * * @param activity activity that we were called from */ synchronized void save(final Activity activity) { try { getDelegator().writeToFile(activity); App.getTaskStorage().writeToFile(activity); } catch (IOException e) { Log.e(DEBUG_TAG, "Problem saving", e); } } /** * Saves to a file (asynchronously) * * @param activity activity that we were called from */ void saveAsync(final Activity activity) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { save(activity); // the disadvantage of saving async is that something might have // changed during the write .... so we force the dirty flags on getDelegator().dirty(); App.getTaskStorage().setDirty(); return null; } }.execute(); } /** * Saves the current editing state (selected objects, editing mode, etc) to file. */ void saveEditingState(Main main) { TileLayerServer osmts = map.getOpenStreetMapTilesOverlay().getRendererInfo(); EditState editState = new EditState(main, this, osmts, main.getImageFileName(), viewBox, main.getFollowGPS()); new SavingHelper<EditState>().save(main,EDITSTATE_FILENAME, editState, false); } /** * Loads the current editing state (selected objects, editing mode, etc) from file. * * @param main instance of main to setup * @param setViewBox set the view box if true */ void loadEditingState(Main main, boolean setViewBox) { EditState editState = new SavingHelper<EditState>().load(main,EDITSTATE_FILENAME, false); if(editState != null) { // editState.setOffset(map.getOpenStreetMapTilesOverlay().getRendererInfo()); editState.setMiscState(main, this); editState.setSelected(main, this); if (setViewBox) { editState.setViewBox(this,map); } } } /** * Loads data from a file in the background. * * @param context */ void loadStateFromFile(@NonNull final FragmentActivity activity, final PostAsyncActionHandler postLoad) { final int READ_FAILED = 0; final int READ_OK = 1; final int READ_BACKUP = 2; final Map map = activity instanceof Main ? ((Main)activity).getMap() : null; AsyncTask<Void, Void, Integer> loader = new AsyncTask<Void, Void, Integer>() { final AlertDialog progress = ProgressDialog.get(activity, Progress.PROGRESS_LOADING); @Override protected void onPreExecute() { progress.show(); Log.d(DEBUG_TAG, "loadFromFile onPreExecute"); } @Override protected Integer doInBackground(Void... v) { if (getDelegator().readFromFile(activity)) { if (map != null) { viewBox.setBorders(map, getDelegator().getLastBox()); } return Integer.valueOf(READ_OK); } else if (getDelegator().readFromFile(activity,StorageDelegator.FILENAME + ".backup")) { getDelegator().dirty(); // we need to overwrite the saved state asap if (map != null) { viewBox.setBorders(map, getDelegator().getLastBox()); } return Integer.valueOf(READ_BACKUP); } return Integer.valueOf(READ_FAILED); } @Override protected void onPostExecute(Integer result) { Log.d(DEBUG_TAG, "loadFromFile onPostExecute"); try { progress.dismiss(); } catch (Exception ex) { Log.e(DEBUG_TAG, "loadFromFile dismiss dialog failed with " + ex); } if (result.intValue() != READ_FAILED) { Log.d(DEBUG_TAG, "loadfromFile: File read correctly"); if (map != null) { try { viewBox.setRatio(map, (float)map.getWidth() / (float)map.getHeight()); } catch (Exception e) { // invalid dimensions of similar error try { viewBox.setBorders(map, new BoundingBox(-180.0,-GeoMath.MAX_LAT,180.0,GeoMath.MAX_LAT)); } catch (OsmException e1) { // Can't happen? e1.printStackTrace(); } } DataStyle.updateStrokes(STROKE_FACTOR / viewBox.getWidth()); // safety measure if not done in loadEiditngState loadEditingState((Main) activity, true); } if (postLoad != null) { postLoad.onSuccess(); } if (map != null) { invalidateMap(); } // this updates the Undo icon if present activity.supportInvalidateOptionsMenu(); if (result.intValue() == READ_BACKUP) { Snack.barError(activity, R.string.toast_used_backup); } } else { Log.d(DEBUG_TAG, "loadfromFile: File read failed"); Intent intent = new Intent(activity, BoxPicker.class); activity.startActivityForResult(intent, Main.REQUEST_BOUNDING_BOX); Snack.barError(activity, R.string.toast_state_file_failed); if (postLoad != null) { postLoad.onError(); } } } }; loader.execute(); } /** * Loads the saved task state from a file in the background. * * @param activity the activity calling this method */ void loadBugsFromFile(@NonNull final Activity activity, final PostAsyncActionHandler postLoad) { final int READ_FAILED = 0; final int READ_OK = 1; final int READ_BACKUP = 2; AsyncTask<Void, Void, Integer> loader = new AsyncTask<Void, Void, Integer>() { @Override protected void onPreExecute() { Log.d(DEBUG_TAG, "loadBugsFromFile onPreExecute"); } @Override protected Integer doInBackground(Void... v) { if (App.getTaskStorage().readFromFile(activity)) { // viewBox.setBorders(getDelegator().getLastBox()); return Integer.valueOf(READ_OK); } return Integer.valueOf(READ_FAILED); } @Override protected void onPostExecute(Integer result) { Log.d(DEBUG_TAG, "loadBugsFromFile onPostExecute"); if (result.intValue() != READ_FAILED) { Log.d(DEBUG_TAG, "loadBugsfromFile: File read correctly"); // FIXME if no bbox exists from data, ty to use one from bugs if (postLoad != null) { postLoad.onSuccess(); } if (result.intValue() == READ_BACKUP) { Snack.barError(activity, R.string.toast_used_bug_backup); } } else { Log.d(DEBUG_TAG, "loadBugsfromFile: File read failed"); if (postLoad != null) { postLoad.onError(); } } } }; loader.execute(); } /** * Loads data from a file * */ public void syncLoadFromFile(@NonNull FragmentActivity activity) { final int READ_FAILED = 0; final int READ_OK = 1; final int READ_BACKUP = 2; int result = READ_FAILED; Map map = activity instanceof Main ? ((Main)activity).getMap() : null; Progress.showDialog(activity, Progress.PROGRESS_LOADING); if (getDelegator().readFromFile(activity)) { viewBox.setBorders(map, getDelegator().getLastBox()); result = READ_OK; } Progress.dismissDialog(activity, Progress.PROGRESS_LOADING); if (result != READ_FAILED) { Log.d(DEBUG_TAG, "syncLoadfromFile: File read correctly"); if (map != null) { try { viewBox.setRatio(map, (float)map.getWidth() / (float)map.getHeight()); } catch (Exception e) { // invalid dimensions of similar error try { viewBox.setBorders(map, new BoundingBox(-180.0,-GeoMath.MAX_LAT,180.0,GeoMath.MAX_LAT)); } catch (OsmException e1) { // Can't happen? e1.printStackTrace(); } } DataStyle.updateStrokes(STROKE_FACTOR / viewBox.getWidth()); loadEditingState((Main) activity, true); } if (map != null) { invalidateMap(); } activity.supportInvalidateOptionsMenu(); if (result == READ_BACKUP) { Snack.barError(activity, R.string.toast_used_backup); } } else { Log.d(DEBUG_TAG, "syncLoadfromFile: File read failed"); Snack.barError(activity, R.string.toast_state_file_failed); } } /** * Uploads to the server in the background. * * @param activity Activity this is called from * @param comment Changeset comment. * @param source The changeset source tag to add. * @param closeChangeset Whether to close the changeset after upload or not. */ public void upload(@NonNull final FragmentActivity activity, final String comment, final String source, final boolean closeChangeset) { final String PROGRESS_TAG = "data"; final Server server = prefs.getServer(); new AsyncTask<Void, Void, UploadResult>() { @Override protected void onPreExecute() { Progress.showDialog(activity, Progress.PROGRESS_UPLOADING, PROGRESS_TAG); lastComments.push(comment); lastSources.push(source); } @Override protected UploadResult doInBackground(Void... params) { UploadResult result = new UploadResult(); try { server.getCapabilities(); // update status if (!(server.apiAvailable() && server.writableDB())) { result.error = ErrorCodes.API_OFFLINE; return result; } getDelegator().uploadToServer(server, comment, source, closeChangeset); } catch (final MalformedURLException e) { Log.e(DEBUG_TAG, "", e); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(e); } catch (final ProtocolException e) { Log.e(DEBUG_TAG, "", e); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(e); } catch (final OsmServerException e) { result.httpError = e.getErrorCode(); result.message = e.getMessageWithDescription(); switch (e.getErrorCode()) { case HttpURLConnection.HTTP_FORBIDDEN: result.error = ErrorCodes.FORBIDDEN; break; case HttpURLConnection.HTTP_UNAUTHORIZED: result.error = ErrorCodes.INVALID_LOGIN; break; case HttpURLConnection.HTTP_GONE: case HttpURLConnection.HTTP_CONFLICT: case HttpURLConnection.HTTP_PRECON_FAILED: result.error = ErrorCodes.UPLOAD_CONFLICT; result.elementType = e.getElementType(); result.osmId = e.getElementId(); break; case HttpURLConnection.HTTP_BAD_REQUEST: result.error = ErrorCodes.BAD_REQUEST; result.message = e.getMessage(); break; case HttpURLConnection.HTTP_NOT_FOUND: case HttpURLConnection.HTTP_INTERNAL_ERROR: case HttpURLConnection.HTTP_BAD_GATEWAY: case HttpURLConnection.HTTP_UNAVAILABLE: result.error = ErrorCodes.UPLOAD_PROBLEM; break; //TODO: implement other state handling default: Log.e(DEBUG_TAG, "", e); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(e); break; } } catch (final IOException e) { result.error = ErrorCodes.NO_CONNECTION; Log.e(DEBUG_TAG, "", e); } catch (final NullPointerException e) { Log.e(DEBUG_TAG, "", e); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(e); } return result; } @Override protected void onPostExecute(UploadResult result) { Progress.dismissDialog(activity, Progress.PROGRESS_UPLOADING, PROGRESS_TAG); if (result.error == 0) { save(activity); // save now to avoid problems if it doesn't succeed later on, FIXME async or sync Snack.barInfo(activity, R.string.toast_upload_success); getDelegator().clearUndo(); // only clear on successful upload activity.supportInvalidateOptionsMenu(); } activity.getCurrentFocus().invalidate(); if (!activity.isFinishing()) { if (result.error == ErrorCodes.UPLOAD_CONFLICT) { if (result.osmId > 0) { UploadConflict.showDialog(activity, result); } else { Log.e(DEBUG_TAG, "No OSM element found for conflict"); ErrorAlert.showDialog(activity,ErrorCodes.UPLOAD_PROBLEM); } } else if (result.error == ErrorCodes.INVALID_LOGIN) { InvalidLogin.showDialog(activity); } else if (result.error == ErrorCodes.FORBIDDEN) { ForbiddenLogin.showDialog(activity,result.message); } else if (result.error == ErrorCodes.BAD_REQUEST) { ErrorAlert.showDialog(activity,result.error,result.message); } else if (result.error != 0) { ErrorAlert.showDialog(activity,result.error); } } } }.execute(); } /** * Uploads a GPS track to the server. * * @param track the track to upload * @param description a description of the track sent to the server * @param tags the tags to apply to the GPS track (comma delimeted) * @param visibility the track visibility, one of the following: private, public, trackable, identifiable */ public void uploadTrack(@NonNull final FragmentActivity activity, final Track track, final String description, final String tags, final Visibility visibility) { final Server server = prefs.getServer(); new AsyncTask<Void, Void, Integer>() { @Override protected void onPreExecute() { Progress.showDialog(activity, Progress.PROGRESS_UPLOADING); } @Override protected Integer doInBackground(Void... params) { int result = 0; try { server.uploadTrack(track, description, tags, visibility); } catch (final MalformedURLException e) { Log.e(DEBUG_TAG, "", e); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(e); } catch (final ProtocolException e) { Log.e(DEBUG_TAG, "", e); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(e); } catch (final OsmServerException e) { switch (e.getErrorCode()) { //FIXME use the same mechanics as for data uoload case HttpURLConnection.HTTP_FORBIDDEN: case HttpURLConnection.HTTP_UNAUTHORIZED: result = ErrorCodes.INVALID_LOGIN; break; case HttpURLConnection.HTTP_BAD_REQUEST: case HttpURLConnection.HTTP_PRECON_FAILED: case HttpURLConnection.HTTP_CONFLICT: result = ErrorCodes.UPLOAD_PROBLEM; break; case HttpURLConnection.HTTP_NOT_FOUND: case HttpURLConnection.HTTP_GONE: case HttpURLConnection.HTTP_INTERNAL_ERROR: case HttpURLConnection.HTTP_BAD_GATEWAY: case HttpURLConnection.HTTP_UNAVAILABLE: result = ErrorCodes.UPLOAD_PROBLEM; break; //TODO: implement other state handling default: Log.e(DEBUG_TAG, "", e); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(e); break; } } catch (final IOException e) { result = ErrorCodes.NO_CONNECTION; Log.e(DEBUG_TAG, "", e); } catch (final NullPointerException e) { Log.e(DEBUG_TAG, "", e); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(e); } catch (IllegalArgumentException e) { result = ErrorCodes.UPLOAD_PROBLEM; } catch (IllegalStateException e) { result = ErrorCodes.UPLOAD_PROBLEM; } catch (XmlPullParserException e) { result = ErrorCodes.UPLOAD_PROBLEM; } return result; } @Override protected void onPostExecute(Integer result) { Progress.dismissDialog(activity, Progress.PROGRESS_UPLOADING); if (result == 0) { Snack.barInfo(activity, R.string.toast_upload_success); } activity.getCurrentFocus().invalidate(); if (result != 0) { if (!activity.isFinishing()) { if (result == ErrorCodes.INVALID_LOGIN) { InvalidLogin.showDialog(activity); } else { ErrorAlert.showDialog(activity,result); } } } } }.execute(); } /** * Show a toast indicating how many unread mails are on the server * * @param activity activity calling this method */ public void checkForMail(final FragmentActivity activity) { final Server server = prefs.getServer(); new AsyncTask<Void, Void, Integer>() { @Override protected void onPreExecute() { } @Override protected Integer doInBackground(Void... params) { int result = 0; UserDetails userDetails = server.getUserDetails(); if (userDetails != null) { result = userDetails.unread; } return result; } @Override protected void onPostExecute(Integer result) { if (result > 0) { try { if (activity != null) { Snack.barInfo(activity, result == 1 ? activity.getResources().getString(R.string.toast_one_unread_mail) : activity.getResources().getString(R.string.toast_unread_mail, result), R.string.read, new View.OnClickListener() { @Override public void onClick(View v) { try { activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Urls.OSM_LOGIN))); } catch (Exception ex) { //never crash Log.e(DEBUG_TAG,"Linking to the OSM login page failed " + ex.getMessage()); } } }); } } catch (java.util.IllegalFormatFlagsException iffex) { // do nothing ... this is stop bugs in the Android format parsing crashing the upload, happens at least with the PL string } } } }.execute(); } /** * Make a new bug at the given screen X/Y coordinates. * * @param x The screen X-coordinate of the bug. * @param y The screen Y-coordinate of the bug. * @return The new bug, which must have a comment added before it can be submitted to OSB. */ public Note makeNewBug(final float x, final float y) { int lat = yToLatE7(y); int lon = xToLonE7(x); return new Note(lat, lon); } /** * Setter to a) set the internal value and b) push the value to {@link #map}. * @param selectedNode node to select */ public synchronized void setSelectedNode(final Node selectedNode) { if (selectedNode != null) { // always restart selectedNodes = new LinkedList<Node>(); selectedNodes.add(selectedNode); } else { selectedNodes = null; } map.setSelectedNodes(selectedNodes); resetFilterCache(); } /** * Add nodes to the internal list * @param selectedNode node to add to selection */ public synchronized void addSelectedNode(final Node selectedNode) { if (selectedNodes == null) { setSelectedNode(selectedNode); } else { if (!selectedNodes.contains(selectedNode)) { selectedNodes.add(selectedNode); } } resetFilterCache(); } /** * De-select a node * @param node node to remove from selection */ public synchronized void removeSelectedNode(Node node) { if (selectedNodes != null) { selectedNodes.remove(node); if (selectedNodes.size() == 0) { selectedNodes = null; } resetFilterCache(); } } /** * Setter to a) set the internal value and b) push the value to {@link #map}. * @param selectedWay way to select */ public synchronized void setSelectedWay(final Way selectedWay) { if (selectedWay != null) { // always restart selectedWays = new LinkedList<Way>(); selectedWays.add(selectedWay); } else { selectedWays = null; } map.setSelectedWays(selectedWays); resetFilterCache(); } /** * Adds the given way to the list of currently selected ways. * @param selectedWay way to add to selection */ public synchronized void addSelectedWay(final Way selectedWay) { if (selectedWays == null) { setSelectedWay(selectedWay); } else { if (!selectedWays.contains(selectedWay)) { selectedWays.add(selectedWay); } } resetFilterCache(); } /** * Removes the given way from the list of currently selected ways. * @param way way to de-select */ public synchronized void removeSelectedWay(Way way) { if (selectedWays != null) { selectedWays.remove(way); if (selectedWays.size() == 0) { selectedWays = null; } resetFilterCache(); } } /** * Setter to a) set the internal value and b) push the value to {@link #map}. * @param selectedRelation relation to select */ public synchronized void setSelectedRelation(final Relation selectedRelation) { if (selectedRelation != null) { // always restart selectedRelations = new LinkedList<Relation>(); selectedRelations.add(selectedRelation); } else { selectedRelations = null; } if (selectedRelation != null) { selectRelation(selectedRelation); } resetFilterCache(); } /** * De-select the relation * @param relation relation to remove from selection */ public synchronized void removeSelectedRelation(Relation relation) { if (selectedRelations != null) { selectedRelations.remove(relation); if (selectedRelations.size() == 0) { selectedRelations = null; } resetFilterCache(); } } /** * Adds the given relation to the list of currently selected relations. * @param selectedRelation relation to add to selection */ public synchronized void addSelectedRelation(final Relation selectedRelation) { if (selectedRelations == null) { setSelectedRelation(selectedRelation); } else { if (!selectedRelations.contains(selectedRelation)) { selectedRelations.add(selectedRelation); } } resetFilterCache(); } /** * Helper to clear the current, if any, filter cache */ private void resetFilterCache() { if (filter != null) { filter.clear(); } } /** * Set the currently selected bug. * * @param bug The selected bug. */ public synchronized void setSelectedBug(final Task bug) { this.selectedBug = bug; } /** * @return the selectedNode (currently simply the first in the list) */ public synchronized final Node getSelectedNode() { if (selectedNodes != null && selectedNodes.size() > 0) { if (!exists(selectedNodes.get(0))) { selectedNodes = null; // clear selection if node was deleted return null; } else { return selectedNodes.get(0); } } return null; } /** * Get list of selected nodes */ public List<Node> getSelectedNodes() { return selectedNodes; } /** * Return how many nodes are selected */ public int selectedNodesCount() { return selectedNodes == null ? 0 : selectedNodes.size(); } /** * @return the selectedWay (currently simply the first in the list) */ public synchronized final Way getSelectedWay() { if (selectedWays != null && selectedWays.size() > 0) { if (!exists(selectedWays.get(0))) { selectedWays = null; // clear selection if node was deleted return null; } else { return selectedWays.get(0); } } return null; } /** * Get list of selected ways */ public List<Way> getSelectedWays() { return selectedWays; } /** * Return how many ways are selected */ public int selectedWaysCount() { return selectedWays == null ? 0 : selectedWays.size(); } /** * Get list of selected ways */ public List<Relation> getSelectedRelations() { return selectedRelations; } /** * Return how many ways are selected */ public int selectedRelationsCount() { return selectedRelations == null ? 0 : selectedRelations.size(); } /** * Check is all selected elements exist, return true if we actually had to remove something */ boolean resyncSelected() { boolean result = false; if (selectedNodesCount() > 0) { for (Node n:new ArrayList<Node>(selectedNodes)) { if (!getDelegator().getCurrentStorage().contains(n)) { selectedNodes.remove(n); result = true; } } } if (selectedWaysCount() > 0) { for (Way w:new ArrayList<Way>(selectedWays)) { if (!getDelegator().getCurrentStorage().contains(w)) { selectedWays.remove(w); result = true; } } } if (selectedRelationsCount() > 0) { for (Relation r:new ArrayList<Relation>(selectedRelations)) { if (!getDelegator().getCurrentStorage().contains(r)) { selectedRelations.remove(r); result = true; } } } return result; } /** * @param e * @return true is e is selected */ public synchronized boolean isSelected(OsmElement e) { if (e instanceof Node) { return selectedNodes != null && selectedNodes.contains((Node) e); } else if (e instanceof Way) { return selectedWays != null && selectedWays.contains((Way) e); } else if (e instanceof Relation) { return selectedRelations != null && selectedRelations.contains((Relation) e); } return false; } /** * Get the selected bug. * * @return The selected bug. */ public synchronized final Task getSelectedBug() { return selectedBug; } /** * Get a list of all nodes currently in storage * @return unmodifiable list of all nodes currently loaded */ public List<Node> getNodes() { return getDelegator().getCurrentStorage().getNodes(); } /** * Get a list of all nodes contained in bounding box box currently in storage * @param box the bounding box * @return unmodifiable list of all nodes currently loaded contained in box */ public List<Node> getNodes(BoundingBox box) { return getDelegator().getCurrentStorage().getNodes(box); } /** * Get a list of all modified (created, modified, deleted) nodes currently in storage * @return all modified nodes currently loaded */ public List<Node> getModifiedNodes() { return getDelegator().getApiStorage().getNodes(); } /** * Get a list of all nodes contained in bounding box box currently in storage * @param box the bounding box * @return all modified nodes currently loaded contained in box */ public List<Node> getModifiedNodes(BoundingBox box) { return getDelegator().getApiStorage().getNodes(box); } /** * Get a list of all ways currently in storage * @return unmodifiable list of all ways currently loaded */ public List<Way> getWays() { return getDelegator().getCurrentStorage().getWays(); } /** * Get a list of all ways contained in or possibly intersecting the bounding box box currently in storage * @param box the bounding box * @return unmodifiable list of all nodes currently loaded contained in box */ public List<Way> getWays(BoundingBox box) { return getDelegator().getCurrentStorage().getWays(box); } /** * Get a list of all modified (created, modified, deleted) ways currently in storage * @return unmodifiable list of all modified ways currently loaded */ public List<Way> getModifiedWays() { return getDelegator().getApiStorage().getWays(); } /** * Get a list of all relations currently in storage * @return unmodifiable list of all relations currently loaded */ public List<Relation> getRelations() { return getDelegator().getCurrentStorage().getRelations(); } /** * Get a list of all modified (created, modified, deleted) relations currently in storage * @return unmodifiable list of all modified relations currently loaded */ public List<Relation> getModifiedRelations() { return getDelegator().getApiStorage().getRelations(); } /** * Will be called when the screen orientation was changed. * * @param map the new Map-Instance. Be aware: The View-dimensions are not yet set... */ public void setMap(Map map) { this.map = map; map.setDelegator(getDelegator()); map.setViewBox(viewBox); setSelectedBug(null); setSelectedNode(null); setSelectedWay(null); setSelectedRelation(null); invalidateMap(); } /** * Getter for testing * @return map object */ public Map getMap() { return map; } /** * Convenience method to invalidate the map */ void invalidateMap() { if (map != null) { map.invalidate(); } } /** * @return a list of all pending changes to upload */ public List<String> getPendingChanges(final Context aCaller) { return getDelegator().listChanges(aCaller.getResources()); } /** * Sets the set of elements that can currently be clicked. * <ul> * <li>If set to null, the map will use default behaviour.</li> * <li>If set to a non-null value, the map will highlight only elements in the list.</li> * </ul> * @param clickable a set of elements to which highlighting should be limited, or null to remove the limitation */ public synchronized void setClickableElements(Set<OsmElement> clickable) { clickableElements = clickable; } /** * Get elements that can currently be clicked * * @return the list of clickable elements. May be null, meaning no restrictions on clickable elements */ @Nullable public synchronized Set<OsmElement> getClickableElements() { return clickableElements; } /** * Sets if we return relations when touching/clicking. * * @param on true if we should return relations */ public void setReturnRelations(boolean on) { returnRelations = on; } /** * Checks if an element exists, i.e. is in currentStorage * * @param element the element that is to be checked * @return true if the element exists, false otherwise */ public boolean exists(OsmElement element) { return getDelegator().getCurrentStorage().contains(element); } /** * @return the X coordinate (in pixels) of the given node's position on the * screen (note that the returned position may be outside of the * screens bounds). */ public float getNodeScreenX(Node node) { return lonE7ToX(node.getLon()); } /** * @return the Y coordinate (in pixels) of the given node's position on the * screen (note that the returned position may be outside of the * screens bounds). */ public float getNodeScreenY(Node node) { return latE7ToY(node.getLat()); } /** Helper class for ordering nodes/ways by distance from a click */ private static class DistanceSorter<OUTTYPE extends OsmElement, T extends OUTTYPE> { private Comparator<Entry<T, Double>> comparator = new Comparator<Entry<T, Double>>() { @Override public int compare(Entry<T,Double> lhs, Entry<T,Double> rhs) { if (lhs == rhs) return 0; if (lhs.getValue() > rhs.getValue()) return 1; if (lhs.getValue() < rhs.getValue()) return -1; return 0; } }; /** Takes an element-distance map and returns the elements ordered by distance */ public ArrayList<OUTTYPE> sort(HashMap<T, Double> input) { ArrayList<Entry<T, Double>> entries = new ArrayList<Entry<T,Double>>(input.entrySet()); Collections.sort(entries, comparator); ArrayList<OUTTYPE> result = new ArrayList<OUTTYPE>(entries.size()); for (Entry<T, Double> entry : entries) result.add(entry.getKey()); return result; } } /** * Creates a turn restriction relation using the given objects as the members in the relation. * * @param activity activity we were called from * @param fromWay the way on which turning off of is restricted in some fashion * @param viaElement the "intersection node" at which the turn is restricted * @param toWay the way that the turn restriction prevents turning onto * @param restriction_type the kind of turn which is restricted * @return a relation element for the turn restriction */ public Relation createRestriction(@Nullable Activity activity, Way fromWay, OsmElement viaElement, Way toWay, String restriction_type) { createCheckpoint(activity, R.string.undo_action_create_relation); Relation restriction = getDelegator().createAndInsertRelation(null); SortedMap<String,String> tags = new TreeMap<String,String>(); tags.put("restriction", restriction_type == null ? "" : restriction_type); tags.put("type", "restriction"); getDelegator().setTags(restriction, tags); RelationMember from = new RelationMember("from", fromWay); getDelegator().addElementToRelation(from, restriction); RelationMember via = new RelationMember("via", viaElement); getDelegator().addElementToRelation(via, restriction); RelationMember to = new RelationMember("to", toWay); getDelegator().addElementToRelation(to, restriction); return restriction; } /** * Creates a new relation containing the given members. * * @param activity activity we were called from * @param type the 'type=*' tag to set on the relation itself * @param members the osm elements to include in the relation * @return the new relation */ public Relation createRelation(@Nullable Activity activity, String type, List<OsmElement> members ) { createCheckpoint(activity, R.string.undo_action_create_relation); Relation relation = getDelegator().createAndInsertRelation(members); SortedMap<String,String> tags = new TreeMap<String,String>(); if (type != null) tags.put("type", type); else tags.put("type", ""); getDelegator().setTags(relation, tags); return relation; } /** * Adds the list of elements to the given relation with an empty role set for each new member. * * @param activity activity we were called from */ public void addMembers(@Nullable Activity activity, Relation relation, ArrayList<OsmElement> members) { createCheckpoint(activity, R.string.undo_action_update_relations); getDelegator().addMembersToRelation(relation, members); } /** * Sets the set of ways that belong to a relation and should be highlighted. * If set to null, the map will use default behaviour. * If set to a non-null value, the map will highlight only elements in the list. * @param set of elements to which highlighting should be limited, or null to remove the limitation */ public void setSelectedRelationWays(List<Way> ways) { selectedRelationWays = ways; } public void addSelectedRelationWay(Way way) { if (selectedRelationWays == null) { selectedRelationWays = new LinkedList<Way>(); } selectedRelationWays.add(way); } public void removeSelectedRelationWay(Way way) { if (selectedRelationWays != null) { selectedRelationWays.remove(way); } } public List<Way> getSelectedRelationWays() { return selectedRelationWays; } /** * Set relation members to be highlighted * @param r */ public void selectRelation(Relation r) { if (r!=null) { for (RelationMember rm : r.getMembers()) { OsmElement e = rm.getElement(); if (e != null) { if (e.getName().equals(Way.NAME)) { addSelectedRelationWay((Way) e); } else if (e.getName().equals(Node.NAME)) { addSelectedRelationNode((Node) e); } else if (e.getName().equals(Relation.NAME) && (selectedRelationRelations == null || !selectedRelationRelations.contains((Relation)e))) { // break recursion if already selected addSelectedRelationRelation((Relation) e); } } } } } /** * Sets the set of nodes that belong to a relation and should be highlighted. * If set to null, the map will use default behaviour. * If set to a non-null value, the map will highlight only elements in the list. * @param set of elements to which highlighting should be limited, or null to remove the limitation */ public void setSelectedRelationNodes(List<Node> nodes) { selectedRelationNodes = nodes; } public void addSelectedRelationNode(Node node) { if (selectedRelationNodes == null) { selectedRelationNodes = new LinkedList<Node>(); } selectedRelationNodes.add(node); } public void removeSelectedRelationNode(Node node) { if (selectedRelationNodes != null) { selectedRelationNodes.remove(node); } } public List<Node> getSelectedRelationNodes() { return selectedRelationNodes; } /** * Sets the set of relations that belong to a relation and should be highlighted. * If set to null, the map will use default behaviour. * If set to a non-null value, the map will highlight only elements in the list. * @param set of elements to which highlighting should be limited, or null to remove the limitation */ public void setSelectedRelationRelations(List<Relation> relations) { selectedRelationRelations = relations; if (selectedRelationRelations != null) { for (Relation r:selectedRelationRelations) { selectRelation(r); } } } public void addSelectedRelationRelation(Relation relation) { if (selectedRelationRelations == null) { selectedRelationRelations = new LinkedList<Relation>(); } selectedRelationRelations.add(relation); } public void removeSelectedRelationRelation(Relation relation) { if (selectedRelationRelations != null) { selectedRelationRelations.remove(relation); } } public List<Relation> getSelectedRelationRelations() { return selectedRelationRelations; } public void deselectAll() { setSelectedNode(null); setSelectedWay(null); setSelectedRelation(null); setSelectedRelationNodes(null); setSelectedRelationWays(null); } /** * Fixup an object with a version conflict * * Note: this could do with some more explaining * @param activity activity we were called from * @param newVersion new version to use * @param elementLocal the local instance of the element * @param elementOnServer the remote instance of the element */ public void fixElementWithConflict(@Nullable Activity activity, long newVersion, OsmElement elementLocal, OsmElement elementOnServer) { createCheckpoint(activity, R.string.undo_action_fix_conflict); if (elementOnServer == null) { // deleted on server if (elementLocal.getState() != OsmElement.STATE_DELETED) { // but not locally // given that the element is deleted on the server we likely need to add it back to ways and relations there too if (elementLocal.getName().equals(Node.NAME)) { for (Way w:getWaysForNode((Node)elementLocal)) { getDelegator().setOsmVersion(w,w.getOsmVersion()+1); } } if (elementLocal.hasParentRelations()) { for (Relation r:elementLocal.getParentRelations()) { getDelegator().setOsmVersion(r,r.getOsmVersion()+1); } } } else { // deleted locally too // note this sets the state to unchanged, but the element // isn't referenced anywhere anymore so that doesn't matter getDelegator().removeFromUpload(elementLocal); return; } } getDelegator().setOsmVersion(elementLocal,newVersion); } /** * Displays a crosshair marker on the screen at the coordinates given (in pixels). */ public void showCrosshairs(float x, float y) { map.showCrosshairs(x, y); invalidateMap(); } /** * Hide a display crosshairs */ public void hideCrosshairs() { map.hideCrosshairs(); } /** * Copy element to clipboard * @param element element to copy */ public void copyToClipboard(OsmElement element) { if (element instanceof Node) { getDelegator().copyToClipboard(element, ((Node)element).getLat(), ((Node)element).getLon()); } else if (element instanceof Way) { // use current centroid of way int result[] = Logic.centroid(map.getWidth(), map.getHeight(), viewBox,(Way)element); if (result != null) { getDelegator().copyToClipboard(element, result[0], result[1]); } else { Log.e(DEBUG_TAG,"centroid of way " + element.getDescription() + " is null"); } } } /** * Cut element to clipboard * @param activity the activity we were called from * @param element the element to cut */ public void cutToClipboard(@Nullable Activity activity, OsmElement element) { createCheckpoint(activity, R.string.undo_action_cut); if (element instanceof Node) { getDelegator().cutToClipboard(element, ((Node)element).getLat(), ((Node)element).getLon()); } else if (element instanceof Way) { int result[] = Logic.centroid(map.getWidth(), map.getHeight(), viewBox,(Way)element); if (result != null) { getDelegator().cutToClipboard(element, result[0], result[1]); } else { Log.e(DEBUG_TAG,"centroid of way " + element.getDescription() + " is null"); } } invalidateMap(); } /** * Paste current contents of the clipboard * @param activity the activity we were called from * @param x screen x to position the object at * @param y screen y to position the object at */ public void pasteFromClipboard(@Nullable Activity activity, float x, float y) { createCheckpoint(activity, R.string.undo_action_paste); int lat = yToLatE7(y); int lon = xToLonE7(x); getDelegator().pasteFromClipboard(lat, lon); } public boolean clipboardIsEmpty() { return getDelegator().clipboardIsEmpty(); } /** * calculate the centroid of a way * @param viewvBox * @param h * @param w * @param way * @return WS84 coordinates of centroid */ private static int[] centroid(int w, int h, BoundingBox v, final Way way) { float XY[] = centroidXY(w,h,v,way); if (XY == null) { return null; } int lat = GeoMath.yToLatE7(h, w, v, XY[1]); int lon = GeoMath.xToLonE7(w, v, XY[0]); return new int[]{lat,lon}; } /** * Calculate the centroid of a way * * @param viewvBox display bounding box * @param h viewbox height * @param w viewbox width * @param way way to caculate centroid of * @return screen coordinates of centroid, null if the way has problems and if the way has length or area zero return the coordinates of the first node */ @Nullable private static float[] centroidXY(int w, int h, @NonNull BoundingBox v, @NonNull final Way way) { if (way == null || way.nodeCount() == 0) { return null; } // List<Node> vertices = way.getNodes(); if (way.isClosed()) { // see http://paulbourke.net/geometry/polygonmesh/ double A = 0; double Y = 0; double X = 0; int vs = vertices.size(); for (int i = 0; i < vs ; i++ ) { double x1 = GeoMath.lonE7ToX(w, v, vertices.get(i).getLon()); double y1 = GeoMath.latE7ToY(h, w, v, vertices.get(i).getLat()); double x2 = GeoMath.lonE7ToX(w, v, vertices.get((i+1) % vs).getLon()); double y2 = GeoMath.latE7ToY(h, w, v, vertices.get((i+1) % vs).getLat()); double d = x1*y2 - x2*y1; A = A + d; X = X + (x1+x2)*d; Y = Y + (y1+y2)*d; } if (Util.notZero(A)) { Y = Y/(3*A); X = X/(3*A); return new float[]{(float)X, (float)Y}; } else { // area zero -> we can choose any node Node n0 = vertices.get(0); return new float[]{(float)GeoMath.lonE7ToX(w, v, n0.getLon()),(float)GeoMath.latE7ToY(h, w, v, n0.getLat())}; } } else { // double L = 0; double Y = 0; double X = 0; int vs = vertices.size(); for (int i = 0; i < (vs-1) ; i++ ) { double x1 = GeoMath.lonE7ToX(w, v, vertices.get(i).getLon()); double y1 = GeoMath.latE7ToY(h, w, v, vertices.get(i).getLat()); double x2 = GeoMath.lonE7ToX(w, v, vertices.get(i+1).getLon()); double y2 = GeoMath.latE7ToY(h, w, v, vertices.get(i+1).getLat()); double len = Math.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)); L = L + len; X = X + len * (x1+x2)/2; Y = Y + len * (y1+y2)/2; } if (Util.notZero(L)) { Y = Y/L; X = X/L; return new float[]{(float)X, (float)Y}; } else { // length zero -> we can choose any node Node n0 = vertices.get(0); return new float[]{(float)GeoMath.lonE7ToX(w, v, n0.getLon()),(float)GeoMath.latE7ToY(h, w, v, n0.getLat())}; } } } /** * Calculate the centroid of a way * * @param way way to calculate the centroid of * @return WGS84 coordinates of centroid, null if the way has problems and if the way has length or area zero return the coordinates of the first node */ @Nullable public static double[] centroidLonLat(@NonNull final Way way) { if (way == null) { return null; } // List<Node> vertices = way.getNodes(); if (way.isClosed()) { // see http://paulbourke.net/geometry/polygonmesh/ double A = 0; double Y = 0; double X = 0; int vs = vertices.size(); for (int i = 0; i < vs ; i++ ) { double x1 = vertices.get(i).getLon() / 1E7D; double y1 = GeoMath.latE7ToMercator(vertices.get(i).getLat()); double x2 = vertices.get((i+1) % vs).getLon() / 1E7D; double y2 = GeoMath.latE7ToMercator(vertices.get((i+1) % vs).getLat()); A = A + (x1*y2 - x2*y1); X = X + (x1+x2)*(x1*y2-x2*y1); Y = Y + (y1+y2)*(x1*y2-x2*y1); } if (Util.notZero(A)) { Y = GeoMath.mercatorToLat(Y/(3*A)); X = X/(3*A); return new double[]{X, Y}; } else { // area zero -> we can choose any node Node n0 = vertices.get(0); return new double[]{n0.getLon()/1E7D,n0.getLat()/1E7D}; } } else { // double L = 0; double Y = 0; double X = 0; int vs = vertices.size(); for (int i = 0; i < (vs-1) ; i++ ) { double x1 = vertices.get(i).getLon() / 1E7D; double y1 = GeoMath.latE7ToMercator(vertices.get(i).getLat()); double x2 = vertices.get(i+1).getLon() / 1E7D; double y2 = GeoMath.latE7ToMercator(vertices.get((i+1)).getLat()); double len = Math.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)); L = L + len; X = X + len * (x1+x2)/2; Y = Y + len * (y1+y2)/2; } if (Util.notZero(L)) { Y = GeoMath.mercatorToLat(Y/L); X = X/L; return new double[]{X, Y}; } else { // length zero -> we can choose any node Node n0 = vertices.get(0); return new double[]{n0.getLon()/1E7D,n0.getLat()/1E7D}; } } } /** * Arrange way points in a circle * * Note: currently only works if map is present * @param activity this method was called from, if null no warnings will be displayed * @param way way to circulize */ public void performCirculize(@Nullable FragmentActivity activity, Way way) { if (way.getNodes().size() < 3) return; createCheckpoint(activity, R.string.undo_action_circulize); int[] center = centroid(map.getWidth(), map.getHeight(), viewBox, way); if (center != null) { getDelegator().circulizeWay(map, center, way); invalidateMap(); displayAttachedObjectWarning(activity, way); } else { Log.e(DEBUG_TAG,"performCirculize unable to determin centroid for way " + way.getDescription()); } } /** * convenience function * @param x * @return */ public int xToLonE7(float x) { return GeoMath.xToLonE7(map.getWidth(), viewBox, x); } /** * convenience function * @param y * @return */ public int yToLatE7(float y) { return GeoMath.yToLatE7(map.getHeight(), map.getWidth(), viewBox, y); } /** * convenience function * @param lon * @return */ public float lonE7ToX(int lon) { return GeoMath.lonE7ToX(map.getWidth(), viewBox, lon); } /** * convenience function * @param lat * @return */ public float latE7ToY(int lat) { return GeoMath.latE7ToY(map.getHeight(),map.getWidth(), viewBox, lat); } /** * @return the delegator */ private static StorageDelegator getDelegator() { return App.getDelegator(); } public void getDataLock() { getDelegator().lock(); } public void dataUnlock() { getDelegator().unlock(); } /** * @return the viewBox */ public BoundingBox getViewBox() { return viewBox; } /** * Return the last used comment * @return comment */ public String getLastComment() { return lastComments.last(); } /** * Return the last used comments index 0 is the most recent one * @return ArrayList of the comments */ public ArrayList<String> getLastComments() { return lastComments; } /** * Set the list of last comments * @param comments */ public void setLastComments(ArrayList<String> comments) { lastComments = new MRUList<String>(comments); lastComments.ensureCapacity(MRULIST_SIZE); } /** * Return the last used source string * @return source */ public String getLastSource() { return lastSources.last(); } /** * Return the last used source strings index 0 is the most recent one * @return ArrayList of the source strings */ public ArrayList<String> getLastSources() { return lastSources; } /** * Set the list of last used source strings * @param sources */ public void setLastSources(ArrayList<String> sources) { lastSources = new MRUList<String>(sources); lastSources.ensureCapacity(MRULIST_SIZE); } /** * @return the current object filter */ public Filter getFilter() { return filter; } /** * Set the object filter * @param filter */ public void setFilter(Filter filter) { this.filter = filter; } /** * Display a warning if an operation on the element e would effect a filtered/hidden object * @param activity activity this method was called from, if null no warnings will be displayed * @param e */ private <T extends OsmElement> void displayAttachedObjectWarning(@Nullable FragmentActivity activity, T e) { ArrayList<T> a = new ArrayList<T>(); a.add(e); displayAttachedObjectWarning(activity, a); } /** * Display a warning if an operation on the element e1 or e2 would effect a filtered/hidden object * @param activity activity this method was called from, if null no warnings will be displayed * @param e1 * @param e2 */ private <T extends OsmElement> void displayAttachedObjectWarning(@Nullable FragmentActivity activity, T e1, T e2) { ArrayList<T> a = new ArrayList<T>(); a.add(e1); a.add(e2); displayAttachedObjectWarning(activity, a); } /** * Display a warning if an operation on the element e1 or e2 would effect a filtered/hidden object * @param activity activity this method was called from, if null no warnings will be displayed * @param e1 * @param e2 * @param checkRelationsOnly */ private <T extends OsmElement> void displayAttachedObjectWarning(@Nullable FragmentActivity activity, T e1, T e2, boolean checkRelationsOnly) { ArrayList<T> a = new ArrayList<T>(); a.add(e1); a.add(e2); displayAttachedObjectWarning(activity,a, checkRelationsOnly); } /** * Display a warning if an operation on the elements included in list would effect a filtered/hidden object * @param activity activity this method was called from, if null no warnings will be displayed * @param list */ private <T extends OsmElement> void displayAttachedObjectWarning(@Nullable FragmentActivity activity, Collection<T> list) { displayAttachedObjectWarning(activity,list, false); } /** * Display a warning if an operation on the elements included in list would effect a filtered/hidden object * @param activity activity this method was called from, if null no warnings will be displayed * @param list * @param checkRelationsOnly */ private <T extends OsmElement> void displayAttachedObjectWarning(@Nullable FragmentActivity activity, Collection<T> list, boolean checkRelationsOnly) { if (getFilter() != null && showAttachedObjectWarning()) { elementLoop: for (OsmElement e:list) { if (!checkRelationsOnly) { if (e instanceof Node) { List<Way> ways = getWaysForNode((Node)e); if (ways.size() > 0) { for (Way w:ways) { if (!getFilter().include(w, false)) { AttachedObjectWarning.showDialog(activity); break elementLoop; } } } } else if (e instanceof Way) { for (Node n:((Way)e).getNodes()) { List<Way> ways = getWaysForNode(n); if (ways.size() > 0) { for (Way w:ways) { if (!getFilter().include(w, false)) { AttachedObjectWarning.showDialog(activity); break elementLoop; } } } } } } if (e.hasParentRelations()) { for (Relation r:e.getParentRelations()) { if (!getFilter().include(r, false)) { AttachedObjectWarning.showDialog(activity); break elementLoop; } } } } } } }