package de.blau.android.views.overlay; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.os.Message; import android.support.annotation.Nullable; import android.support.v4.app.FragmentActivity; import android.util.Log; import android.view.MotionEvent; import android.view.View; import de.blau.android.Map; import de.blau.android.dialogs.Progress; import de.blau.android.osm.BoundingBox; import de.blau.android.resources.DataStyle; import de.blau.android.resources.TileLayerServer; import de.blau.android.services.util.MapTile; import de.blau.android.util.GeoMath; import de.blau.android.util.Offset; import de.blau.android.util.Snack; import de.blau.android.views.IMapView; import de.blau.android.views.util.MapTileProvider; /** * Overlay that draws downloaded tiles which may be displayed on top of an * {@link IMapView}. To add an overlay, subclass this class, create an * instance, and add it to the list obtained from getOverlays() of * {@link Map}. * <br/> * This class was taken from OpenStreetMapViewer (original package org.andnav.osm) in 2010-06 * and changed significantly by Marcus Wolschon to be integrated into the de.blau.androin * OSMEditor. * @author Nicolas Gramlich * @author Marcus Wolschon <Marcus@Wolschon.biz> */ public class MapTilesOverlay extends MapViewOverlay { private static final String DEBUG_TAG = MapTilesOverlay.class.getSimpleName(); /** Define a minimum active area for taps on the tile attribution data. */ private static final int TAPAREA_MIN_WIDTH = 40; private static final int TAPAREA_MIN_HEIGHT = 40; /** * */ private boolean coverageWarningDisplayed = false; private String coverageWarningMessage; /** Tap tracking */ private float downX, downY; private boolean moved; private static Rect tapArea = null; /** * The view we are a part of. */ private View myView; /** * The tile-server to load a rendered map from. */ private TileLayerServer myRendererInfo; /** Current renderer */ private final MapTileProvider mTileProvider; private final Paint mPaint = new Paint(); private Paint textPaint = new Paint(); /** * * @param aView The view we are a part of. * @param aRendererInfo The tile-server to load a rendered map from. * @param aTileProvider (may be null) */ public MapTilesOverlay(final View aView, final TileLayerServer aRendererInfo, final MapTileProvider aTileProvider) { myView = aView; setRendererInfo(aRendererInfo); if(aTileProvider == null) { mTileProvider = new MapTileProvider(myView.getContext(), new SimpleInvalidationHandler(myView)); } else { mTileProvider = aTileProvider; } // textPaint = DataStyle.getCurrent(DataStyle.ATTRIBUTION_TEXT).getPaint(); // mPaint.setAlpha(aRendererInfo.getDefaultAlpha()); Log.d(DEBUG_TAG,"provider " + aRendererInfo.getId() + " min zoom " + aRendererInfo.getMinZoomLevel() + " max " + aRendererInfo.getMaxZoomLevel()); } @Override public boolean isReadyToDraw() { return myRendererInfo.isMetadataLoaded(); } /** * Empty the cache * * @param activity activity this was called from, if null don't display progress */ public void flushTileCache(@Nullable final FragmentActivity activity) { new AsyncTask<Void,Void,Void>() { @Override protected void onPreExecute() { if (activity != null) { Progress.showDialog(activity, Progress.PROGRESS_DELETING); } } @Override protected Void doInBackground(Void... params) { mTileProvider.flushCache(myRendererInfo.getId()); return null; } @Override protected void onPostExecute(Void result) { if (activity != null) { Progress.dismissDialog(activity, Progress.PROGRESS_DELETING); } } }.execute(); } @Override public void onDestroy() { super.onDestroy(); mTileProvider.clear(); } /** * Try to reduce memory use. */ @Override public void onLowMemory() { super.onLowMemory(); // The tile provider with its cache consumes the most memory. mTileProvider.onLowMemory(); } public TileLayerServer getRendererInfo() { return myRendererInfo; } /** * Set the tile layer to display * * Updates warning message if we are outside of coverage * @param tileLayer layer to use */ public void setRendererInfo(final TileLayerServer tileLayer) { if (myRendererInfo != tileLayer) { try { coverageWarningMessage = myView.getResources().getString(de.blau.android.R.string.toast_no_coverage,tileLayer.getName()); } catch (Exception ex) { coverageWarningMessage = ""; } coverageWarningDisplayed = false; } myRendererInfo = tileLayer; } public MapTileProvider getTileProvider() { return mTileProvider; } public void setContrast(float a) { // mPaint.setAlpha(a); float scale = a + 1.f; float translate = (-.5f * scale + .5f) * 255.f; ColorMatrix cm = new ColorMatrix(); cm.set(new float[] { scale, 0, 0, 0, translate, 0, scale, 0, 0, translate, 0, 0, scale, 0, translate, 0, 0, 0, 1, 0 }); mPaint.setColorFilter(new ColorMatrixColorFilter(cm)); } /** * @param x a x tile -number * @param aZoomLevel a zoom-level of a tile * @return the longitude of the tile */ private static double tile2lon(int x, int aZoomLevel) { return x / Math.pow(2.0, aZoomLevel) * 360.0 - 180; } /** * @param y a y tile -number * @param aZoomLevel a zoom-level of a tile * @return the latitude of the tile */ private static double tile2lat(int y, int aZoomLevel) { double n = Math.PI - (2.0 * Math.PI * y) / Math.pow(2.0, aZoomLevel); return Math.toDegrees(Math.atan(Math.sinh(n))); } /** * {@inheritDoc}. */ @Override protected void onDraw(Canvas c, IMapView osmv) { BoundingBox viewBox = osmv.getViewBox(); if (!myRendererInfo.covers(viewBox)) { if (!coverageWarningDisplayed) { coverageWarningDisplayed = true; Snack.toastTopWarning(myView.getContext(), coverageWarningMessage); } return; // no point, return immediately } coverageWarningDisplayed = false; long owner = (long) (Math.random() * Long.MAX_VALUE); // unique values so that we can track in the cache which invocation of onDraw the tile belongs too // Do some calculations and drag attributes to local variables to save //some performance. final Rect viewPort = c.getClipBounds(); final int zoomLevel = osmv.getZoomLevel(); // if (zoomLevel < myRendererInfo.getMinZoomLevel()) { // Log.d("OpenStreetMapTilesOverlay","Tiles for " + myRendererInfo.getId() + " are not available for zoom " + zoomLevel); // return; // } double lonOffset = 0d; double latOffset = 0d; Offset offset = myRendererInfo.getOffset(zoomLevel); if (offset != null) { lonOffset = offset.lon; latOffset = offset.lat; } final MapTile tile = new MapTile(myRendererInfo.getId(), 0, 0, 0); // reused instance of OpenStreetMapTile final MapTile originalTile = new MapTile(tile); // final double lonLeft = viewBox.getLeft() / 1E7d - (lonOffset > 0 ? lonOffset : 0d); final double lonRight = viewBox.getRight() / 1E7d - (lonOffset < 0 ? lonOffset : 0d); final double latTop = Math.toRadians(viewBox.getTop() / 1E7d - (latOffset < 0 ? latOffset : 0d)); final double latBottom = Math.toRadians(viewBox.getBottom() / 1E7d - (latOffset > 0 ? latOffset : 0d)); // pseudo-code for lon/lat to tile numbers //n = 2 ^ zoom //xtile = ((lon_deg + 180) / 360) * n //ytile = (1 - (log(tan(lat_rad) + sec(lat_rad)) / PI)) / 2 * n final double n = Math.pow(2d, zoomLevel); final int xTileLeft = (int) Math.floor(((lonLeft + 180d) / 360d) * n); final int xTileRight = (int) Math.floor(((lonRight + 180d) / 360d) * n); // Log.d("OpenStreetMapTilesOverlay","tileleft " + xTileLeft + " tileright " + xTileRight + " lonRight " + lonRight + " zoom " + zoomLevel); final int yTileTop = (int) Math.floor((1d - Math.log(Math.tan(latTop) + 1d / Math.cos(latTop)) / Math.PI) * n / 2d); final int yTileBottom = (int) Math.floor((1d - Math.log(Math.tan(latBottom) + 1d / Math.cos(latBottom)) / Math.PI) * n / 2d); final int tileNeededLeft = Math.min(xTileLeft, xTileRight); final int tileNeededRight = Math.max(xTileLeft, xTileRight); final int tileNeededTop = Math.min(yTileTop, yTileBottom); final int tileNeededBottom = Math.max(yTileTop, yTileBottom); // Log.d("OpenStreetMapTileOverlay","zoom " + zoomLevel + " tile left " + xTileLeft + " right " + xTileRight + " top " +yTileTop + " bottom " + yTileBottom); // Log.d("OpenStreetMapTileOverlay"," top " + tileNeededTop + " bottom " + tileNeededBottom); // Log.d("OpenStreetMapTileOverlay","lonLeft " + lonLeft + " lonRight " + lonRight + " latTop " + Math.toDegrees(latTop)+ " latBottom " + Math.toDegrees(latBottom)); int maxZoom = myRendererInfo.getMaxZoomLevel(); int minZoom = myRendererInfo.getMinZoomLevel(); int maxOverZoom = myRendererInfo.getMaxOverZoom(); // Currently not useful for bing // int tempMaxZoom = myRendererInfo.getMaxZoom(viewBox); // if (tempMaxZoom != -1) { // Log.d(DEBUG_TAG,"area max zoom " + tempMaxZoom + " regular " + maxZoom + " maxOverZoom " + maxOverZoom + " current zoom " + zoomLevel); // maxZoom = tempMaxZoom; // } final int mapTileMask = (1 << zoomLevel) - 1; Rect destRect = null; // destination rect for bit map int destIncX = 0, destIncY = 0; int xPos = 0, yPos = 0; boolean squareTiles = myRendererInfo.getTileWidth() == myRendererInfo.getTileHeight(); // Draw all the MapTiles that intersect with the screen // y = y tile number (latitude) // int requiredTiles = (tileNeededBottom - tileNeededTop + 1) * (tileNeededRight - tileNeededLeft + 1); // Log.d("OpenStreetMapTileOverlay", "" + requiredTiles + " tiles needed to cover the screen at this level"); for (int y = tileNeededTop; y <= tileNeededBottom; y++) { // x = x tile number (longitude) for (int x = tileNeededLeft; x <= tileNeededRight; x++) { tile.reinit(); // Set the specifications for the required tile tile.zoomLevel = zoomLevel; tile.x = x & mapTileMask; tile.y = y & mapTileMask; originalTile.zoomLevel = tile.zoomLevel; originalTile.x = tile.x; originalTile.y = tile.y; // destination rect if (destRect == null) { // avoid recalculating this for every tile destRect = getScreenRectForTile(c, osmv, zoomLevel, y, x, squareTiles, lonOffset, latOffset); destIncX = destRect.width(); destIncY = destRect.height(); // Log.d("OpenStreetMapTileOverlay","tile width " + destIncX + " height " + destIncY); } // Set the size and top left corner on the source bitmap int sw = myRendererInfo.getTileWidth(); int sh = myRendererInfo.getTileHeight(); int tx = 0; int ty = 0; Bitmap tileBitmap = null; // only actually try to get tile if in range if (tile.zoomLevel >= minZoom && tile.zoomLevel <= maxZoom) { tileBitmap = mTileProvider.getMapTile(tile, owner); } if (tileBitmap == null) { tile.reinit(); // Log.d("OpenStreetMapTileOverlay","tile " + tile.toString() + " not available trying larger"); // OVERZOOM // Preferred tile is not available - request it // mTileProvider.preCacheTile(tile); already done in getMapTile // See if there are any alternative tiles available - try // using larger tiles // maximum maxOverZoom zoom levels up, with standard tiles this reduces the width to 64 bits while ((tileBitmap == null) && (zoomLevel - tile.zoomLevel) <= maxOverZoom && tile.zoomLevel > minZoom) { // As we zoom out to larger-scale tiles, we want to // draw smaller and smaller sections of them sw >>= 1; // smaller size sh >>= 1; tx >>= 1; // smaller offsets ty >>= 1; // select the correct quarter if ((tile.x & 1) != 0) { tx += (myRendererInfo.getTileWidth() >> 1); } if ((tile.y & 1) != 0) { ty += (myRendererInfo.getTileHeight() >> 1); } // zoom out to next level tile.x >>= 1; tile.y >>= 1; --tile.zoomLevel; // Log.d("OpenStreetMapTileOverlay","trying zoom level " + tile.zoomLevel); tileBitmap = mTileProvider.getMapTileFromCache(tile, owner); if (tileBitmap == null && (originalTile.zoomLevel > maxZoom && tile.zoomLevel == maxZoom)) { // Only try this it we are overzooming in which case we -do- want to retrieve the maxZoom tiles if we don't have them // Log.d("OpenStreetMapTileOverlay","larger tile " + tile.toString() + " available"); tileBitmap = mTileProvider.getMapTile(tile, owner); } } } if (tileBitmap != null) { // Log.d("OpenStreetMapTilesOverlay","tile x " + tile.x + " left " + destRect.left + " right " + destRect.right + xPos); c.drawBitmap( tileBitmap, new Rect(tx, ty, tx + sw, ty + sh), new Rect(destRect.left + xPos, destRect.top + yPos, destRect.right + xPos, destRect.bottom + yPos), mPaint); } else { tile.reinit(); // Still no tile available - try smaller scale tiles if (!drawTile(owner, c, osmv, 0, zoomLevel + 2, zoomLevel, x & mapTileMask, y & mapTileMask, squareTiles, lonOffset, latOffset)) { // Log.d("OpenStreetMapTileOverlay","no usable tiles found"); // store an error tile tile.zoomLevel = zoomLevel; tile.x = x & mapTileMask; tile.y = y & mapTileMask; if (!mTileProvider.isTileAvailable(originalTile)) { // might have turned up in the mean time // mTileProvider.cacheError(originalTile); } } } // Log.d("OpenStreetMapTileOverlay","Dest rect " + (destRect.left + xPos) + " " + (destRect.right + xPos) + " " + (destRect.top + yPos) + " " + (destRect.bottom + yPos)); xPos += destIncX; } xPos = 0; yPos += destIncY; } // Draw the tile layer branding logo (if it exists) if (tapArea == null) { resetAttributionArea(viewPort, 0); } Drawable brandLogo = myRendererInfo.getBrandLogo(); if (brandLogo != null) { tapArea.top -= brandLogo.getIntrinsicHeight(); tapArea.right += brandLogo.getIntrinsicWidth(); brandLogo.setBounds(tapArea); brandLogo.draw(c); } // Draw the attributions (if any) for (String attr : myRendererInfo.getAttributions(zoomLevel, osmv.getViewBox())) { c.drawText(attr, tapArea.left, tapArea.top, textPaint); tapArea.top -= textPaint.getTextSize(); tapArea.right = Math.max(tapArea.right, tapArea.left + (int)textPaint.measureText(attr)); } // Impose a minimum tap area if (tapArea.width() < TAPAREA_MIN_WIDTH) { tapArea.right = tapArea.left + TAPAREA_MIN_WIDTH; } //TODO fix, causes problems with multiple layers // if (tapArea.height() < TAPAREA_MIN_HEIGHT) { // tapArea.top = tapArea.bottom - TAPAREA_MIN_HEIGHT; // } } public static void resetAttributionArea(Rect viewPort, int bottomOffset) { if (tapArea == null) { tapArea = new Rect(); } tapArea.left = 0; tapArea.right = 0; tapArea.top = viewPort.bottom - bottomOffset; tapArea.bottom = viewPort.bottom - bottomOffset; } /** Recursively search the cache for smaller tiles to fill in the required * space. * @param owner TODO * @param c Canvas to draw on. * @param osmv Map view area. * @param minz Minimum zoom level. * @param maxz Maximum zoom level to attempt - don't take too long searching. * @param z Zoom level to draw. * @param x Tile X to draw. * @param y Tile Y to draw. * @param lonOffset TODO * @param latOffset TODO */ private boolean drawTile(long owner, Canvas c, IMapView osmv, int minz, int maxz, int z, int x, int y, boolean squareTiles, double lonOffset, double latOffset) { final MapTile tile = new MapTile(myRendererInfo.getId(), z, x, y); Bitmap bitmap = mTileProvider.getMapTileFromCache(tile, owner); if (bitmap != null) { // Log.d("OpenStreetMapTileOverlay","smaller tile " + tile.toString() + " available"); c.drawBitmap( bitmap, new Rect(0, 0, myRendererInfo.getTileWidth(), myRendererInfo.getTileHeight()), getScreenRectForTile(c, osmv, z, y, x, squareTiles, lonOffset, latOffset), mPaint); return true; } else { if (z < maxz && z < myRendererInfo.getMaxZoomLevel()) { // Log.d("OpenStreetMapTileOverlay","tile not available trying smaller"); // try smaller scale tiles x <<= 1; y <<= 1; ++z; // Log.d("OpenStreetMapTileOverlay","trying higher zoom level " + z); boolean result = drawTile(owner, c, osmv, z, maxz, z , x , y, squareTiles, lonOffset, latOffset); result = drawTile(owner, c, osmv, z, maxz, z, x + 1 , y, squareTiles, lonOffset, latOffset) && result; result = drawTile(owner, c, osmv, z, maxz, z , x, y + 1, squareTiles, lonOffset, latOffset) && result; result = drawTile(owner, c, osmv, z, maxz, z, x + 1, y + 1, squareTiles, lonOffset, latOffset) && result; return result; } else { // final fail return false; } } } /** * NOTE: currently assumes square tiles * @param c the canvas we draw to (we need its clip-bound's width and height) * @param osmv the view with its viewBox * @param zoomLevel the zoom-level of the tile * @param y the y-number of the tile * @param x the x-number of the tile * @param lonOffset TODO * @param latOffset TODO * @return the rectangle of screen-coordinates it consumes. */ private Rect getScreenRectForTile(Canvas c, IMapView osmv, final int zoomLevel, int y, int x, boolean squareTiles, double lonOffset, double latOffset) { double north = tile2lat(y , zoomLevel); // double south = tile2lat(y + 1, zoomLevel); only calculate when needed (aka non square tiles) double west = tile2lon(x , zoomLevel); double east = tile2lon(x + 1, zoomLevel); int w = c.getClipBounds().width(); int h = c.getClipBounds().height(); int screenLeft = (int) GeoMath.lonE7ToX(w , osmv.getViewBox(), (int) ((west + lonOffset) * 1E7)); int tileWidth = 1 + (int) Math.floor((double)(east - west) * 1E7 * w / osmv.getViewBox().getWidth()); // calculate here to avoid rounding differences int screenTop = (int) GeoMath.latE7ToY(h, w, osmv.getViewBox(), (int) ((north + latOffset)* 1E7)); int screenBottom = squareTiles ? screenTop + tileWidth : (int) GeoMath.latE7ToY(h, w, osmv.getViewBox(), (int) ((tile2lat(y + 1, zoomLevel) + latOffset)* 1E7)); // Log.d("OpenStreeMapTileOverlay", "Dest Rect " + screenLeft + " " + screenTop + " " + screenRight + " " + screenBottom); return new Rect(screenLeft, screenTop, screenLeft+tileWidth, screenBottom); } /** * {@inheritDoc}. */ @Override public void onDrawFinished(Canvas c, IMapView osmv) { } /** * Handle touch events in order to display tile layer End User Terms Of Use * when the tile provider branding logo or attributions are tapped. * @param event The touch event information. * @param mapView The parent map view of this overlay. * @return true if the event was handled. */ @Override public boolean onTouchEvent(MotionEvent event, IMapView mapView) { boolean done = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: downX = event.getX(); downY = event.getY(); moved = false; break; case MotionEvent.ACTION_UP: done = true; // FALL THROUGH case MotionEvent.ACTION_MOVE: moved |= (Math.abs(event.getX() - downX) > 20 || Math.abs(event.getY() - downY) > 20); if (done && !moved && (tapArea != null) && tapArea.contains((int)event.getX(), (int)event.getY())) { String touUri = myRendererInfo.getTouUri(); if (touUri != null) { // Display the End User Terms Of Use (in the browser) Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(touUri)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); myView.getContext().startActivity(intent); return true; } } break; } return false; } /** * Invalidate myView when a new tile got downloaded. */ private static class SimpleInvalidationHandler extends Handler { private View v; private int viewInvalidates = 0; public SimpleInvalidationHandler(View v) { super(); this.v = v; } class R implements Runnable { @Override public void run() { // Log.d("OpenStreetMapOverlay", "SimpleInvalidationHandler #viewInvalidates " + viewInvalidates); // if (!drawing) { // don't invalidate when we are drawing viewInvalidates = 0; v.invalidate(); //} } } @Override public void handleMessage(final Message msg) { switch (msg.what) { case MapTile.MAPTILE_SUCCESS_ID: // Log.d("OpenStreetMapTileOverlay","received invalidate"); if (viewInvalidates == 0) { // try to suppress inordinate number of invalidates Handler handler = new Handler(); handler.postDelayed(new R(), 100); // wait 1/10th of a second viewInvalidates++; } else viewInvalidates++; break; } } } }