package de.blau.android; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.WeakHashMap; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.location.Location; import android.os.Build; import android.support.v4.content.ContextCompat; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import de.blau.android.exception.OsmException; import de.blau.android.filter.Filter; import de.blau.android.osm.BoundingBox; import de.blau.android.osm.GeoPoint; import de.blau.android.osm.GeoPoint.InterruptibleGeoPoint; import de.blau.android.osm.Node; import de.blau.android.osm.OsmElement; import de.blau.android.osm.Relation; import de.blau.android.osm.StorageDelegator; import de.blau.android.osm.Tags; import de.blau.android.osm.Way; import de.blau.android.prefs.Preferences; import de.blau.android.presets.Preset; import de.blau.android.presets.Preset.PresetItem; import de.blau.android.resources.DataStyle; import de.blau.android.resources.DataStyle.FeatureStyle; import de.blau.android.resources.TileLayerServer; import de.blau.android.services.TrackerService; import de.blau.android.util.Density; import de.blau.android.util.GeoMath; import de.blau.android.util.Offset; import de.blau.android.util.ThemeUtils; import de.blau.android.util.Util; import de.blau.android.util.collections.LongHashSet; import de.blau.android.views.IMapView; import de.blau.android.views.overlay.MapOverlayTilesOverlay; import de.blau.android.views.overlay.MapTilesOverlay; import de.blau.android.views.overlay.MapViewOverlay; /** * Paints all data provided previously by {@link Logic}.<br/> * As well as a number of overlays. * There is a default overlay that fetches rendered tiles * from an OpenStreetMap-server. * * @author mb * @author Marcus Wolschon <Marcus@Wolschon.biz> */ public class Map extends View implements IMapView { @SuppressWarnings("unused") private static final String DEBUG_TAG = Map.class.getSimpleName(); public static final int ICON_SIZE_DP = 20; /** Use reflection to access Canvas method only available in API11. */ private static final Method mIsHardwareAccelerated; private static final int HOUSE_NUMBER_RADIUS = 10; /** * zoom level from which on we display data */ private static final int SHOW_DATA_LIMIT = 12; /** * zoom level from which on we display icons and house numbers */ private static final int SHOW_ICONS_LIMIT = 15; /** half the width/height of a node icon in px */ private final int iconRadius; private final int iconSelectedBorder; private final int houseNumberRadius; private final int verticalNumberOffset; private final ArrayList<BoundingBox> boundingBoxes = new ArrayList<BoundingBox>(); private Preferences prefs; /** Direction we're pointing. 0-359 is valid, anything else is invalid.*/ private float orientation = -1f; /** * List of Overlays we are showing.<br/> * This list is initialized to contain only one * {@link MapTilesOverlay} at construction-time but * can be changed to contain additional overlays later. * @see #getOverlays() */ final List<MapViewOverlay> mOverlays = Collections.synchronizedList(new ArrayList<MapViewOverlay>()); /** * The visible area in decimal-degree (WGS84) -space. */ private BoundingBox myViewBox; private StorageDelegator delegator; /** * show icons for POIs (in a wide sense of the word) */ private boolean showIcons = false; /** * show icons for POIs tagged on (closed) ways */ private boolean showWayIcons = false; /** * Always darken non-downloaded areas */ private boolean alwaysDrawBoundingBoxes = false; /** * Stores icons that apply to a certain "thing". This can be e.g. a node or a SortedMap of tags. */ private final WeakHashMap<Object, Bitmap> iconcache = new WeakHashMap<Object, Bitmap>(); /** * Stores strings that apply to a certain "thing". This can be e.g. a node or a SortedMap of tags. */ private final WeakHashMap<Object, String> labelcache = new WeakHashMap<Object, String>(); /** Caches if the map is zoomed into edit range during one onDraw pass */ private boolean tmpDrawingInEditRange; /** Caches the edit mode during one onDraw pass */ private Mode tmpDrawingEditMode; /** Caches the currently selected nodes during one onDraw pass */ private List<Node> tmpDrawingSelectedNodes; /** Caches the currently selected ways during one onDraw pass */ private List<Way> tmpDrawingSelectedWays; /** Caches the current "clickable elements" set during one onDraw pass */ private Set<OsmElement> tmpClickableElements; /** used for highlighting relation members */ private List<Way> tmpDrawingSelectedRelationWays; private List<Node> tmpDrawingSelectedRelationNodes; /** * Locked or not */ private boolean tmpLocked; /** * */ private ArrayList<Way> tmpStyledWays = new ArrayList<Way>(); private ArrayList<Way> tmpHiddenWays = new ArrayList<Way>(); /** Caches the preset during one onDraw pass */ private Preset[] tmpPresets; /** Caches the Paint used for node tolerance */ private Paint nodeTolerancePaint; private Paint nodeTolerancePaint2; /** Caches the Paint used for way tolerance */ private Paint wayTolerancePaint; private Paint wayTolerancePaint2; /** cached zoom level, calculated once per onDraw pass **/ private int zoomLevel = 0; /** Cache the current filter **/ private Filter tmpFilter = null; /** */ private boolean inNodeIconZoomRange = false; /** * We just need one path object */ private Path path = new Path(); private LongHashSet handles; private Location displayLocation = null; private boolean isFollowingGPS = false; private TrackerService tracker; private Paint textPaint = new Paint(); /** * support for display a crosshairs at a position */ private boolean showCrosshairs = false; private int crosshairsLat = 0; private int crosshairsLon = 0; static { Method m; try { m = Canvas.class.getMethod("isHardwareAccelerated", (Class[])null); } catch (NoSuchMethodException e) { m = null; } mIsHardwareAccelerated = m; } private Context context; private Rect canvasBounds; @SuppressLint("NewApi") public Map(final Context context) { super(context); this.context = context; canvasBounds = new Rect(); setFocusable(true); setFocusableInTouchMode(true); //Style me setBackgroundColor(ContextCompat.getColor(context,R.color.ccc_white)); setDrawingCacheEnabled(false); // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { setLayerType(View.LAYER_TYPE_SOFTWARE, null); } iconRadius = Density.dpToPx(ICON_SIZE_DP / 2); houseNumberRadius = Density.dpToPx(HOUSE_NUMBER_RADIUS); verticalNumberOffset = Density.dpToPx(3); iconSelectedBorder = Density.dpToPx(2); // TODO externalize textPaint.setColor(Color.WHITE); textPaint.setTypeface(Typeface.SANS_SERIF); textPaint.setTextSize(Density.dpToPx(12)); textPaint.setShadowLayer(1, 0, 0, Color.BLACK); } public void createOverlays(Context ctx) { // create an overlay that displays pre-rendered tiles from the internet. synchronized(mOverlays) { if (mOverlays.size() == 0) // only set once { if (prefs == null) // just to be safe mOverlays.add(new MapTilesOverlay(this, TileLayerServer.getDefault(ctx, true), null)); else { // mOverlays.add(new OpenStreetMapTilesOverlay(this, OpenStreetMapTileServer.get(getResources(), prefs.backgroundLayer(), true), null)); MapTilesOverlay osmto = new MapTilesOverlay(this, TileLayerServer.get(ctx, prefs.backgroundLayer(), true), null); // Log.d("Map","background tile renderer " + osmto.getRendererInfo().toString()); mOverlays.add(osmto); mOverlays.add(new MapOverlayTilesOverlay(this)); } mOverlays.add(new de.blau.android.tasks.MapOverlay(this, prefs.getServer())); mOverlays.add(new de.blau.android.photos.MapOverlay(this, prefs.getServer())); mOverlays.add(new de.blau.android.grid.MapOverlay(this, prefs.getServer())); } } } public MapTilesOverlay getOpenStreetMapTilesOverlay() { synchronized(mOverlays) { for (MapViewOverlay osmvo : mOverlays) { if ((osmvo instanceof MapTilesOverlay) && !(osmvo instanceof MapOverlayTilesOverlay)) { return (MapTilesOverlay)osmvo; } } } return null; } /** * The names of these clases are patently silly and should be refactored * @return */ public MapOverlayTilesOverlay getOpenStreetMapOverlayTilesOverlay() { synchronized(mOverlays) { for (MapViewOverlay osmvo : mOverlays) { if (osmvo instanceof MapOverlayTilesOverlay) { return (MapOverlayTilesOverlay)osmvo; } } } return null; } public de.blau.android.tasks.MapOverlay getOpenStreetBugsOverlay() { synchronized(mOverlays) { for (MapViewOverlay osmvo : mOverlays) { if (osmvo instanceof de.blau.android.tasks.MapOverlay) { return (de.blau.android.tasks.MapOverlay)osmvo; } } } return null; } public de.blau.android.photos.MapOverlay getPhotosOverlay() { synchronized(mOverlays) { for (MapViewOverlay osmvo : mOverlays) { if (osmvo instanceof de.blau.android.photos.MapOverlay) { return (de.blau.android.photos.MapOverlay)osmvo; } } } return null; } public void onDestroy() { synchronized(mOverlays) { for (MapViewOverlay osmvo : mOverlays) { osmvo.onDestroy(); } } tracker = null; iconcache.clear(); labelcache.clear(); tmpPresets = null; } public void onLowMemory() { synchronized(mOverlays) { for (MapViewOverlay osmvo : mOverlays) { osmvo.onLowMemory(); } } } /** * {@inheritDoc} */ @Override protected void onDraw(final Canvas canvas) { super.onDraw(canvas); long time = System.currentTimeMillis(); zoomLevel = calcZoomLevel(canvas); // set in paintOsmData now tmpDrawingInEditRange = Main.logic.isInEditZoomRange(); final Logic logic = App.getLogic(); tmpDrawingEditMode = logic.getMode(); tmpFilter = logic.getFilter(); tmpDrawingSelectedNodes = logic.getSelectedNodes(); tmpDrawingSelectedWays = logic.getSelectedWays(); tmpClickableElements = logic.getClickableElements(); tmpDrawingSelectedRelationWays = logic.getSelectedRelationWays(); tmpDrawingSelectedRelationNodes = logic.getSelectedRelationNodes(); tmpPresets = App.getCurrentPresets(context); tmpLocked = logic.isLocked(); inNodeIconZoomRange = zoomLevel > SHOW_ICONS_LIMIT; // handles = null; this forces creation of a new object, simply clear it in paintHandles after use // Draw our Overlays. canvas.getClipBounds(canvasBounds); MapTilesOverlay.resetAttributionArea(canvasBounds, 0); synchronized(mOverlays) { for (MapViewOverlay osmvo : mOverlays) { if (!(osmvo instanceof de.blau.android.tasks.MapOverlay)) { osmvo.onManagedDraw(canvas, this); } } } if (zoomLevel > SHOW_DATA_LIMIT) { paintOsmData(canvas); } getOpenStreetBugsOverlay().onManagedDraw(canvas, this); // draw bugs on top of data if (zoomLevel > 10) { if (tmpDrawingEditMode != Mode.MODE_ALIGN_BACKGROUND) { // shallow copy to avoid modification issues boundingBoxes.clear(); boundingBoxes.addAll(delegator.getBoundingBoxes()); paintStorageBox(canvas, boundingBoxes); } paintGpsTrack(canvas); } paintGpsPos(canvas); if (tmpDrawingInEditRange) paintCrosshairs(canvas); if (tmpDrawingEditMode == Mode.MODE_ALIGN_BACKGROUND) { paintZoomAndOffset(canvas); } time = System.currentTimeMillis() - time; if (prefs.isStatsVisible()) { paintStats(canvas, (int) (1 / (time / 1000f))); } } @Override protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { super.onSizeChanged(w, h, oldw, oldh); try { myViewBox.setRatio(this, (float) w / h, true); } catch (OsmException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /* Overlay Event Forwarders */ @Override public boolean onTouchEvent(MotionEvent event) { synchronized(mOverlays) { for (MapViewOverlay osmvo : mOverlays) { if (osmvo.onTouchEvent(event, this)) { return true; } } } return super.onTouchEvent(event); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { synchronized(mOverlays) { for (MapViewOverlay osmvo : mOverlays) { if (osmvo.onKeyDown(keyCode, event, this)) { return true; } } } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { synchronized(mOverlays) { for (MapViewOverlay osmvo : mOverlays) { if (osmvo.onKeyUp(keyCode, event, this)) { return true; } } } return super.onKeyUp(keyCode, event); } @Override public boolean onTrackballEvent(MotionEvent event) { synchronized(mOverlays) { for (MapViewOverlay osmvo : mOverlays) { if (osmvo.onTrackballEvent(event, this)) { return true; } } } return super.onTrackballEvent(event); } /** * As of Android 4.0.4, clipping with Op.DIFFERENCE is not supported if hardware acceleration is used. * (see http://android-developers.blogspot.de/2011/03/android-30-hardware-acceleration.html) * Op.DIFFERENCE and clipPath supported as of 18 * * !!! FIXME Disable using HW clipping completely for now, see bug https://github.com/MarcusWolschon/osmeditor4android/issues/307 * * @param c Canvas to check * @return true if the canvas supports proper clipping with Op.DIFFERENCE */ private boolean hasFullClippingSupport(Canvas c) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB /*&& Build.VERSION.SDK_INT < 18*/ && mIsHardwareAccelerated != null) { try { return !(Boolean)mIsHardwareAccelerated.invoke(c, (Object[])null); } catch (IllegalArgumentException e) { } catch (IllegalAccessException e) { } catch (InvocationTargetException e) { } } // Older versions do not use hardware acceleration return true; } private boolean myIsHardwareAccelerated(Canvas c) { if (mIsHardwareAccelerated != null) { try { return (Boolean)mIsHardwareAccelerated.invoke(c, (Object[])null); } catch (IllegalArgumentException e) { } catch (IllegalAccessException e) { } catch (InvocationTargetException e) { } } // Older versions do not use hardware acceleration return false; } private void paintCrosshairs(Canvas canvas) { // if (showCrosshairs) { float x = GeoMath.lonE7ToX(getWidth(), getViewBox(), crosshairsLon); float y = GeoMath.latE7ToY(getHeight(), getWidth(), getViewBox(),crosshairsLat); Paint paint = DataStyle.getCurrent(DataStyle.CROSSHAIRS_HALO).getPaint(); drawCrosshairs(canvas, x, y, paint); paint = DataStyle.getCurrent(DataStyle.CROSSHAIRS).getPaint(); drawCrosshairs(canvas, x, y, paint); } } private void drawCrosshairs(Canvas canvas, float x, float y, Paint paint) { canvas.save(); canvas.translate(x, y); canvas.drawPath(DataStyle.getCurrent().crosshairs_path, paint); canvas.restore(); } private void paintGpsTrack(Canvas canvas) { if (tracker == null) return; float[] linePoints = pointListToLinePointsArray(new ArrayList<Float>(), tracker.getTrackPoints()); canvas.drawLines(linePoints, DataStyle.getCurrent(DataStyle.GPS_TRACK).getPaint()); } /** * @param canvas */ private void paintGpsPos(final Canvas canvas) { if (displayLocation == null) return; BoundingBox viewBox = getViewBox(); float x = GeoMath.lonE7ToX(getWidth(), viewBox, (int)(displayLocation.getLongitude() * 1E7)); float y = GeoMath.latE7ToY(getHeight(), getWidth(), viewBox, (int)(displayLocation.getLatitude() * 1E7)); float o = -1f; if (displayLocation.hasBearing() && displayLocation.hasSpeed() && displayLocation.getSpeed() > 1.4f) { // 1.4m/s ~= 5km/h ~= walking pace // faster than walking pace - use the GPS bearing o = displayLocation.getBearing(); } else { // slower than walking pace - use the compass orientation (if available) if (orientation >= 0) { o = orientation; } } Paint paint = null; if (isFollowingGPS) { paint = DataStyle.getCurrent(DataStyle.GPS_POS_FOLLOW).getPaint(); } else { paint = DataStyle.getCurrent(DataStyle.GPS_POS).getPaint(); } if (o < 0) { // no orientation data available canvas.drawCircle(x, y, paint.getStrokeWidth(), paint); } else { // show the orientation using a pointy indicator canvas.save(); canvas.translate(x, y); canvas.rotate(o); canvas.drawPath(DataStyle.getCurrent().orientation_path, paint); canvas.restore(); } if (displayLocation.hasAccuracy()) { // FIXME this assumes square pixels float accuracyInPixels = (float) (GeoMath.convertMetersToGeoDistance(displayLocation.getAccuracy())*((double)getWidth()/(viewBox.getWidth()/1E7D))); RectF accuracyRect = new RectF(x-accuracyInPixels,y+accuracyInPixels,x+accuracyInPixels,y-accuracyInPixels); canvas.drawOval(accuracyRect, DataStyle.getCurrent(DataStyle.GPS_ACCURACY).getPaint()); } } private void paintStats(final Canvas canvas, final int fps) { int pos = 1; String text = ""; Paint infotextPaint = DataStyle.getCurrent(DataStyle.INFOTEXT).getPaint(); float textSize = infotextPaint.getTextSize(); BoundingBox viewBox = getViewBox(); text = "viewBox: " + viewBox.toString(); canvas.drawText(text, 5, getHeight() - textSize * pos++, infotextPaint); text = "Relations (current/API) :" + delegator.getCurrentStorage().getRelations().size() + "/" + delegator.getApiRelationCount(); canvas.drawText(text, 5, getHeight() - textSize * pos++, infotextPaint); text = "Ways (current/API) :" + delegator.getCurrentStorage().getWays().size() + "/" + delegator.getApiWayCount(); canvas.drawText(text, 5, getHeight() - textSize * pos++, infotextPaint); text = "Nodes (current/Waynodes/API) :" + delegator.getCurrentStorage().getNodes().size() + "/" + delegator.getCurrentStorage().getWaynodes().size() + "/" + delegator.getApiNodeCount(); canvas.drawText(text, 5, getHeight() - textSize * pos++, infotextPaint); text = "fps: " + fps; canvas.drawText(text, 5, getHeight() - textSize * pos++, infotextPaint); text = "hardware acceleration: " + (myIsHardwareAccelerated(canvas) ? "on" : "off"); canvas.drawText(text, 5, getHeight() - textSize * pos++, infotextPaint); } /** * Paint the current tile zoom level and offset ... very ugly * @param canvas */ private void paintZoomAndOffset(final Canvas canvas) { int pos = ThemeUtils.getActionBarHeight(context) + 5 + (int)de.blau.android.grid.MapOverlay.LONGTICKS*3; Offset o = getOpenStreetMapTilesOverlay().getRendererInfo().getOffset(zoomLevel); String text = "Z " + zoomLevel + " Offset " + (o != null ? String.format(Locale.US,"%.5f",o.lon) + "/" + String.format(Locale.US,"%.5f",o.lat) : "0.00000/0.00000"); float textSize = textPaint.getTextSize(); canvas.drawText(text, 5, pos + textSize, textPaint); } /** * Paints all OSM data on the given canvas. * * @param canvas Canvas, where the data shall be painted on. */ private void paintOsmData(final Canvas canvas) { ArrayList<Float> points = new ArrayList<Float>(); // allocate this just once // first find all nodes that we need to display (for density calculations) List<Node> paintNodes = delegator.getCurrentStorage().getNodes(getViewBox()); // the following should guarantee that if the selected node is off screen but the handle not, the handle gets drawn // note this isn't perfect because touch areas of other nodes just outside the screen still won't get drawn // TODO check if we can't avoid searching paintNodes multiple times if (tmpDrawingSelectedNodes != null) { for (Node n:tmpDrawingSelectedNodes) { if (!paintNodes.contains(n)) { paintNodes.add(n); } } } // tmpDrawingInEditRange = App.getLogic().isInEditZoomRange(); // do this after density calc boolean drawTolerance = tmpDrawingInEditRange // if we are not in editing range none of the further checks are necessary && !tmpLocked && tmpDrawingEditMode.elementsSelectable(); //Paint all ways List<Way> ways = delegator.getCurrentStorage().getWays(getViewBox()); boolean filterMode = tmpFilter != null; // we have an active filter /* Split the ways it to whose that we are going to show and those that we hide, rendering is far simpler for the later */ tmpHiddenWays.clear(); tmpStyledWays.clear(); for (Way w:ways) { if (filterMode) { if (tmpFilter.include(w, tmpDrawingInEditRange && tmpDrawingSelectedWays != null && tmpDrawingSelectedWays.contains(w))) { tmpStyledWays.add(w); } else { tmpHiddenWays.add(w); } } else { tmpStyledWays.add(w); } } // draw hidden ways first for (Way w:tmpHiddenWays) { paintHiddenWay(points, canvas,w); } boolean displayHandles = tmpDrawingSelectedNodes == null && tmpDrawingSelectedRelationWays == null && tmpDrawingSelectedRelationNodes == null && tmpDrawingEditMode.elementsGeomEditiable(); Collections.sort(tmpStyledWays,layerComparator); for (Way w:tmpStyledWays) { paintWay(points, canvas,w,displayHandles, drawTolerance); } //Paint nodes Boolean hwAccelarationWorkaround = myIsHardwareAccelerated(canvas) && Build.VERSION.SDK_INT < 19; for (Node n:paintNodes) { paintNode(canvas, n, hwAccelarationWorkaround, drawTolerance); } paintHandles(canvas); } /** * For ordering according to layer value and draw lines on top of areas in the same layer */ private Comparator<Way> layerComparator = new Comparator<Way>() { @Override public int compare(Way w1, Way w2) { int layer1 = 0; int layer2 = 0; String layer1Str = w1.getTagWithKey(Tags.KEY_LAYER); if (layer1Str!=null) { try { layer1 = Integer.parseInt(layer1Str); } catch (NumberFormatException e) { // FIXME should validate here } } String layer2Str = w2.getTagWithKey(Tags.KEY_LAYER); if (layer2Str!=null) { try { layer2 = Integer.parseInt(layer2Str); } catch (NumberFormatException e) { // FIXME should validate here } } int result = layer2 == layer1 ? 0 : layer2 > layer1 ? +1 : -1; if (result == 0) { FeatureStyle fs1 = getAndSetStyle(w1); Style style1 = fs1.getPaint().getStyle(); FeatureStyle fs2 = getAndSetStyle(w2); Style style2 = fs2.getPaint().getStyle(); result = style2 == style1 ? 0 : style2 == Style.STROKE ? -1 : +1; } return result; } }; /** * * @return true if too many nodes are on screen for editing */ public boolean tooManyNodes() { return false; // TODO code is currently disabled because it is not quite satisfactory // return nodesOnScreenCount>(maxOnScreenNodes-unusedNodeSpace); } private void paintStorageBox(final Canvas canvas, List<BoundingBox> list) { if (!tmpLocked || alwaysDrawBoundingBoxes) { Canvas c = canvas; Bitmap b = null; // Clipping with Op.DIFFERENCE is not supported when a device uses hardware acceleration // drawing to a bitmap however will currently not be accelerated if (!hasFullClippingSupport(canvas)) { b = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); c = new Canvas(b); } else { c.save(); } int screenWidth = getWidth(); int screenHeight = getHeight(); BoundingBox viewBox = getViewBox(); path.reset(); RectF screen = new RectF(0, 0, getWidth(), getHeight()); for (BoundingBox bb:list) { if (viewBox.intersects(bb)) { // only need to do this if we are on screen float left = GeoMath.lonE7ToX(screenWidth, viewBox, bb.getLeft()); float right = GeoMath.lonE7ToX(screenWidth, viewBox, bb.getRight()); float bottom = GeoMath.latE7ToY(screenHeight, screenWidth, viewBox , bb.getBottom()); float top = GeoMath.latE7ToY(screenHeight, screenWidth, viewBox, bb.getTop()); RectF rect = new RectF(left, top, right, bottom); rect.intersect(screen); path.addRect(rect, Path.Direction.CW); } } Paint boxpaint = DataStyle.getCurrent(DataStyle.VIEWBOX).getPaint(); c.clipPath(path, Region.Op.DIFFERENCE); c.drawRect(screen, boxpaint); if (!hasFullClippingSupport(canvas)) { canvas.drawBitmap(b, 0, 0, null); } else { c.restore(); } } } /** * Paints the given node on the canvas. * * @param canvas Canvas, where the node shall be painted on. * @param node Node which shall be painted. * @param hwAccelarationWorkaround TODO * @param drawTolerance */ private void paintNode(final Canvas canvas, final Node node, boolean hwAccelarationWorkaround, boolean drawTolerance) { int lat = node.getLat(); int lon = node.getLon(); boolean isSelected = tmpDrawingSelectedNodes != null && tmpDrawingSelectedNodes.contains(node); BoundingBox viewBox = getViewBox(); float x = GeoMath.lonE7ToX(getWidth(), viewBox, lon); float y = GeoMath.latE7ToY(getHeight(), getWidth(), viewBox, lat); boolean isTagged = node.isTagged(); boolean filteredObject = false; boolean filterMode = tmpFilter != null; // we have an active filter if (filterMode) { filteredObject = tmpFilter.include(node, isSelected); } //draw tolerance if (drawTolerance && (!filterMode || (filterMode && filteredObject))) { if (prefs.isToleranceVisible() && tmpClickableElements == null) { drawNodeTolerance(canvas, node.getState(), lat, lon, isTagged, x, y, nodeTolerancePaint); } else if (tmpClickableElements != null && tmpClickableElements.contains(node)) { drawNodeTolerance(canvas, node.getState(), lat, lon, isTagged, x, y, nodeTolerancePaint2); } } String featureKey; String featureKeyThin; String featureKeyTagged; if (isSelected && tmpDrawingInEditRange) { // general node style featureKey = DataStyle.SELECTED_NODE; // style for house numbers featureKeyThin = DataStyle.SELECTED_NODE_THIN; // style for tagged nodes or otherwise important featureKeyTagged = DataStyle.SELECTED_NODE_TAGGED; if (tmpDrawingSelectedNodes.size() == 1 && tmpDrawingSelectedWays == null && prefs.largeDragArea() && tmpDrawingEditMode.elementsGeomEditiable()) { // don't draw large areas in multi-select mode canvas.drawCircle(x, y, DataStyle.getCurrent().largDragToleranceRadius, DataStyle.getCurrent(DataStyle.NODE_DRAG_RADIUS).getPaint()); } } else if ((tmpDrawingSelectedRelationNodes != null && tmpDrawingSelectedRelationNodes.contains(node)) && tmpDrawingInEditRange) { // general node style featureKey = DataStyle.SELECTED_RELATION_NODE; // style for house numbers featureKeyThin = DataStyle.SELECTED_RELATION_NODE_THIN; // style for tagged nodes or otherwise important featureKeyTagged = DataStyle.SELECTED_RELATION_NODE_TAGGED; isSelected = true; } else if (node.hasProblem(context)) { // general node style featureKey = DataStyle.PROBLEM_NODE; // style for house numbers featureKeyThin = DataStyle.PROBLEM_NODE_THIN; // style for tagged nodes or otherwise important featureKeyTagged = DataStyle.PROBLEM_NODE_TAGGED; } else { // general node style featureKey = DataStyle.NODE; // style for house numbers featureKeyThin = DataStyle.NODE_THIN; // style for tagged nodes or otherwise important featureKeyTagged = DataStyle.NODE_TAGGED; } boolean noIcon = true; boolean isTaggedAndInZoomLimit = isTagged && inNodeIconZoomRange; if (filterMode && !filteredObject) { featureKey = DataStyle.HIDDEN_NODE; featureKeyThin = featureKey; featureKeyTagged = featureKey; isTaggedAndInZoomLimit = false; } if (isTaggedAndInZoomLimit && showIcons) { noIcon = tmpPresets == null || !paintNodeIcon(node, canvas, x, y, isSelected ? featureKeyTagged : null); if (noIcon) { String houseNumber = node.getTagWithKey(Tags.KEY_ADDR_HOUSENUMBER); if (houseNumber != null && !"".equals(houseNumber)) { // draw house-numbers paintHouseNumber(x,y,canvas,featureKeyThin,houseNumber); return; } } } if (noIcon) { // draw regular nodes or without icons Paint p = DataStyle.getCurrent(isTagged ? featureKeyTagged : featureKey).getPaint(); float strokeWidth = p.getStrokeWidth(); if (hwAccelarationWorkaround) { //FIXME we don't actually know if this is slower than drawPoint canvas.drawCircle(x, y, strokeWidth/2, p); } else { canvas.drawPoint(x, y, p); } if (isTaggedAndInZoomLimit) { paintNodeLabel(x, y, canvas, featureKeyThin, strokeWidth, node); } } } /** * Draw a circle with center at x,y with the house number in it * @param x * @param y * @param canvas * @param featureKeyThin * @param houseNumber */ private void paintHouseNumber(final float x, final float y, final Canvas canvas, final String featureKeyThin, final String houseNumber) { Paint paint2 = DataStyle.getCurrent(featureKeyThin).getPaint(); canvas.drawCircle(x, y, houseNumberRadius, paint2); canvas.drawText(houseNumber, x - (paint2.measureText(houseNumber) / 2), y + verticalNumberOffset, paint2); } /** * Praint a label under the node, does not try to do collision avoidance * @param x * @param y * @param canvas * @param featureKeyThin * @param strokeWidth * @param node */ private void paintNodeLabel(final float x, final float y, final Canvas canvas, final String featureKeyThin, final float strokeWidth, final Node node) { Paint paint2 = DataStyle.getCurrent(featureKeyThin).getPaint(); SortedMap<String, String> tags = node.getTags(); String label = labelcache.get(tags); // may be null! if (label == null) { if (!labelcache.containsKey(tags)) { label = node.getTagWithKey(Tags.KEY_NAME); if (label == null && tmpPresets != null) { PresetItem match = Preset.findBestMatch(tmpPresets,node.getTags()); if (match != null) { label = match.getTranslatedName(); } else { label = node.getPrimaryTag(); // if label is still null, leave it as is } } labelcache.put(tags,label); if (label==null) { return; } } else { return; } } canvas.drawText(label, x - (paint2.measureText(label) / 2), y + strokeWidth + 2*verticalNumberOffset, paint2); } /** * Retrieve icon for the element, caching it if it isn't in the cache * * @param element * @return icon or null if none is found */ private Bitmap getIcon(OsmElement element) { SortedMap<String, String> tags = element.getTags(); Bitmap icon = iconcache.get(tags); // may be null! if (icon == null && tmpPresets != null) { if (iconcache.containsKey(tags)) { // no point in trying to match return icon; } // icon not cached, ask the preset, render to a bitmap and cache result PresetItem match = null; if (element instanceof Way) { // don't show building icons, but only icons for buildings SortedMap<String,String> tempTags = new TreeMap<String,String>(tags); if (tempTags.remove(Tags.KEY_BUILDING) != null || element.hasTag(Tags.KEY_INDOOR,Tags.VALUE_ROOM)) { match = Preset.findBestMatch(tmpPresets,tempTags); } } else { match = Preset.findBestMatch(tmpPresets,tags); } if (match != null) { Drawable iconDrawable = match.getMapIcon(); if (iconDrawable != null) { icon = Bitmap.createBitmap(iconRadius*2, iconRadius*2, Config.ARGB_8888); // icon.eraseColor(Color.WHITE); // replace nothing with white? iconDrawable.draw(new Canvas(icon)); } } iconcache.put(tags, icon); } return icon; } /** * Paints an icon for an element. tmpPreset needs to be available (i.e. not null). * @param element the element whose icon should be painted * @param canvas the canvas on which to draw * @param x the x position where the center of the icon goes * @param y the y position where the center of the icon goes */ private boolean paintNodeIcon(OsmElement element, Canvas canvas, float x, float y, String featureKey) { Bitmap icon = getIcon(element); if (icon != null) { float w2 = icon.getWidth()/2f; float h2 = icon.getHeight()/2f; if (featureKey != null) { // selected RectF r = new RectF(x - w2 - iconSelectedBorder, y - h2 - iconSelectedBorder, x + w2 + iconSelectedBorder, y + h2 + iconSelectedBorder); canvas.drawRoundRect(r, iconSelectedBorder, iconSelectedBorder, DataStyle.getCurrent(featureKey).getPaint()); } // we have an icon! draw it. canvas.drawBitmap(icon, x - w2, y - h2, null); return true; } return false; } /** * @param canvas * @param lat * @param lon * @param isTagged TODO * @param x * @param y * @param node */ private void drawNodeTolerance(final Canvas canvas, final Byte nodeState, final int lat, final int lon, boolean isTagged, final float x, final float y, Paint paint) { if (nodeState != OsmElement.STATE_UNCHANGED || delegator.isInDownload(lat, lon)) { canvas.drawCircle(x, y, isTagged ? paint.getStrokeWidth() : wayTolerancePaint.getStrokeWidth()/2, paint); } } /** * Paints the given way on the canvas. * * @param canvas Canvas, where the node shall be painted on. * @param way way which shall be painted. * @param drawTolerance */ private void paintWay(ArrayList<Float>points, final Canvas canvas, final Way way, final boolean displayHandles, boolean drawTolerance) { float[] linePoints = pointListToLinePointsArray(points, way.getNodes()); Paint paint; boolean isSelected = tmpDrawingInEditRange // if we are not in editing range don't show selected way ... may be a better idea to do so && tmpDrawingSelectedWays != null && tmpDrawingSelectedWays.contains(way) ; boolean isMemberOfSelectedRelation = tmpDrawingInEditRange && tmpDrawingSelectedRelationWays != null && tmpDrawingSelectedRelationWays.contains(way); //draw way tolerance if (drawTolerance) { if (prefs.isToleranceVisible() && tmpClickableElements == null) { canvas.drawLines(linePoints, wayTolerancePaint); } else if (tmpClickableElements != null && tmpClickableElements.contains(way)) { canvas.drawLines(linePoints, wayTolerancePaint2); } } //draw selectedWay highlighting if (isSelected) { paint = DataStyle.getCurrent(DataStyle.SELECTED_WAY).getPaint(); canvas.drawLines(linePoints, paint); paint = DataStyle.getCurrent(DataStyle.WAY_DIRECTION).getPaint(); drawWayArrows(canvas, linePoints, false, paint, displayHandles && tmpDrawingSelectedWays.size()==1); } else if (isMemberOfSelectedRelation) { paint = DataStyle.getCurrent(DataStyle.SELECTED_RELATION_WAY).getPaint(); canvas.drawLines(linePoints, paint); paint = DataStyle.getCurrent(DataStyle.WAY_DIRECTION).getPaint(); drawWayArrows(canvas, linePoints, false, paint, false); } int onewayCode = way.getOneway(); if (onewayCode != 0) { FeatureStyle fp = DataStyle.getCurrent(DataStyle.ONEWAY_DIRECTION); drawWayArrows(canvas, linePoints, (onewayCode == -1), fp.getPaint(), false); } else if (way.getTagWithKey(Tags.KEY_WATERWAY) != null) { // waterways flow in the way direction FeatureStyle fp = DataStyle.getCurrent(DataStyle.ONEWAY_DIRECTION); drawWayArrows(canvas, linePoints, false, fp.getPaint(), false); } // FeatureStyle fp; // no need to get the default here if (way.hasProblem(context)) { fp = DataStyle.getCurrent(DataStyle.PROBLEM_WAY); } else { fp = getAndSetStyle(way); } // draw the way itself // canvas.drawLines(linePoints, fp.getPaint()); doesn't work properly with HW acceleration if (linePoints.length > 2) { path.reset(); path.moveTo(linePoints[0], linePoints[1]); for (int i=0;i<(linePoints.length);i=i+4) { path.lineTo(linePoints[i+2], linePoints[i+3]); } canvas.drawPath(path, fp.getPaint()); } // display icons on closed ways if (showIcons && showWayIcons && zoomLevel > SHOW_ICONS_LIMIT && way.isClosed()) { int vs = linePoints.length; if (vs < way.nodeCount()*2) { return; } double A = 0; double Y = 0; double X = 0; for (int i = 0; i < vs ; i=i+2 ) { // calc centroid double x1 = linePoints[i]; double y1 = linePoints[i+1]; double x2 = linePoints[(i+2) % vs]; double y2 = linePoints[(i+3) % vs]; 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); if (tmpPresets == null || !paintNodeIcon(way, canvas, (float)X, (float)Y, isSelected?DataStyle.SELECTED_NODE_TAGGED:null)) { String houseNumber = way.getTagWithKey(Tags.KEY_ADDR_HOUSENUMBER); if (houseNumber != null && !"".equals(houseNumber)) { // draw house-numbers paintHouseNumber((float)X,(float)Y,canvas,isSelected?DataStyle.SELECTED_NODE_THIN:DataStyle.NODE_THIN,houseNumber); return; } } } } } /** * Paints the given way on the canvas with the "hidden" style. * * @param canvas Canvas, where the node shall be painted on. * @param way way which shall be painted. */ private void paintHiddenWay(ArrayList<Float>points, final Canvas canvas, final Way way) { float[] linePoints = pointListToLinePointsArray(points, way.getNodes()); // FeatureStyle fp = DataStyle.getCurrent(DataStyle.HIDDEN_WAY); // draw the way itself // canvas.drawLines(linePoints, fp.getPaint()); doesn't work properly with HW acceleration if (linePoints.length > 2) { path.reset(); path.moveTo(linePoints[0], linePoints[1]); for (int i=0;i<(linePoints.length);i=i+4) { path.lineTo(linePoints[i+2], linePoints[i+3]); } canvas.drawPath(path, fp.getPaint()); } } /** * Determine the style to use for way and cache it in the way object * @param way * @return */ private FeatureStyle getAndSetStyle(final Way way) { FeatureStyle fp; FeatureStyle wayFp = way.getFeatureProfile(); if (wayFp == null) { fp = DataStyle.getCurrent(DataStyle.WAY); // default for ways // three levels of hierarchy for roads and special casing of tracks, two levels for everything else String highwayType = way.getTagWithKey(Tags.KEY_HIGHWAY); if (highwayType != null) { FeatureStyle tempFp = DataStyle.getCurrent("way-highway"); if (tempFp != null) { fp = tempFp; } tempFp = DataStyle.getCurrent("way-highway-" + highwayType); if (tempFp != null) { fp = tempFp; } String highwaySubType; if (highwayType.equals("track")) { // special case highwaySubType = way.getTagWithKey("tracktype"); } else { highwaySubType = way.getTagWithKey(highwayType); } if (highwaySubType != null) { tempFp = DataStyle.getCurrent("way-highway-" + highwayType + "-" + highwaySubType); if (tempFp != null) { fp = tempFp; } } } else { // order in the array defines precedence String[] tags = {"building","railway","leisure","landuse","waterway","natural","addr:interpolation","boundary","amenity","shop","power", "aerialway","military","historic","indoor","building:part"}; FeatureStyle tempFp = null; for (String tag:tags) { tempFp = getProfile(tag, way); if (tempFp != null) { fp = tempFp; break; } } if (tempFp == null) { ArrayList<Relation> relations = way.getParentRelations(); // check for any relation memberships with low prio, take first one String[] relationTags = {"boundary","leisure","landuse","natural","waterway","building"}; if (relations != null) { for (Relation r : relations) { for (String tag:relationTags) { tempFp = getProfile(tag, r); if (tempFp != null) { fp = tempFp; break; } } if (tempFp != null) { // break out of loop over relations break; } } } } } way.setFeatureProfile(fp); } else { fp = wayFp; } return fp; } private void paintHandles(Canvas canvas) { if (handles != null && handles.size() > 0) { canvas.save(); float lastX = 0; float lastY = 0; for (long l:handles.values()) { // draw handle // canvas.drawCircle(x0 + xDelta/2, y0 + yDelta/2, 5, Profile.getCurrent(Profile.HANDLE).getPaint()); // canvas.drawPoint(x0 + xDelta/2, y0 + yDelta/2, Profile.getCurrent(Profile.HANDLE).getPaint()); float X = Float.intBitsToFloat((int)(l>>>32)); float Y = Float.intBitsToFloat((int)(l)); canvas.translate(X-lastX, Y-lastY); lastX = X; lastY = Y; canvas.drawPath(DataStyle.getCurrent().x_path, DataStyle.getCurrent(DataStyle.HANDLE).getPaint()); } canvas.restore(); handles.clear(); // this is hopefully faster than allocating a new set } } private FeatureStyle getProfile(String tag, OsmElement e) { String mainType = e.getTagWithKey(tag); FeatureStyle fp = null; if (mainType != null) { FeatureStyle tempFp = DataStyle.getCurrent("way-" + tag); if (tempFp != null) { fp = tempFp; } tempFp = DataStyle.getCurrent("way-" + tag + "-" + mainType); if (tempFp != null) { fp = tempFp; } } return fp; } /** * Draws directional arrows for a way * @param canvas the canvas on which to draw * @param linePoints line segment array in the format returned by {@link #pointListToLinePointsArray(Iterable)}. * @param reverse if true, the arrows will be painted in the reverse direction * @param paint the paint to use for drawing the arrows * @param addHandles if true draw arrows at 1/4 and 3/4 of the length and save the middle pos. for drawing a handle */ private void drawWayArrows(Canvas canvas, float[] linePoints, boolean reverse, Paint paint, boolean addHandles) { double minLen = DataStyle.getCurrent().minLenForHandle; int ptr = 0; while (ptr < linePoints.length) { float x1 = linePoints[ptr++]; float y1 = linePoints[ptr++]; float x2 = linePoints[ptr++]; float y2 = linePoints[ptr++]; float xDelta = x2-x1; float yDelta = y2-y1; boolean secondArrow = false; if (addHandles) { double len = Math.hypot(xDelta,yDelta); if (len > minLen) { if (handles == null) handles = new LongHashSet(); handles.put(((long)(Float.floatToRawIntBits(x1 + xDelta/2)) <<32) + (long)Float.floatToRawIntBits(y1 + yDelta/2)); xDelta = xDelta / 4; yDelta = yDelta / 4; secondArrow = true; } else { xDelta = xDelta / 2; yDelta = yDelta / 2; } } else { xDelta = xDelta / 2; yDelta = yDelta / 2; } float x = x1 + xDelta; float y = y1 + yDelta; float angle = (float)(Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI); canvas.save(); canvas.translate(x,y); canvas.rotate(reverse ? angle-180 : angle); canvas.drawPath(DataStyle.WAY_DIRECTION_PATH, paint); canvas.restore(); if (secondArrow) { canvas.save(); canvas.translate(x+2*xDelta,y+2*yDelta); canvas.rotate(reverse ? angle-180 : angle); canvas.drawPath(DataStyle.WAY_DIRECTION_PATH, paint); canvas.restore(); } } } /** * Converts a geographical way/path/track to a list of screen-coordinate points for drawing. * Only segments that are inside the ViewBox are included. * * @param points list to (re-)use for projected points * @param nodes An iterable (e.g. List or array) with GeoPoints of the line that should be drawn * (e.g. a Way or a GPS track) * @return an array of floats in the format expected by {@link Canvas#drawLines(float[], Paint)}. */ private float[] pointListToLinePointsArray(final ArrayList<Float> points, final List<? extends GeoPoint> nodes) { points.clear(); // reset BoundingBox box = getViewBox(); boolean testInterrupted = false; //loop over all nodes GeoPoint prevNode = null; GeoPoint lastDrawnNode = null; float prevX=0f; float prevY=0f; int w = getWidth(); int h = getHeight(); boolean thisIntersects = false; boolean nextIntersects = false; for (int i=0;i<nodes.size();i++) { GeoPoint node = nodes.get(i); int nodeLon = node.getLon(); int nodeLat = node.getLat(); boolean interrupted = false; if (i==0) { // just do this once testInterrupted = node instanceof InterruptibleGeoPoint; } if (testInterrupted) { interrupted = ((InterruptibleGeoPoint)node).isInterrupted(); } nextIntersects = true; GeoPoint nextNode = null; int nextNodeLat = 0; int nextNodeLon = 0; if (i<nodes.size()-1) { nextNode = nodes.get(i+1); nextNodeLat = nextNode.getLat(); nextNodeLon = nextNode.getLon(); nextIntersects = box.intersects(nextNodeLat,nextNodeLon,nodeLat, nodeLon); } float X = Float.MIN_VALUE; float Y = Float.MIN_VALUE; if (!interrupted && prevNode != null) { if (thisIntersects || nextIntersects || (!(nextNode != null && lastDrawnNode != null) || box.intersects(nextNodeLat, nextNodeLon, lastDrawnNode.getLat(), lastDrawnNode.getLon()))) { X = GeoMath.lonE7ToX(w, box, nodeLon); Y = GeoMath.latE7ToY(h, w, box, nodeLat); if (prevX == Float.MIN_VALUE) { // last segment didn't intersect prevX = GeoMath.lonE7ToX(w, box, prevNode.getLon()); prevY = GeoMath.latE7ToY(h, w, box, prevNode.getLat()); } // Line segment needs to be drawn points.add(prevX); points.add(prevY); points.add(X); points.add(Y); lastDrawnNode = node; } } prevNode = node; prevX = X; prevY = Y; thisIntersects = nextIntersects; } // convert from ArrayList<Float> to float[] float[] result = new float[points.size()]; int i = 0; for (Float f : points) result[i++] = f; return result; } /** * ${@inheritDoc}. */ @Override public BoundingBox getViewBox() { return myViewBox; } /** * @param aSelectedNode the currently selected node to edit. */ void setSelectedNodes(final List<Node> aSelectedNodes) { tmpDrawingSelectedNodes = aSelectedNodes; } /** * * @param aSelectedWay the currently selected way to edit. */ void setSelectedWays(final List<Way> aSelectedWays) { tmpDrawingSelectedWays = aSelectedWays; } public Preferences getPrefs() { return prefs; } public void setPrefs(Context ctx, final Preferences aPreference) { prefs = aPreference; TileLayerServer.setBlacklist(prefs.getServer().getCachedCapabilities().imageryBlacklist); synchronized(mOverlays) { for (MapViewOverlay osmvo : mOverlays) { if (osmvo instanceof MapTilesOverlay && !(osmvo instanceof MapOverlayTilesOverlay)) { final TileLayerServer backgroundTS = TileLayerServer.get(ctx, prefs.backgroundLayer(), true); ((MapTilesOverlay)osmvo).setRendererInfo(backgroundTS); } else if (osmvo instanceof MapOverlayTilesOverlay) { final TileLayerServer overlayTS = TileLayerServer.get(ctx, prefs.overlayLayer(), true); ((MapOverlayTilesOverlay)osmvo).setRendererInfo(overlayTS); } } } showIcons = prefs.getShowIcons(); showWayIcons = prefs.getShowWayIcons(); iconcache.clear(); alwaysDrawBoundingBoxes = prefs.getAlwaysDrawBoundingBoxes(); } public void updateProfile () { // changes when profile changes nodeTolerancePaint = DataStyle.getCurrent(DataStyle.NODE_TOLERANCE).getPaint(); nodeTolerancePaint2 = DataStyle.getCurrent(DataStyle.NODE_TOLERANCE_2).getPaint(); wayTolerancePaint = DataStyle.getCurrent(DataStyle.WAY_TOLERANCE).getPaint(); wayTolerancePaint2 = DataStyle.getCurrent(DataStyle.WAY_TOLERANCE_2).getPaint(); } void setOrientation(final float orientation) { this.orientation = orientation; } void setLocation(Location location) { displayLocation = location; } void setTracker(TrackerService tracker) { this.tracker = tracker; } void setDelegator(final StorageDelegator delegator) { this.delegator = delegator; } public void setViewBox(final BoundingBox viewBox) { myViewBox = viewBox; try { myViewBox.setRatio(this, (float) getWidth()/ getHeight(), false); } catch (OsmException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void showCrosshairs(float x, float y) { showCrosshairs = true; // store as lat lon for redraws on translation and zooming crosshairsLat = GeoMath.yToLatE7(getHeight(), getWidth(), getViewBox(), y); crosshairsLon = GeoMath.xToLonE7(getWidth() , getViewBox(), x); } public void hideCrosshairs() { showCrosshairs = false; } /** * You can add/remove/reorder your Overlays using the List of * {@link MapViewOverlay}. The first (index 0) Overlay gets drawn * first, the one with the highest as the last one. */ public List<MapViewOverlay> getOverlays() { return mOverlays; } /** * ${@inheritDoc}. */ @Override public int getZoomLevel() { return zoomLevel; } /** * This calculates the best tile zoom level to use (not the actual zoom level of the map!) * @param viewPort * @return the tile zoom level */ private int calcZoomLevel(Canvas canvas) { final TileLayerServer s = getOpenStreetMapTilesOverlay().getRendererInfo(); if (!s.isMetadataLoaded()) // protection on startup return 0; // Calculate lat/lon of view extents final double latBottom = getViewBox().getBottom() / 1E7; // GeoMath.yToLatE7(viewPort.height(), getViewBox(), viewPort.bottom) / 1E7d; final double lonRight = getViewBox().getRight() / 1E7; // GeoMath.xToLonE7(viewPort.width() , getViewBox(), viewPort.right ) / 1E7d; final double latTop = getViewBox().getTop() / 1E7; // GeoMath.yToLatE7(viewPort.height(), getViewBox(), viewPort.top ) / 1E7d; final double lonLeft = getViewBox().getLeft() / 1E7; // GeoMath.xToLonE7(viewPort.width() , getViewBox(), viewPort.left ) / 1E7d; // Calculate tile x/y scaled 0.0 to 1.0 final double xTileRight = (lonRight + 180d) / 360d; final double xTileLeft = (lonLeft + 180d) / 360d; final double yTileBottom = (1d - Math.log(Math.tan(Math.toRadians(latBottom)) + 1d / Math.cos(Math.toRadians(latBottom))) / Math.PI) / 2d; final double yTileTop = (1d - Math.log(Math.tan(Math.toRadians(latTop )) + 1d / Math.cos(Math.toRadians(latTop ))) / Math.PI) / 2d; // Calculate the ideal zoom to fit into the view final double xTiles = (canvas.getWidth() / (xTileRight - xTileLeft)) / s.getTileWidth(); final double yTiles = (canvas.getHeight() / (yTileBottom - yTileTop )) / s.getTileHeight(); final double xZoom = Math.log(xTiles) / Math.log(2d); final double yZoom = Math.log(yTiles) / Math.log(2d); // Zoom out to the next integer step int zoom = (int)Math.floor(Math.max(0, Math.min(xZoom, yZoom))); zoom = Math.min(zoom, s.getMaxZoomLevel()); return zoom; } public Location getLocation() { return displayLocation; } /** * Set the flag that determines if the arror is just an outline or not * @param follow */ public void setFollowGPS(boolean follow) { isFollowingGPS = follow; } /** * Return a list of the names of the currently used layers * @return */ public ArrayList<String> getImageryNames() { ArrayList<String>result = new ArrayList<String>(); synchronized(mOverlays) { for (MapViewOverlay osmvo : mOverlays) { if ((osmvo instanceof MapTilesOverlay)) { result.add(((MapTilesOverlay)osmvo).getRendererInfo().getName()); } } } return result; } }