/*
* 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.overlay;
import java.util.List;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.mapsforge.android.maps.MapView;
import org.mapsforge.android.maps.PausableThread;
import org.mapsforge.android.maps.Projection;
import org.mapsforge.core.model.BoundingBox;
import org.mapsforge.core.model.MapPosition;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Point;
import android.os.Handler;
public class OverlayController extends PausableThread {
private static final String THREAD_NAME = OverlayController.class.getSimpleName();
private final ReentrantReadWriteLock sizeChange = new ReentrantReadWriteLock();
private Bitmap bitmap1;
private Bitmap bitmap2;
private boolean changeSizeNeeded;
private int width;
private int height;
private final MapView mapView;
private final Matrix matrix;
private Canvas overlayCanvas;
private boolean redrawNeeded;
private Handler handler;
public OverlayController(MapView mapView, Handler h) {
super();
this.handler = h;
this.mapView = mapView;
this.matrix = new Matrix();
this.changeSizeNeeded = true;
}
/**
* Draws this {@code OverlayController} on the given canvas.
*
* @param canvas
* the canvas on which this {@code OverlayController} should draw itself.
*/
public void draw(Canvas canvas) {
if (this.bitmap1 != null) {
synchronized (this.matrix) {
canvas.drawBitmap(this.bitmap1, this.matrix, null);
}
}
}
/**
* Must be called whenever the size of the enclosing {@link MapView} has changed.
*/
public void onSizeChanged() {
this.changeSizeNeeded = true;
wakeUpThread();
}
/**
* @param scaleX
* the horizontal scale.
* @param scaleY
* the vertical scale.
* @param pivotX
* the horizontal pivot point in pixel.
* @param pivotY
* the vertical pivot point in pixel.
*/
public void postScale(float scaleX, float scaleY, float pivotX, float pivotY) {
synchronized (this.matrix) {
this.matrix.postScale(scaleX, scaleY, pivotX, pivotY);
}
}
/**
* @param translateX
* the horizontal translation in pixel.
* @param translateY
* the vertical translation in pixel.
*/
public void postTranslate(float translateX, float translateY) {
synchronized (this.matrix) {
this.matrix.postTranslate(translateX, translateY);
}
}
/**
* Requests a redraw of all overlays.
*/
public void redrawOverlays() {
this.redrawNeeded = true;
wakeUpThread();
}
private void adjustMatrix(MapPosition mapPositionBefore, MapPosition mapPositionAfter) {
Projection projection = this.mapView.getProjection();
Point pointBefore = projection.toPoint(mapPositionBefore.geoPoint, null, mapPositionBefore.zoomLevel);
Point pointAfter = projection.toPoint(mapPositionAfter.geoPoint, null, mapPositionBefore.zoomLevel);
int zoomLevelDiff = mapPositionAfter.zoomLevel - mapPositionBefore.zoomLevel;
float scaleFactor = (float) Math.pow(2, zoomLevelDiff);
int pivotX = this.overlayCanvas.getWidth() / 2;
int pivotY = this.overlayCanvas.getHeight() / 2;
this.matrix.reset();
this.matrix.postTranslate(pointBefore.x - pointAfter.x, pointBefore.y - pointAfter.y);
this.matrix.postScale(scaleFactor, scaleFactor, pivotX, pivotY);
}
private boolean changeSize() {
int newWidth = this.mapView.getWidth();
int newHeight = this.mapView.getHeight();
// size will only be changed if the view has been laid out (so width/height >0) and
// when the size is not the same as the size requested before (this stops duplicate
// requests)
if (newWidth > 0 && newHeight > 0) {
if (this.width == newWidth && this.height == newHeight) {
this.changeSizeNeeded = false;
this.redrawNeeded = false;
return false;
}
recycleBitmaps();
this.width = newWidth;
this.height = newHeight;
this.bitmap1 = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
this.bitmap2 = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
this.changeSizeNeeded = false;
this.redrawNeeded = true;
return true;
}
return false;
}
private void checkRedraw() {
// can draw concurrently unless size is being changed
this.sizeChange.readLock().lock();
try {
if (this.redrawNeeded) {
this.redrawNeeded = false;
beforeRedraw();
redraw();
afterRedraw();
}
} finally {
this.sizeChange.readLock().unlock();
}
}
private boolean checkSize() {
// write lock to stop all threads using the overlay bitmap
this.sizeChange.writeLock().lock();
try {
if (this.changeSizeNeeded) {
return changeSize();
}
return true;
} finally {
this.sizeChange.writeLock().unlock();
}
}
private void recycleBitmaps() {
if (this.bitmap1 != null) {
this.bitmap1.recycle();
this.bitmap1 = null;
}
if (this.bitmap2 != null) {
this.bitmap2.recycle();
this.bitmap2 = null;
}
// the overlay canvas needs to be set to null,
// otherwise bitmaps may not be recycled immediately.
// see: https://code.google.com/p/android/issues/detail?id=8488
this.overlayCanvas = null;
}
private void redraw() {
if (this.overlayCanvas == null) {
this.overlayCanvas = new Canvas();
}
this.bitmap2.eraseColor(Color.TRANSPARENT);
this.overlayCanvas.setBitmap(this.bitmap2);
MapPosition mapPositionBefore = this.mapView.getMapViewPosition().getMapPosition();
BoundingBox boundingBox = this.mapView.getMapViewPosition().getBoundingBox();
List<Overlay> overlays = this.mapView.getOverlays();
synchronized (overlays) {
for (Overlay overlay : overlays) {
overlay.draw(boundingBox, mapPositionBefore.zoomLevel, this.overlayCanvas);
}
}
MapPosition mapPositionAfter = this.mapView.getMapViewPosition().getMapPosition();
synchronized (this.matrix) {
adjustMatrix(mapPositionBefore, mapPositionAfter);
swapBitmaps();
}
this.mapView.postInvalidate();
}
private void swapBitmaps() {
Bitmap bitmapTemp = this.bitmap1;
this.bitmap1 = this.bitmap2;
this.bitmap2 = bitmapTemp;
}
private void wakeUpThread() {
synchronized (this) {
notify();
}
}
@Override
protected void afterRun() {
this.recycleBitmaps();
}
@Override
protected void doWork() {
if (checkSize()) {
checkRedraw();
}
}
public void afterRedraw() {
this.handler.sendEmptyMessage(0);
}
public void beforeRedraw() {
this.handler.sendEmptyMessage(1);
}
@Override
protected String getThreadName() {
return THREAD_NAME;
}
@Override
protected ThreadPriority getThreadPriority() {
return ThreadPriority.BELOW_NORMAL;
}
@Override
protected boolean hasWork() {
return this.changeSizeNeeded || this.redrawNeeded;
}
}