/* * 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.List; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewGroup; 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.JobTheme; import org.mapsforge.android.maps.mapgenerator.MapGenerator; import org.mapsforge.android.maps.mapgenerator.MapGeneratorFactory; import org.mapsforge.android.maps.mapgenerator.MapGeneratorJob; 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.databaserenderer.ExternalRenderTheme; import org.mapsforge.android.maps.mapgenerator.tiledownloader.TileDownloader; import org.mapsforge.android.maps.overlay.Overlay; import org.mapsforge.android.maps.overlay.OverlayList; import org.mapsforge.android.maps.rendertheme.InternalRenderTheme; 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 eu.geopaparazzi.library.database.GPLog; /** * 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 #getController()} method returns a * {@link MapController} to programmatically modify the position and zoom level of the map. * <p/> * This implementation supports offline map rendering as well as downloading map images (tiles) over an Internet * connection. The operation mode of a MapView can be set in the constructor and changed at runtime with the * {@link #setMapGeneratorInternal(MapGenerator)} method. Some MapView parameters depend on the selected operation mode. * <p/> * In offline rendering mode a special 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; private DebugSettings debugSettings; private TileCache fileSystemTileCache; private final FpsCounter fpsCounter; private final FrameBuffer frameBuffer; private TileCache inMemoryTileCache; private JobParameters jobParameters; private final JobQueue jobQueue; private final MapController mapController; private final MapDatabase mapDatabase; private File mapFile; private MapGenerator mapGenerator; private final MapMover mapMover; private final MapScaleBar mapScaleBar; private final MapViewPosition mapViewPosition; private final MapWorker mapWorker; private final MapZoomControls mapZoomControls; private final List<Overlay> overlays; private final Projection projection; private final TouchEventHandler touchEventHandler; private final ZoomAnimator zoomAnimator; /** * @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, new DatabaseRenderer()); } /** * @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) { this(context, attributeSet, MapGeneratorFactory.createMapGenerator(attributeSet)); } /** * @param context the enclosing MapActivity instance. * @param mapGenerator the MapGenerator for this MapView. * @throws IllegalArgumentException if the context object is not an instance of {@link MapActivity}. */ public MapView(Context context, MapGenerator mapGenerator) { this(context, null, mapGenerator); } private MapView(Context context, AttributeSet attributeSet, MapGenerator mapGenerator) { 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); try { this.fileSystemTileCache = new FileSystemTileCache(DEFAULT_TILE_CACHE_SIZE_FILE_SYSTEM, mapActivity.getMapViewId()); } catch (Exception e) { GPLog.error(this, "ERROR:", e); } this.inMemoryTileCache = new InMemoryTileCache(DEFAULT_TILE_CACHE_SIZE_IN_MEMORY); this.fpsCounter = new FpsCounter(); this.frameBuffer = new FrameBuffer(this); this.jobParameters = new JobParameters(DEFAULT_RENDER_THEME, DEFAULT_TEXT_SCALE); this.jobQueue = new JobQueue(this); this.mapController = new MapController(this); this.mapDatabase = new MapDatabase(); this.mapViewPosition = new MapViewPosition(this); this.mapScaleBar = new MapScaleBar(this); this.mapZoomControls = new MapZoomControls(mapActivity, this); this.overlays = new OverlayList(this); this.projection = new MapViewProjection(this); this.touchEventHandler = TouchEventHandler.getInstance(mapActivity, this); this.mapWorker = new MapWorker(this); this.mapWorker.start(); this.mapMover = new MapMover(this); this.mapMover.start(); this.zoomAnimator = new ZoomAnimator(this); this.zoomAnimator.start(); setMapGeneratorInternal(mapGenerator); GeoPoint startPoint = this.mapGenerator.getStartPoint(); if (startPoint != null) { this.mapViewPosition.setMapCenter(startPoint); } Byte startZoomLevel = this.mapGenerator.getStartZoomLevel(); if (startZoomLevel != null) { this.mapViewPosition.setZoomLevel(startZoomLevel.byteValue()); } mapActivity.registerMapView(this); } /** * @return the MapController for this MapView. */ public MapController getController() { return this.mapController; } /** * @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; } /** * @param capacity the in-memory tile cache capacity to use. */ public void setInMemoryTileCacheSize(int capacity) { this.inMemoryTileCache.destroy(); this.inMemoryTileCache = null; this.inMemoryTileCache = new InMemoryTileCache(capacity); } /** * @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. * @throws UnsupportedOperationException if the current MapGenerator works with an Internet connection. */ public MapDatabase getMapDatabase() { if (this.mapGenerator.requiresInternetConnection()) { throw new UnsupportedOperationException(); } return this.mapDatabase; } /** * @return the currently used map file. * @throws UnsupportedOperationException if the current MapGenerator mode works with an Internet connection. */ public File getMapFile() { if (this.mapGenerator.requiresInternetConnection()) { throw new UnsupportedOperationException(); } return this.mapFile; } /** * @return the currently used MapGenerator (may be null). */ public MapGenerator getMapGenerator() { return this.mapGenerator; } /** * @return the MapMover which is used by this MapView. */ public MapMover getMapMover() { return this.mapMover; } /** * @return the current position and zoom level of this MapView. */ public MapViewPosition getMapPosition() { return this.mapViewPosition; } /** * @return the scale bar which is used in this MapView. */ public MapScaleBar getMapScaleBar() { return this.mapScaleBar; } /** * @return the zoom controls instance which is used in this MapView. */ public MapZoomControls getMapZoomControls() { return this.mapZoomControls; } /** * 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; } /** * 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 = this.touchEventHandler.getAction(motionEvent); this.mapZoomControls.onMapViewTouchEvent(action); return this.touchEventHandler.handleTouchEvent(motionEvent); } @Override public boolean onTrackballEvent(MotionEvent motionEvent) { return this.mapMover.onTrackballEvent(motionEvent); } /** * Calculates all necessary tiles and adds jobs accordingly. */ public void redrawTiles() { if (this.getWidth() <= 0 || this.getHeight() <= 0) { return; } synchronized (this.overlays) { for (int i = 0, n = this.overlays.size(); i < n; ++i) { this.overlays.get(i).requestRedraw(); } } MapPosition mapPosition = this.mapViewPosition.getMapPosition(); if (mapPosition == null) { return; } GeoPoint geoPoint = mapPosition.geoPoint; double pixelLeft = MercatorProjection.longitudeToPixelX(geoPoint.getLongitude(), mapPosition.zoomLevel); double pixelTop = MercatorProjection.latitudeToPixelY(geoPoint.getLatitude(), 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); Object cacheId; if (this.mapGenerator.requiresInternetConnection()) { cacheId = ((TileDownloader) this.mapGenerator).getHostName(); } else { cacheId = this.mapFile; } 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, cacheId, this.jobParameters, this.debugSettings); if (this.inMemoryTileCache.containsKey(mapGeneratorJob)) { Bitmap bitmap = this.inMemoryTileCache.get(mapGeneratorJob); this.frameBuffer.drawBitmap(mapGeneratorJob.tile, bitmap); } else if (this.fileSystemTileCache != null && this.fileSystemTileCache.containsKey(mapGeneratorJob)) { Bitmap bitmap = this.fileSystemTileCache.get(mapGeneratorJob); if (bitmap != null) { this.frameBuffer.drawBitmap(mapGeneratorJob.tile, bitmap); 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); } } } if (this.mapScaleBar.isShowMapScaleBar()) { this.mapScaleBar.redrawScaleBar(); } invalidateOnUiThread(); this.jobQueue.requestSchedule(); synchronized (this.mapWorker) { this.mapWorker.notify(); } } /** * 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); } /** * Sets the center of the MapView and triggers a redraw. * * @param geoPoint the new center point of the map. */ public void setCenter(GeoPoint geoPoint) { MapPosition mapPosition = new MapPosition(geoPoint, this.mapViewPosition.getZoomLevel()); setCenterAndZoom(mapPosition); } /** * @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 UnsupportedOperationException if the current MapGenerator mode works with an Internet connection. * @throws IllegalArgumentException if the supplied mapFile is null. */ public FileOpenResult setMapFile(File mapFile) { if (this.mapGenerator.requiresInternetConnection()) { throw new UnsupportedOperationException(); } 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.mapGenerator.getStartPoint(); if (startPoint != null) { this.mapViewPosition.setMapCenter(startPoint); } Byte startZoomLevel = this.mapGenerator.getStartZoomLevel(); if (startZoomLevel != null) { this.mapViewPosition.setZoomLevel(startZoomLevel.byteValue()); } clearAndRedrawMapView(); return FileOpenResult.SUCCESS; } this.mapFile = null; clearAndRedrawMapView(); return fileOpenResult; } /** * Sets the MapGenerator for this MapView. * * @param mapGenerator the new MapGenerator. */ public void setMapGenerator(MapGenerator mapGenerator) { if (this.mapGenerator != mapGenerator) { setMapGeneratorInternal(mapGenerator); clearAndRedrawMapView(); } } /** * 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 UnsupportedOperationException if the current MapGenerator does not support render themes. * @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"); } else if (this.mapGenerator.requiresInternetConnection()) { throw new UnsupportedOperationException(); } JobTheme 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. * @throws UnsupportedOperationException if the current MapGenerator does not support render themes. */ public void setRenderTheme(InternalRenderTheme internalRenderTheme) { if (internalRenderTheme == null) { throw new IllegalArgumentException("render theme must not be null"); } else if (this.mapGenerator.requiresInternetConnection()) { throw new UnsupportedOperationException(); } 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; } /** * Zooms in or out by the given amount of zoom levels. * * @param zoomLevelDiff the difference to the current zoom level. * @param zoomStart the zoom factor at the begin of the animation. * @return true if the zoom level was changed, false otherwise. */ public boolean zoom(byte zoomLevelDiff, float zoomStart) { float matrixScaleFactor; if (zoomLevelDiff > 0) { // check if zoom in is possible if (this.mapViewPosition.getZoomLevel() + zoomLevelDiff > getMaximumPossibleZoomLevel()) { return false; } matrixScaleFactor = 1 << zoomLevelDiff; } else if (zoomLevelDiff < 0) { // check if zoom out is possible if (this.mapViewPosition.getZoomLevel() + zoomLevelDiff < this.mapZoomControls.getZoomLevelMin()) { return false; } matrixScaleFactor = 1.0f / (1 << -zoomLevelDiff); } else { // zoom level is unchanged matrixScaleFactor = 1; } this.mapViewPosition.setZoomLevel((byte) (this.mapViewPosition.getZoomLevel() + zoomLevelDiff)); this.mapZoomControls.onZoomLevelChange(this.mapViewPosition.getZoomLevel()); this.zoomAnimator.setParameters(zoomStart, matrixScaleFactor, getWidth() >> 1, getHeight() >> 1); this.zoomAnimator.startAnimation(); return true; } private void setMapGeneratorInternal(MapGenerator mapGenerator) { if (mapGenerator == null) { throw new IllegalArgumentException("mapGenerator must not be null"); } if (mapGenerator instanceof DatabaseRenderer) { ((DatabaseRenderer) mapGenerator).setMapDatabase(this.mapDatabase); } this.mapGenerator = mapGenerator; this.mapWorker.setMapGenerator(this.mapGenerator); } @Override protected void onDraw(Canvas canvas) { this.frameBuffer.draw(canvas); synchronized (this.overlays) { for (int i = 0, n = this.overlays.size(); i < n; ++i) { try { this.overlays.get(i).draw(canvas); } catch (Exception e) { android.util.Log.e("MAPSFORG#MAPVIEW#ONDRAW", "Problems drawing overlay", e); e.printStackTrace(); } } } 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(); redrawTiles(); synchronized (this.overlays) { for (int i = 0, n = this.overlays.size(); i < n; ++i) { this.overlays.get(i).onSizeChanged(); } } } } void clearAndRedrawMapView() { this.jobQueue.clear(); this.frameBuffer.clear(); redrawTiles(); } void destroy() { this.overlays.clear(); 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.touchEventHandler.destroy(); this.mapScaleBar.destroy(); this.inMemoryTileCache.destroy(); this.fileSystemTileCache.destroy(); this.mapDatabase.closeFile(); } /** * @return the maximum possible zoom level. */ byte getMaximumPossibleZoomLevel() { return (byte) Math.min(this.mapZoomControls.getZoomLevelMax(), this.mapGenerator.getZoomLevelMax()); } /** * @return true if the current center position of this MapView is valid, false otherwise. */ boolean hasValidCenter() { if (!this.mapViewPosition.isValid()) { return false; } else if (!this.mapGenerator.requiresInternetConnection() && (!this.mapDatabase.hasOpenFile() || !this.mapDatabase.getMapFileInfo().boundingBox .contains(getMapPosition().getMapCenter()))) { return false; } return true; } byte limitZoomLevel(byte zoom) { return (byte) Math.max(Math.min(zoom, getMaximumPossibleZoomLevel()), this.mapZoomControls.getZoomLevelMin()); } void onPause() { this.mapWorker.pause(); this.mapMover.pause(); this.zoomAnimator.pause(); } void onResume() { this.mapWorker.proceed(); this.mapMover.proceed(); this.zoomAnimator.proceed(); } /** * Sets the center and zoom level of this MapView and triggers a redraw. * * @param mapPosition the new map position of this MapView. */ void setCenterAndZoom(MapPosition mapPosition) { if (hasValidCenter()) { // calculate the distance between previous and current position MapPosition mapPositionOld = this.mapViewPosition.getMapPosition(); GeoPoint geoPointOld = mapPositionOld.geoPoint; GeoPoint geoPointNew = mapPosition.geoPoint; double oldPixelX = MercatorProjection.longitudeToPixelX(geoPointOld.getLongitude(), mapPositionOld.zoomLevel); double newPixelX = MercatorProjection.longitudeToPixelX(geoPointNew.getLongitude(), mapPosition.zoomLevel); double oldPixelY = MercatorProjection.latitudeToPixelY(geoPointOld.getLatitude(), mapPositionOld.zoomLevel); double newPixelY = MercatorProjection.latitudeToPixelY(geoPointNew.getLatitude(), mapPosition.zoomLevel); float matrixTranslateX = (float) (oldPixelX - newPixelX); float matrixTranslateY = (float) (oldPixelY - newPixelY); this.frameBuffer.matrixPostTranslate(matrixTranslateX, matrixTranslateY); } this.mapViewPosition.setMapCenterAndZoomLevel(mapPosition); this.mapZoomControls.onZoomLevelChange(this.mapViewPosition.getZoomLevel()); redrawTiles(); } }