/* * Copyright 2010, 2011, 2012 mapsforge.org * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with * this program. If not, see <http://www.gnu.org/licenses/>. */ package org.mapsforge.android.maps; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.mapsforge.android.AndroidUtils; import org.mapsforge.android.maps.inputhandling.MapMover; import org.mapsforge.android.maps.inputhandling.TouchEventHandler; import org.mapsforge.android.maps.inputhandling.ZoomAnimator; import org.mapsforge.android.maps.mapgenerator.FileSystemTileCache; import org.mapsforge.android.maps.mapgenerator.InMemoryTileCache; import org.mapsforge.android.maps.mapgenerator.JobParameters; import org.mapsforge.android.maps.mapgenerator.JobQueue; import org.mapsforge.android.maps.mapgenerator.MapGeneratorJob; import org.mapsforge.android.maps.mapgenerator.MapRenderer; import org.mapsforge.android.maps.mapgenerator.MapWorker; import org.mapsforge.android.maps.mapgenerator.TileCache; import org.mapsforge.android.maps.mapgenerator.databaserenderer.DatabaseRenderer; import org.mapsforge.android.maps.mapgenerator.mbtiles.MbTilesDatabaseRenderer; import org.mapsforge.android.maps.overlay.Overlay; import org.mapsforge.android.maps.overlay.OverlayController; import org.mapsforge.core.model.GeoPoint; import org.mapsforge.core.model.MapPosition; import org.mapsforge.core.model.Tile; import org.mapsforge.core.util.MercatorProjection; import org.mapsforge.map.reader.MapDatabase; import org.mapsforge.map.reader.header.FileOpenResult; import org.mapsforge.map.rendertheme.ExternalRenderTheme; import org.mapsforge.map.rendertheme.InternalRenderTheme; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.Canvas; import android.os.Handler; import android.os.Message; import android.preference.PreferenceManager; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewGroup; /** * A MapView shows a map on the display of the device. It handles all user input and touch gestures to move and zoom the * map. This MapView also includes a scale bar and zoom controls. The {@link #getMapViewPosition()} method returns a * {@link MapViewPosition} to programmatically modify the position and zoom level of the map. * <p> * A binary database file is required which contains the map data. Map files can be stored in any folder. The current * map file is set by calling {@link #setMapFile(File)}. To retrieve the current {@link MapDatabase}, use the * {@link #getMapDatabase()} method. * <p> * {@link Overlay Overlays} can be used to display geographical data such as points and ways. To draw an overlay on top * of the map, add it to the list returned by {@link #getOverlays()}. */ public class MapView extends ViewGroup { /** * Default render theme of the MapView. */ public static final InternalRenderTheme DEFAULT_RENDER_THEME = InternalRenderTheme.OSMARENDER; private static final float DEFAULT_TEXT_SCALE = 1; private static final int DEFAULT_TILE_CACHE_SIZE_FILE_SYSTEM = 100; private static final int DEFAULT_TILE_CACHE_SIZE_IN_MEMORY = 20; public static final String MAPSFORGE_BACKGROUND_FILEPATH = "mapsforge_background_filepath"; public static final String MAPSFORGE_BACKGROUND_FILEPATH_CHANGED = "mapsforge_background_file_changed"; public static final String MAPSFORGE_BACKGROUND_RENDERER_TYPE = "mapsforge_background_type"; private MapRenderer mapRenderer; private DebugSettings debugSettings; private final TileCache fileSystemTileCache; private final FpsCounter fpsCounter; private final FrameBuffer frameBuffer; private final TileCache inMemoryTileCache; private JobParameters jobParameters; private final JobQueue jobQueue; private final MapDatabase mapDatabase; private File mapFile; private final MapMover mapMover; private final MapScaleBar mapScaleBar; private final MapViewPosition mapViewPosition; private final MapWorker mapWorker; private final MapZoomControls mapZoomControls; private final OverlayController overlayController; private final List<Overlay> overlays; private final Projection projection; private final TouchEventHandler touchEventHandler; private final ZoomAnimator zoomAnimator; private Handler handler; /** * @param context * the enclosing MapActivity instance. * @throws IllegalArgumentException * if the context object is not an instance of {@link MapActivity}. */ public MapView(Context context) { this(context, null); } /** * @param context * the enclosing MapActivity instance. * @param attributeSet * a set of attributes. * @throws IllegalArgumentException * if the context object is not an instance of {@link MapActivity}. */ public MapView(Context context, AttributeSet attributeSet) { super(context, attributeSet); if (!(context instanceof MapActivity)) { throw new IllegalArgumentException("context is not an instance of MapActivity"); } MapActivity mapActivity = (MapActivity) context; setBackgroundColor(FrameBuffer.MAP_VIEW_BACKGROUND); setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS); setWillNotDraw(false); this.debugSettings = new DebugSettings(false, false, false); this.fileSystemTileCache = new FileSystemTileCache(DEFAULT_TILE_CACHE_SIZE_FILE_SYSTEM, mapActivity.getMapViewId()); this.fpsCounter = new FpsCounter(); this.frameBuffer = new FrameBuffer(this); this.inMemoryTileCache = new InMemoryTileCache(DEFAULT_TILE_CACHE_SIZE_IN_MEMORY); this.jobParameters = new JobParameters(DEFAULT_RENDER_THEME, DEFAULT_TEXT_SCALE); this.jobQueue = new JobQueue(this); this.mapDatabase = new MapDatabase(); this.mapViewPosition = new MapViewPosition(this); this.mapScaleBar = new MapScaleBar(this); this.mapZoomControls = new MapZoomControls(context, this); this.overlays = Collections.synchronizedList(new ArrayList<Overlay>()); this.projection = new MapViewProjection(this); this.touchEventHandler = new TouchEventHandler(mapActivity.getActivityContext(), this); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); BackgroundSourceType type = BackgroundSourceType.values()[Integer.parseInt(prefs.getString( MAPSFORGE_BACKGROUND_RENDERER_TYPE, "0"))]; // TODO add a default filepath inside the downloaded zip file // like MapFilesProvider.getEnvironmentDirPath(null) + MapFilesProvider.getBaseDir() + fileName.mbtiles final String filePath = prefs.getString(MAPSFORGE_BACKGROUND_FILEPATH, null); if (filePath == null && type == BackgroundSourceType.MBTILES) { // no chance to use MBTiles come background without file // -> use Mapsforge instead type = BackgroundSourceType.MAPSFORGE; } MapRenderer _mapRenderer = null; switch (type) { case MAPSFORGE: _mapRenderer = new DatabaseRenderer(this.mapDatabase); break; case MBTILES: _mapRenderer = new MbTilesDatabaseRenderer(this.getContext(), filePath); break; case GEOCOLLECT: // TODO break; default: break; } setRenderer(_mapRenderer, false); this.mapWorker = new MapWorker(this); this.mapWorker.start(); this.mapMover = new MapMover(this); this.mapWorker.setDatabaseRenderer(this.mapRenderer); this.mapMover.start(); this.zoomAnimator = new ZoomAnimator(this); this.zoomAnimator.start(); this.handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case (0): loadStop(); break; case (1): loadStart(); break; } } }; this.overlayController = new OverlayController(this, handler); this.overlayController.start(); GeoPoint startPoint = this.mapRenderer.getStartPoint(); Byte startZoomLevel = this.mapRenderer.getStartZoomLevel(); if (startPoint != null) { this.mapViewPosition.setCenter(startPoint); } if (startZoomLevel != null) { this.mapViewPosition.setZoomLevel(startZoomLevel.byteValue()); } mapActivity.registerMapView(this); } protected void loadStart() { // empty } protected void loadStop() { // empty } /** * @return the currently used DatabaseRenderer (may be null). */ public MapRenderer getDatabaseRenderer() { return this.mapRenderer; } /** * @return the debug settings which are used in this MapView. */ public DebugSettings getDebugSettings() { return this.debugSettings; } /** * @return the file system tile cache which is used in this MapView. */ public TileCache getFileSystemTileCache() { return this.fileSystemTileCache; } /** * @return the FPS counter which is used in this MapView. */ public FpsCounter getFpsCounter() { return this.fpsCounter; } /** * @return the FrameBuffer which is used in this MapView. */ public FrameBuffer getFrameBuffer() { return this.frameBuffer; } /** * @return the in-memory tile cache which is used in this MapView. */ public TileCache getInMemoryTileCache() { return this.inMemoryTileCache; } /** * @return the job queue which is used in this MapView. */ public JobQueue getJobQueue() { return this.jobQueue; } /** * @return the map database which is used for reading map files. */ public MapDatabase getMapDatabase() { return this.mapDatabase; } /** * @return the currently used map file. */ public File getMapFile() { return this.mapFile; } public MapRenderer getMapRenderer() { return this.mapRenderer; } public BackgroundSourceType getMapRendererType() { if (this.mapRenderer instanceof DatabaseRenderer) { return BackgroundSourceType.MAPSFORGE; } else if (this.mapRenderer instanceof MbTilesDatabaseRenderer) { return BackgroundSourceType.MBTILES; } else { return BackgroundSourceType.GEOCOLLECT; } } public String getMapRendererFile() { return this.getMapRenderer().getFileName(); } public boolean usesMapsforgeBackground() { return this.getMapRendererType() == BackgroundSourceType.MAPSFORGE; } public void setRenderer(final MapRenderer pMapRenderer, final boolean setMapWorkerRenderer) { if (this.mapRenderer != null) { this.mapRenderer.destroy(); } this.mapRenderer = pMapRenderer; if (setMapWorkerRenderer) { this.mapWorker.setDatabaseRenderer(this.mapRenderer); } } /** * @return the MapMover which is used by this MapView. */ public MapMover getMapMover() { return this.mapMover; } /** * @return the scale bar which is used in this MapView. */ public MapScaleBar getMapScaleBar() { return this.mapScaleBar; } /** * @return the current position and zoom level of this MapView. */ public MapViewPosition getMapViewPosition() { return this.mapViewPosition; } /** * @return the zoom controls instance which is used in this MapView. */ public MapZoomControls getMapZoomControls() { return this.mapZoomControls; } public OverlayController getOverlayController() { return this.overlayController; } /** * Returns a thread-safe list of overlays for this MapView. It is necessary to manually synchronize on this list * when iterating over it. * * @return the overlay list. */ public List<Overlay> getOverlays() { return this.overlays; } /** * @return the currently used projection of the map. Do not keep this object for a longer time. */ public Projection getProjection() { return this.projection; } public ZoomAnimator getZoomAnimator() { return this.zoomAnimator; } /** * Calls either {@link #invalidate()} or {@link #postInvalidate()}, depending on the current thread. */ public void invalidateOnUiThread() { if (AndroidUtils.currentThreadIsUiThread()) { invalidate(); } else { postInvalidate(); } } /** * @return true if the ZoomAnimator is currently running, false otherwise. */ public boolean isZoomAnimatorRunning() { return this.zoomAnimator.isExecuting(); } @Override public boolean onKeyDown(int keyCode, KeyEvent keyEvent) { return this.mapMover.onKeyDown(keyCode, keyEvent); } @Override public boolean onKeyUp(int keyCode, KeyEvent keyEvent) { return this.mapMover.onKeyUp(keyCode, keyEvent); } @Override public boolean onTouchEvent(MotionEvent motionEvent) { int action = TouchEventHandler.getAction(motionEvent); this.mapZoomControls.onMapViewTouchEvent(action); if (!isClickable()) { return true; } return this.touchEventHandler.onTouchEvent(motionEvent); } @Override public boolean onTrackballEvent(MotionEvent motionEvent) { return this.mapMover.onTrackballEvent(motionEvent); } /** * redraw all the mapview, with also overlays */ public void redraw() { redraw(true); } /** * Triggers a redraw process of the map. * * @param forceOverlayRedraw * if true, redraws the overlays */ public void redraw(boolean forceOverlayRedraw) { if (this.getWidth() <= 0 || this.getHeight() <= 0 || isZoomAnimatorRunning()) { return; } MapPosition mapPosition = this.mapViewPosition.getMapPosition(); if (this.mapFile != null) { GeoPoint geoPoint = mapPosition.geoPoint; double pixelLeft = MercatorProjection.longitudeToPixelX(geoPoint.longitude, mapPosition.zoomLevel); double pixelTop = MercatorProjection.latitudeToPixelY(geoPoint.latitude, mapPosition.zoomLevel); pixelLeft -= getWidth() >> 1; pixelTop -= getHeight() >> 1; long tileLeft = MercatorProjection.pixelXToTileX(pixelLeft, mapPosition.zoomLevel); long tileTop = MercatorProjection.pixelYToTileY(pixelTop, mapPosition.zoomLevel); long tileRight = MercatorProjection.pixelXToTileX(pixelLeft + getWidth(), mapPosition.zoomLevel); long tileBottom = MercatorProjection.pixelYToTileY(pixelTop + getHeight(), mapPosition.zoomLevel); final boolean usesMBTilesRenderer = this.mapRenderer instanceof MbTilesDatabaseRenderer; for (long tileY = tileTop; tileY <= tileBottom; ++tileY) { for (long tileX = tileLeft; tileX <= tileRight; ++tileX) { Tile tile = new Tile(tileX, tileY, mapPosition.zoomLevel); MapGeneratorJob mapGeneratorJob = new MapGeneratorJob(tile, this.mapFile, this.jobParameters, this.debugSettings); if (this.inMemoryTileCache.containsKey(mapGeneratorJob) && !usesMBTilesRenderer) { Bitmap bitmap = this.inMemoryTileCache.get(mapGeneratorJob); this.frameBuffer.drawBitmap(mapGeneratorJob.tile, bitmap); } else if (this.fileSystemTileCache.containsKey(mapGeneratorJob) && !usesMBTilesRenderer) { Bitmap bitmap = this.fileSystemTileCache.get(mapGeneratorJob); if (bitmap != null) { this.frameBuffer.drawBitmap(mapGeneratorJob.tile, bitmap); // only cache "real" mapsforge tiles, no mb tiles if (!usesMBTilesRenderer) { this.inMemoryTileCache.put(mapGeneratorJob, bitmap); } } else { // the image data could not be read from the cache this.jobQueue.addJob(mapGeneratorJob); } } else { // cache miss this.jobQueue.addJob(mapGeneratorJob); } } } this.jobQueue.requestSchedule(); synchronized (this.mapWorker) { this.mapWorker.notify(); } } if (forceOverlayRedraw == true) { this.overlayController.redrawOverlays(); } if (this.mapScaleBar.isShowMapScaleBar()) { this.mapScaleBar.redrawScaleBar(); } invalidateOnUiThread(); } /** * Sets the visibility of the zoom controls. * * @param showZoomControls * true if the zoom controls should be visible, false otherwise. */ public void setBuiltInZoomControls(boolean showZoomControls) { this.mapZoomControls.setShowMapZoomControls(showZoomControls); } /** * @param debugSettings * the new DebugSettings for this MapView. */ public void setDebugSettings(DebugSettings debugSettings) { this.debugSettings = debugSettings; clearAndRedrawMapView(); } /** * Sets the map file for this MapView. * * @param mapFile * the map file. * @return a FileOpenResult to describe whether the operation returned successfully. * @throws IllegalArgumentException * if the supplied mapFile is null. */ public FileOpenResult setMapFile(File mapFile) { if (mapFile == null) { throw new IllegalArgumentException("mapFile must not be null"); } else if (mapFile.equals(this.mapFile)) { // same map file as before return FileOpenResult.SUCCESS; } this.zoomAnimator.pause(); this.mapWorker.pause(); this.mapMover.pause(); this.zoomAnimator.awaitPausing(); this.mapMover.awaitPausing(); this.mapWorker.awaitPausing(); this.mapMover.stopMove(); this.jobQueue.clear(); this.zoomAnimator.proceed(); this.mapWorker.proceed(); this.mapMover.proceed(); this.mapDatabase.closeFile(); FileOpenResult fileOpenResult = this.mapDatabase.openFile(mapFile); if (fileOpenResult.isSuccess()) { this.mapFile = mapFile; GeoPoint startPoint = this.mapRenderer.getStartPoint(); if (startPoint != null) { this.mapViewPosition.setCenter(startPoint); } Byte startZoomLevel = this.mapRenderer.getStartZoomLevel(); if (startZoomLevel != null) { this.mapViewPosition.setZoomLevel(startZoomLevel.byteValue()); } clearAndRedrawMapView(); return FileOpenResult.SUCCESS; } this.mapFile = null; clearAndRedrawMapView(); return fileOpenResult; } /** * Sets the XML file which is used for rendering the map. * * @param renderThemeFile * the XML file which defines the rendering theme. * @throws IllegalArgumentException * if the supplied internalRenderTheme is null. * @throws FileNotFoundException * if the supplied file does not exist, is a directory or cannot be read. */ public void setRenderTheme(File renderThemeFile) throws FileNotFoundException { if (renderThemeFile == null) { throw new IllegalArgumentException("render theme file must not be null"); } org.mapsforge.map.rendertheme.XmlRenderTheme jobTheme = new ExternalRenderTheme(renderThemeFile); this.jobParameters = new JobParameters(jobTheme, this.jobParameters.textScale); clearAndRedrawMapView(); } /** * Sets the internal theme which is used for rendering the map. * * @param internalRenderTheme * the internal rendering theme. * @throws IllegalArgumentException * if the supplied internalRenderTheme is null. */ public void setRenderTheme(InternalRenderTheme internalRenderTheme) { if (internalRenderTheme == null) { throw new IllegalArgumentException("render theme must not be null"); } this.jobParameters = new JobParameters(internalRenderTheme, this.jobParameters.textScale); clearAndRedrawMapView(); } /** * Sets the text scale for the map rendering. Has no effect in downloading mode. * * @param textScale * the new text scale for the map rendering. */ public void setTextScale(float textScale) { this.jobParameters = new JobParameters(this.jobParameters.jobTheme, textScale); clearAndRedrawMapView(); } /** * Takes a screenshot of the currently visible map and saves it as a compressed image. Zoom buttons, scale bar, FPS * counter, overlays, menus and the title bar are not included in the screenshot. * * @param outputFile * the image file. If the file already exists, it will be overwritten. * @param compressFormat * the file format of the compressed image. * @param quality * value from 0 (low) to 100 (high). Has no effect on some formats like PNG. * @return true if the image was saved successfully, false otherwise. * @throws IOException * if an error occurs while writing the image file. */ public boolean takeScreenshot(CompressFormat compressFormat, int quality, File outputFile) throws IOException { FileOutputStream outputStream = new FileOutputStream(outputFile); boolean success = this.frameBuffer.compress(compressFormat, quality, outputStream); outputStream.close(); return success; } @Override protected void onDraw(Canvas canvas) { this.frameBuffer.draw(canvas); this.overlayController.draw(canvas); if (this.mapScaleBar.isShowMapScaleBar()) { this.mapScaleBar.draw(canvas); } if (this.fpsCounter.isShowFpsCounter()) { this.fpsCounter.draw(canvas); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { this.mapZoomControls.onLayout(changed, left, top, right, bottom); } @Override protected final void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // find out how big the zoom controls should be this.mapZoomControls.measure( MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.AT_MOST)); // make sure that MapView is big enough to display the zoom controls setMeasuredDimension(Math.max(MeasureSpec.getSize(widthMeasureSpec), this.mapZoomControls.getMeasuredWidth()), Math.max(MeasureSpec.getSize(heightMeasureSpec), this.mapZoomControls.getMeasuredHeight())); } @Override protected synchronized void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { this.frameBuffer.destroy(); if (width > 0 && height > 0) { this.frameBuffer.onSizeChanged(); this.overlayController.onSizeChanged(); redraw(); } } public void clearAndRedrawMapView() { this.jobQueue.clear(); this.frameBuffer.clear(); redraw(); } public void destroy() { this.overlayController.interrupt(); this.mapMover.interrupt(); this.mapWorker.interrupt(); this.zoomAnimator.interrupt(); try { this.mapWorker.join(); } catch (InterruptedException e) { // restore the interrupted status Thread.currentThread().interrupt(); } this.frameBuffer.destroy(); this.mapScaleBar.destroy(); this.inMemoryTileCache.destroy(); this.fileSystemTileCache.destroy(); this.mapRenderer.destroy(); this.mapDatabase.closeFile(); } /** * @return the maximum possible zoom level. */ public byte getZoomLevelMax() { return (byte) Math.min(this.mapZoomControls.getZoomLevelMax(), this.mapRenderer.getZoomLevelMax()); } public void onPause() { this.mapWorker.pause(); this.mapMover.pause(); this.zoomAnimator.pause(); } public void onResume() { this.mapWorker.proceed(); this.mapMover.proceed(); this.zoomAnimator.proceed(); } }