/*
* 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 org.mapsforge.android.maps.MapView;
import org.mapsforge.android.maps.Projection;
import org.mapsforge.core.model.GeoPoint;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Point;
/**
* Overlay is the abstract base class for all types of overlays. It handles the lifecycle of the overlay thread and
* implements those parts of the redrawing process which all overlays have in common.
* <p>
* To add an overlay to a <code>MapView</code>, create a subclass of this class and add an instance to the list returned
* by {@link MapView#getOverlays()}. When an overlay gets removed from the list, the corresponding thread is
* automatically interrupted and all its resources are freed. Re-adding a previously removed overlay to the list will
* therefore cause an {@link IllegalThreadStateException}.
*/
public abstract class Overlay extends Thread {
/**
* Enumeration of all types of events.
*/
protected enum EventType {
/**
* A long press event.
*
* @see Overlay#onLongPress(GeoPoint, MapView)
*/
LONG_PRESS,
/**
* A tap event.
*
* @see Overlay#onTap(GeoPoint, MapView)
*/
TAP;
}
private static final String THREAD_NAME = "Overlay";
/**
* Flag which is set whenever the MapView dimensions have been changed.
*/
private boolean changedSize;
/**
* Flag to indicate if the overlay has a positive width and height.
*/
private boolean hasValidDimensions;
/**
* Transformation matrix for the overlay.
*/
private final Matrix matrix;
/**
* Used to calculate the scale of the transformation matrix.
*/
private float matrixScaleFactor;
/**
* First internal bitmap for the overlay to draw on.
*/
private Bitmap overlayBitmap1;
/**
* Second internal bitmap for the overlay to draw on.
*/
private Bitmap overlayBitmap2;
/**
* Canvas that is used in the overlay for drawing.
*/
private final Canvas overlayCanvas;
/**
* Stores the top-left map position at which the redraw should happen.
*/
private final Point point;
/**
* Stores the map position before drawing starts.
*/
private Point positionAfterDraw;
/**
* Stores the map position after drawing is finished.
*/
private Point positionBeforeDraw;
/**
* Flag to indicate if the overlay should redraw itself.
*/
private boolean redraw;
/**
* Reference to the MapView instance.
*/
protected MapView internalMapView;
/**
* Default constructor which must be called by all subclasses.
*/
protected Overlay() {
super();
this.overlayCanvas = new Canvas();
this.matrix = new Matrix();
this.point = new Point();
this.positionBeforeDraw = new Point();
this.positionAfterDraw = new Point();
}
/**
* Draws the overlay on the given canvas.
*
* @param canvas
* the canvas on which the overlay should be drawn.
*/
public final void draw(Canvas canvas) {
synchronized (this.matrix) {
if (this.overlayBitmap1 != null) {
canvas.drawBitmap(this.overlayBitmap1, this.matrix, null);
}
}
}
/**
* @param scaleX
* the horizontal scale.
* @param scaleY
* the vertical scale.
* @param pivotX
* the horizontal pivot point.
* @param pivotY
* the vertical pivot point.
*/
public final void matrixPostScale(float scaleX, float scaleY, float pivotX, float pivotY) {
synchronized (this.matrix) {
this.matrix.postScale(scaleX, scaleY, pivotX, pivotY);
}
}
/**
* @param translateX
* the horizontal translation.
* @param translateY
* the vertical translation.
*/
public final void matrixPostTranslate(float translateX, float translateY) {
synchronized (this.matrix) {
this.matrix.postTranslate(translateX, translateY);
}
}
/**
* Handles a long press event. A long press event is only triggered if the map was not moved. A return value of true
* indicates that the long press event has been handled by this overlay and stops its propagation to other overlays.
* <p>
* The default implementation of this method does nothing and returns false.
*
* @param geoPoint
* the point which has been long pressed.
* @param mapView
* the {@link MapView} that triggered the long press event.
* @return true if the long press event was handled, false otherwise.
*/
public boolean onLongPress(GeoPoint geoPoint, MapView mapView) {
return false;
}
/**
* Marks the current dimensions of the overlay as dirty.
*/
public final void onSizeChanged() {
synchronized (this) {
this.changedSize = true;
notify();
}
}
/**
* Handles a tap event. A tap event is only triggered if the map was not moved and no long press event was handled
* within the same gesture. A return value of true indicates that the tap event has been handled by this overlay and
* stops its propagation to other overlays.
* <p>
* The default implementation of this method does nothing and returns false.
*
* @param geoPoint
* the point which has been tapped.
* @param mapView
* the {@link MapView} that triggered the tap event.
* @return true if the tap event was handled, false otherwise.
*/
public boolean onTap(GeoPoint geoPoint, MapView mapView) {
return false;
}
/**
* Requests a redraw of this overlay.
*/
public final void requestRedraw() {
synchronized (this) {
this.redraw = true;
notify();
}
}
@Override
public final void run() {
setName(getThreadName());
while (!isInterrupted()) {
synchronized (this) {
while (!isInterrupted() && !this.changedSize && !this.redraw) {
try {
wait();
} catch (InterruptedException e) {
// restore the interrupted status
interrupt();
}
}
}
if (isInterrupted()) {
break;
}
if (this.changedSize) {
changeSize();
}
if (this.redraw) {
redrawOverlay();
}
}
// help the GC
internalMapView = null;
// free the overlay bitmaps memory
if (this.overlayBitmap1 != null) {
this.overlayBitmap1.recycle();
this.overlayBitmap1 = null;
}
if (this.overlayBitmap2 != null) {
this.overlayBitmap2.recycle();
this.overlayBitmap2 = null;
}
}
/**
* This method is called by the MapView once on each new overlay.
*
* @param mapView
* the calling MapView.
*/
public final void setupOverlay(MapView mapView) {
if (isInterrupted() || !isAlive()) {
throw new IllegalThreadStateException("overlay thread already destroyed");
}
this.internalMapView = mapView;
onSizeChanged();
}
private void redrawOverlay() {
this.redraw = false;
if (!this.hasValidDimensions) {
// there is no area to draw on
return;
}
Projection mapViewProjection = this.internalMapView.getProjection();
// clear the second bitmap and make the canvas use it
this.overlayBitmap2.eraseColor(Color.TRANSPARENT);
this.overlayCanvas.setBitmap(this.overlayBitmap2);
// workaround for http://code.google.com/p/skia/issues/detail?id=387
this.overlayCanvas.setMatrix(this.overlayCanvas.getMatrix());
// save the zoom level and map position before drawing
byte zoomLevelBeforeDraw;
synchronized (this.internalMapView) {
zoomLevelBeforeDraw = this.internalMapView.getMapPosition().getZoomLevel();
this.positionBeforeDraw = mapViewProjection.toPoint(this.internalMapView.getMapPosition().getMapCenter(),
this.positionBeforeDraw, zoomLevelBeforeDraw);
}
// calculate the top-left point of the visible rectangle
this.point.x = this.positionBeforeDraw.x - (this.overlayCanvas.getWidth() >> 1);
this.point.y = this.positionBeforeDraw.y - (this.overlayCanvas.getHeight() >> 1);
if (isInterrupted() || sizeHasChanged() || needRedraw()) {
// stop working
return;
}
// call the draw implementation of the subclass
drawOverlayBitmap(this.overlayCanvas, this.point, mapViewProjection, zoomLevelBeforeDraw);
if (isInterrupted() || sizeHasChanged() || needRedraw()) {
// stop working
return;
}
// save the zoom level and map position after drawing
byte zoomLevelAfterDraw;
synchronized (this.internalMapView) {
zoomLevelAfterDraw = this.internalMapView.getMapPosition().getZoomLevel();
this.positionAfterDraw = mapViewProjection.toPoint(this.internalMapView.getMapPosition().getMapCenter(),
this.positionAfterDraw, zoomLevelBeforeDraw);
}
if (this.internalMapView.isZoomAnimatorRunning()) {
// do not disturb the ongoing animation
return;
}
// adjust the transformation matrix of the overlay
synchronized (this.matrix) {
this.matrix.reset();
this.matrix.postTranslate(this.positionBeforeDraw.x - this.positionAfterDraw.x, this.positionBeforeDraw.y
- this.positionAfterDraw.y);
byte zoomLevelDiff = (byte) (zoomLevelAfterDraw - zoomLevelBeforeDraw);
if (zoomLevelDiff > 0) {
// zoom level has increased
this.matrixScaleFactor = 1 << zoomLevelDiff;
this.matrix.postScale(this.matrixScaleFactor, this.matrixScaleFactor,
this.overlayCanvas.getWidth() >> 1, this.overlayCanvas.getHeight() >> 1);
} else if (zoomLevelDiff < 0) {
// zoom level has decreased
this.matrixScaleFactor = 1.0f / (1 << -zoomLevelDiff);
this.matrix.postScale(this.matrixScaleFactor, this.matrixScaleFactor,
this.overlayCanvas.getWidth() >> 1, this.overlayCanvas.getHeight() >> 1);
}
// swap the two overlay bitmaps
Bitmap overlayBitmapSwap = this.overlayBitmap1;
this.overlayBitmap1 = this.overlayBitmap2;
this.overlayBitmap2 = overlayBitmapSwap;
}
if (isInterrupted() || sizeHasChanged() || needRedraw()) {
// stop working
return;
}
// request the MapView to redraw
this.internalMapView.postInvalidate();
}
/**
* Draws the overlay on the canvas. All subclasses need to implement this method.
*
* @param canvas
* the canvas to draw the overlay on.
* @param drawPosition
* the top-left position of the map relative to the world map.
* @param projection
* the projection to be used for the drawing process.
* @param drawZoomLevel
* the zoom level of the map.
*/
protected abstract void drawOverlayBitmap(Canvas canvas, Point drawPosition, Projection projection,
byte drawZoomLevel);
/**
* Returns the name of the overlay implementation. It will be used as the name for the overlay thread. Subclasses
* should override this method to provide a more specific name.
*
* @return the name of the overlay implementation.
*/
protected String getThreadName() {
return THREAD_NAME;
}
/**
* Changes the size of the overlay according to the MapView dimensions.
*/
public final void changeSize() {
this.changedSize = false;
// check if the previous overlay bitmaps must be recycled
if (this.overlayBitmap1 != null) {
this.overlayBitmap1.recycle();
this.overlayBitmap1 = null;
}
if (this.overlayBitmap2 != null) {
this.overlayBitmap2.recycle();
this.overlayBitmap2 = null;
}
// check if the new dimensions are positive
int width = this.internalMapView.getWidth();
int height = this.internalMapView.getHeight();
// Log.i("OVERLAY", "new Bitmap size" +width + "/" + height);
if (width > 0 && height > 0) {
// create the two overlay bitmaps with the correct dimensions
this.overlayBitmap1 = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
this.overlayBitmap2 = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
this.redraw = true;
this.hasValidDimensions = true;
} else {
this.hasValidDimensions = false;
}
}
/**
* @return true if the dimensions of the overlay have changed, false otherwise.
*/
public boolean sizeHasChanged() {
return this.changedSize;
}
/**
* @return true if the overlay need to be redraw
*/
public boolean needRedraw() {
return this.redraw;
}
public void dispose() {
if (this.overlayBitmap1 != null) {
this.overlayBitmap1.recycle();
this.overlayBitmap1 = null;
}
if (this.overlayBitmap2 != null) {
this.overlayBitmap2.recycle();
this.overlayBitmap2 = null;
}
}
}