package com.qozix.mapview.tiles; import java.util.HashMap; import java.util.LinkedList; import android.annotation.SuppressLint; import android.content.Context; import android.os.AsyncTask; import android.view.View; import android.widget.ImageView; import com.qozix.layouts.FixedLayout; import com.qozix.layouts.ScalingLayout; import com.qozix.mapview.zoom.ZoomLevel; import com.qozix.mapview.zoom.ZoomListener; import com.qozix.mapview.zoom.ZoomManager; @SuppressLint("UseSparseArrays") public class TileManager extends ScalingLayout implements ZoomListener { private static final int RENDER_FLAG = 1; private static final int RENDER_BUFFER = 250; private LinkedList<MapTile> scheduledToRender = new LinkedList<MapTile>(); private LinkedList<MapTile> alreadyRendered = new LinkedList<MapTile>(); private MapTileDecoder decoder = new MapTileDecoderAssets(); private MapTileEnhancer enhancer; private HashMap<Integer, ScalingLayout> tileGroups = new HashMap<Integer, ScalingLayout>(); private TileRenderListener renderListener; private MapTileCache cache; private ZoomLevel zoomLevelToRender; private TileRenderTask lastRunRenderTask; private ScalingLayout currentTileGroup; private ZoomManager zoomManager; private int lastRenderedZoom = -1; private boolean renderIsCancelled = false; private boolean renderIsSuppressed = false; private boolean isRendering = false; private TileRenderHandler handler; public TileManager( Context context, ZoomManager zm ) { super( context ); zoomManager = zm; zoomManager.addZoomListener( this ); handler = new TileRenderHandler( this ); } public void setDecoder( MapTileDecoder d ){ decoder = d; } public void setEnhancer( MapTileEnhancer e) { enhancer = e; } public void setCacheEnabled( boolean shouldCache ) { if ( shouldCache ){ if ( cache == null ){ cache = new MapTileCache( getContext() ); } } else { if ( cache != null ) { cache.destroy(); } cache = null; } } public void setTileRenderListener( TileRenderListener listener ){ renderListener = listener; } public void requestRender() { // if we're requesting it, we must really want one renderIsCancelled = false; renderIsSuppressed = false; // if there's no data about the current zoom level, don't bother if ( zoomLevelToRender == null ) { return; } // throttle requests if ( handler.hasMessages( RENDER_FLAG ) ) { handler.removeMessages( RENDER_FLAG ); } // give it enough buffer that (generally) successive calls will be captured handler.sendEmptyMessageDelayed( RENDER_FLAG, RENDER_BUFFER ); } public void cancelRender() { // hard cancel - this applies to *all* tasks, not just the currently executing task renderIsCancelled = true; // if the currently executing task isn't null... if ( lastRunRenderTask != null ) { // ... and it's in a cancellable state if ( lastRunRenderTask.getStatus() != AsyncTask.Status.FINISHED ) { // ... then squash it lastRunRenderTask.cancel( true ); } } // give it to gc lastRunRenderTask = null; } public void suppressRender() { // this will prevent new tasks from starting, but won't actually cancel the currently executing task renderIsSuppressed = true; } public void updateTileSet() { // fast fail if there aren't any zoom levels registered int numZoomLevels = zoomManager.getNumZoomLevels(); if ( numZoomLevels == 0 ) { return; } // what zoom level should we be showing? int zoom = zoomManager.getZoom(); // fast-fail if there's no change if ( zoom == lastRenderedZoom ) { return; } // save it so we can detect change next time lastRenderedZoom = zoom; // grab reference to this zoom level, so we can get it's tile set for comparison to viewport zoomLevelToRender = zoomManager.getCurrentZoomLevel(); // fetch appropriate child currentTileGroup = getCurrentTileGroup(); // made it this far, so currentTileGroup should be valid, so update clipping updateViewClip( currentTileGroup ); // get the appropriate zoom double scale = zoomManager.getInvertedScale(); // scale the group currentTileGroup.setScale( scale ); // show it currentTileGroup.setVisibility( View.VISIBLE ); // bring it to top of stack currentTileGroup.bringToFront(); } public boolean getIsRendering() { return isRendering; } public void clear() { // suppress and cancel renders suppressRender(); cancelRender(); // destroy all tiles for ( MapTile m : scheduledToRender ) { m.destroy(); } scheduledToRender.clear(); for ( MapTile m : alreadyRendered ) { m.destroy(); } alreadyRendered.clear(); // the above should clear everything, but let's be redundant for ( ScalingLayout tileGroup : tileGroups.values() ) { int totalChildren = tileGroup.getChildCount(); for ( int i = 0; i < totalChildren; i++ ) { View child = tileGroup.getChildAt( i ); if ( child instanceof ImageView ) { ImageView imageView = (ImageView) child; imageView.setImageBitmap( null ); } } tileGroup.removeAllViews(); } } private ScalingLayout getCurrentTileGroup() { int zoom = zoomManager.getZoom(); // if a tile group has already been created and registered, return it if ( tileGroups.containsKey( zoom ) ) { return tileGroups.get( zoom ); } // otherwise create one, register it, and add it to the view tree ScalingLayout tileGroup = new ScalingLayout( getContext() ); tileGroups.put( zoom, tileGroup ); addView( tileGroup ); return tileGroup; } // access omitted deliberately - need package level access for the TileRenderHandler void renderTiles() { // has it been canceled since it was requested? if ( renderIsCancelled ) { return; } // can we keep rending existing tasks, but not start new ones? if ( renderIsSuppressed ) { return; } // fast-fail if there's no available data if ( zoomLevelToRender == null ) { return; } // decode and render the bitmaps asynchronously beginRenderTask(); } private void updateViewClip( View view ) { LayoutParams lp = (LayoutParams) view.getLayoutParams(); lp.width = zoomManager.getComputedCurrentWidth(); lp.height = zoomManager.getComputedCurrentHeight(); view.setLayoutParams( lp ); } private void beginRenderTask() { // find all matching tiles LinkedList<MapTile> intersections = zoomLevelToRender.getIntersections(); // if it's the same list, don't bother if ( scheduledToRender.equals( intersections ) ) { return; } // if we made it here, then replace the old list with the new list scheduledToRender = intersections; // cancel task if it's already running if ( lastRunRenderTask != null ) { if ( lastRunRenderTask.getStatus() != AsyncTask.Status.FINISHED ) { lastRunRenderTask.cancel( true ); } } // start a new one lastRunRenderTask = new TileRenderTask( this ); lastRunRenderTask.execute(); } private FixedLayout.LayoutParams getLayoutFromTile( MapTile m ) { int w = m.getWidth(); int h = m.getHeight(); int x = m.getLeft(); int y = m.getTop(); return new FixedLayout.LayoutParams( w, h, x, y ); } private void cleanup() { // start with all rendered tiles... LinkedList<MapTile> condemned = new LinkedList<MapTile>( alreadyRendered ); // now remove all those that were just qualified condemned.removeAll( scheduledToRender ); // for whatever's left, destroy and remove from list for ( MapTile m : condemned ) { m.destroy(); alreadyRendered.remove( m ); } // hide all other groups for ( ScalingLayout tileGroup : tileGroups.values() ) { if ( currentTileGroup == tileGroup ) { continue; } tileGroup.setVisibility( View.GONE ); } } /* * render tasks (invoked in asynctask's thread) */ void onRenderTaskPreExecute(){ // set a flag that we're working isRendering = true; // notify anybody interested if ( renderListener != null ) { renderListener.onRenderStart(); } } void onRenderTaskCancelled() { if ( renderListener != null ) { renderListener.onRenderCancelled(); } isRendering = false; } void onRenderTaskPostExecute() { // set flag that we're done isRendering = false; // everything's been rendered, so get rid of the old tiles cleanup(); // recurse - request another round of render - if the same intersections are discovered, recursion will end anyways requestRender(); // notify anybody interested if ( renderListener != null ) { renderListener.onRenderComplete(); } } LinkedList<MapTile> getRenderList(){ return new LinkedList<MapTile>( scheduledToRender ); } void decodeIndividualTile( MapTile m ) { m.decode( getContext(), cache, decoder, enhancer ); } void renderIndividualTile( MapTile m ) { if ( alreadyRendered.contains( m ) ) { return; } m.render( getContext() ); alreadyRendered.add( m ); ImageView i = m.getImageView(); LayoutParams l = getLayoutFromTile( m ); currentTileGroup.addView( i, l ); } boolean getRenderIsCancelled() { return renderIsCancelled; } @Override public void onZoomLevelChanged( int oldZoom, int newZoom ) { updateTileSet(); } @Override public void onZoomScaleChanged( double scale ) { setScale( scale ); } }