// Created by plusminus on 17:45:56 - 25.09.2008
package org.osmdroid.views;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import microsoft.mappoint.TileSystem;
import org.metalev.multitouch.controller.MultiTouchController;
import org.metalev.multitouch.controller.MultiTouchController.MultiTouchObjectCanvas;
import org.metalev.multitouch.controller.MultiTouchController.PointInfo;
import org.metalev.multitouch.controller.MultiTouchController.PositionAndScale;
import org.osmdroid.api.IGeoPoint;
import org.osmdroid.api.IMapController;
import org.osmdroid.api.IMapView;
import org.osmdroid.config.Configuration;
import org.osmdroid.events.MapListener;
import org.osmdroid.events.ScrollEvent;
import org.osmdroid.events.ZoomEvent;
import org.osmdroid.tileprovider.MapTileProviderArray;
import org.osmdroid.tileprovider.MapTileProviderBase;
import org.osmdroid.tileprovider.MapTileProviderBasic;
import org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants;
import org.osmdroid.tileprovider.modules.MapTileModuleProviderBase;
import org.osmdroid.tileprovider.tilesource.IStyledTileSource;
import org.osmdroid.tileprovider.tilesource.ITileSource;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.tileprovider.util.SimpleInvalidationHandler;
import org.osmdroid.util.BoundingBoxE6;
import org.osmdroid.util.BoundingBox;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.util.GeometryMath;
import org.osmdroid.views.overlay.DefaultOverlayManager;
import org.osmdroid.views.overlay.Overlay;
import org.osmdroid.views.overlay.OverlayManager;
import org.osmdroid.views.overlay.TilesOverlay;
import org.osmdroid.views.util.constants.MapViewConstants;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Build;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
import android.widget.ZoomButtonsController;
import android.widget.ZoomButtonsController.OnZoomListener;
/**
* This is the primary view for osmdroid
*
* @since the begining
*/
public class MapView extends ViewGroup implements IMapView, MapViewConstants,
MultiTouchObjectCanvas<Object> {
// ===========================================================
// Constants
// ===========================================================
private static final double ZOOM_SENSITIVITY = 1.0;
private static final double ZOOM_LOG_BASE_INV = 1.0 / Math.log(2.0 / ZOOM_SENSITIVITY);
private static Method sMotionEventTransformMethod;
// ===========================================================
// Fields
// ===========================================================
/** Current zoom level for map tiles. */
private int mZoomLevel = 0;
private OverlayManager mOverlayManager;
protected Projection mProjection;
private TilesOverlay mMapOverlay;
private final GestureDetector mGestureDetector;
/** Handles map scrolling */
private final Scroller mScroller;
protected boolean mIsFlinging;
protected final AtomicInteger mTargetZoomLevel = new AtomicInteger();
protected final AtomicBoolean mIsAnimating = new AtomicBoolean(false);
protected Integer mMinimumZoomLevel;
protected Integer mMaximumZoomLevel;
private final MapController mController;
private final ZoomButtonsController mZoomController;
private boolean mEnableZoomController = false;
private MultiTouchController<Object> mMultiTouchController;
protected float mMultiTouchScale = 1.0f;
protected PointF mMultiTouchScalePoint = new PointF();
protected MapListener mListener;
// For rotation
private float mapOrientation = 0;
private final Rect mInvalidateRect = new Rect();
protected BoundingBox mScrollableAreaBoundingBox;
protected Rect mScrollableAreaLimit;
private MapTileProviderBase mTileProvider;
private Handler mTileRequestCompleteHandler;
private boolean mTilesScaledToDpi = false;
final Matrix mRotateScaleMatrix = new Matrix();
final Point mRotateScalePoint = new Point();
/* a point that will be reused to lay out added views */
private final Point mLayoutPoint = new Point();
// Keep a set of listeners for when the maps have a layout
private final LinkedList<OnFirstLayoutListener> mOnFirstLayoutListeners = new LinkedList<MapView.OnFirstLayoutListener>();
/* becomes true once onLayout has been called for the first time i.e. map is ready to go. */
private boolean mLayoutOccurred = false;
public interface OnFirstLayoutListener {
/**
* this generally means that the map is ready to go
* @param v
* @param left
* @param top
* @param right
* @param bottom
*/
void onFirstLayout(View v, int left, int top, int right, int bottom);
}
// ===========================================================
// Constructors
// ===========================================================
public MapView(final Context context,
MapTileProviderBase tileProvider,
final Handler tileRequestCompleteHandler, final AttributeSet attrs) {
this(context, tileProvider, tileRequestCompleteHandler, attrs, Configuration.getInstance().isMapViewHardwareAccelerated());
}
public MapView(final Context context,
MapTileProviderBase tileProvider,
final Handler tileRequestCompleteHandler, final AttributeSet attrs, boolean hardwareAccelerated) {
super(context, attrs);
if(isInEditMode()){ //fix for edit mode in the IDE
mTileRequestCompleteHandler=null;
mController=null;
mZoomController=null;
mScroller=null;
mGestureDetector=null;
return;
}
if (!hardwareAccelerated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
this.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
this.mController = new MapController(this);
this.mScroller = new Scroller(context);
if (tileProvider == null) {
final ITileSource tileSource = getTileSourceFromAttributes(attrs);
tileProvider = isInEditMode()
? new MapTileProviderArray(tileSource, null, new MapTileModuleProviderBase[0])
: new MapTileProviderBasic(context.getApplicationContext(), tileSource);
}
mTileRequestCompleteHandler = tileRequestCompleteHandler == null
? new SimpleInvalidationHandler(this)
: tileRequestCompleteHandler;
mTileProvider = tileProvider;
mTileProvider.setTileRequestCompleteHandler(mTileRequestCompleteHandler);
updateTileSizeForDensity(mTileProvider.getTileSource());
this.mMapOverlay = new TilesOverlay(mTileProvider, context);
mOverlayManager = new DefaultOverlayManager(mMapOverlay);
if (isInEditMode()) {
mZoomController = null;
} else {
mZoomController = new ZoomButtonsController(this);
mZoomController.setOnZoomListener(new MapViewZoomListener());
}
mGestureDetector = new GestureDetector(context, new MapViewGestureDetectorListener());
mGestureDetector.setOnDoubleTapListener(new MapViewDoubleClickListener());
}
/**
* Constructor used by XML layout resource (uses default tile source).
*/
public MapView(final Context context, final AttributeSet attrs) {
this(context, null, null, attrs);
}
public MapView(final Context context) {
this(context, null, null, null);
}
public MapView(final Context context,
final MapTileProviderBase aTileProvider) {
this(context, aTileProvider, null);
}
public MapView(final Context context,
final MapTileProviderBase aTileProvider,
final Handler tileRequestCompleteHandler) {
this(context, aTileProvider, tileRequestCompleteHandler,
null);
}
// ===========================================================
// Getter & Setter
// ===========================================================
@Override
public IMapController getController() {
return this.mController;
}
/**
* You can add/remove/reorder your Overlays using the List of {@link Overlay}. The first (index
* 0) Overlay gets drawn first, the one with the highest as the last one.
*/
public List<Overlay> getOverlays() {
return this.getOverlayManager().overlays();
}
public OverlayManager getOverlayManager() {
return mOverlayManager;
}
public void setOverlayManager(final OverlayManager overlayManager) {
mOverlayManager = overlayManager;
}
public MapTileProviderBase getTileProvider() {
return mTileProvider;
}
public Scroller getScroller() {
return mScroller;
}
public Handler getTileRequestCompleteHandler() {
return mTileRequestCompleteHandler;
}
@Override
public int getLatitudeSpan() {
return this.getBoundingBoxE6().getLatitudeSpanE6();
}
public double getLatitudeSpanDouble() {
return this.getBoundingBox().getLatitudeSpan();
}
@Override
public int getLongitudeSpan() {
return this.getBoundingBoxE6().getLongitudeSpanE6();
}
public double getLongitudeSpanDouble() {
return this.getBoundingBox().getLongitudeSpan();
}
public BoundingBoxE6 getBoundingBoxE6() {
return getProjection().getBoundingBoxE6();
}
public BoundingBox getBoundingBox() {
return getProjection().getBoundingBox();
}
/**
* Gets the current bounds of the screen in <I>screen coordinates</I>.
*/
public Rect getScreenRect(final Rect reuse) {
final Rect out = getIntrinsicScreenRect(reuse);
if (this.getMapOrientation() != 0 && this.getMapOrientation() != 180) {
GeometryMath.getBoundingBoxForRotatatedRectangle(out, out.centerX(), out.centerY(),
this.getMapOrientation(), out);
}
return out;
}
public Rect getIntrinsicScreenRect(final Rect reuse) {
final Rect out = reuse == null ? new Rect() : reuse;
out.set(0, 0, getWidth(), getHeight());
return out;
}
/**
* Get a projection for converting between screen-pixel coordinates and latitude/longitude
* coordinates. You should not hold on to this object for more than one draw, since the
* projection of the map could change.
*
* @return The Projection of the map in its current state. You should not hold on to this object
* for more than one draw, since the projection of the map could change.
*/
@Override
public Projection getProjection() {
if (mProjection == null) {
mProjection = new Projection(this);
}
return mProjection;
}
protected void setProjection(Projection p){
mProjection = p;
}
void setMapCenter(final IGeoPoint aCenter) {
getController().animateTo(aCenter);
}
/**
* @deprecated use {@link #setMapCenter(IGeoPoint)}
*/
void setMapCenter(final int aLatitudeE6, final int aLongitudeE6) {
setMapCenter(new GeoPoint(aLatitudeE6, aLongitudeE6));
}
void setMapCenter(final double aLatitude, final double aLongitude) {
setMapCenter(new GeoPoint(aLatitude, aLongitude));
}
public boolean isTilesScaledToDpi() {
return mTilesScaledToDpi;
}
public void setTilesScaledToDpi(boolean tilesScaledToDpi) {
mTilesScaledToDpi = tilesScaledToDpi;
updateTileSizeForDensity(getTileProvider().getTileSource());
}
private void updateTileSizeForDensity(final ITileSource aTileSource) {
int tile_size = aTileSource.getTileSizePixels();
float density = getResources().getDisplayMetrics().density * 256 / tile_size ;
int size = (int) ( tile_size * (isTilesScaledToDpi() ? density : 1));
if (Configuration.getInstance().isDebugMapView())
Log.d(IMapView.LOGTAG, "Scaling tiles to " + size);
TileSystem.setTileSize(size);
}
public void setTileSource(final ITileSource aTileSource) {
mTileProvider.setTileSource(aTileSource);
updateTileSizeForDensity(aTileSource);
this.checkZoomButtons();
this.setZoomLevel(mZoomLevel); // revalidate zoom level
postInvalidate();
}
/**
* @param aZoomLevel
* the zoom level bound by the tile source
*/
int setZoomLevel(final int aZoomLevel) {
final int minZoomLevel = getMinZoomLevel();
final int maxZoomLevel = getMaxZoomLevel();
final int newZoomLevel = Math.max(minZoomLevel, Math.min(maxZoomLevel, aZoomLevel));
final int curZoomLevel = this.mZoomLevel;
if (newZoomLevel != curZoomLevel) {
if (mScroller!=null) //fix for edit mode in the IDE
mScroller.forceFinished(true);
mIsFlinging = false;
}
// Get our current center point
final IGeoPoint centerGeoPoint = getMapCenter();
this.mZoomLevel = newZoomLevel;
setProjection(null);
this.checkZoomButtons();
if (isLayoutOccurred()) {
getController().setCenter(centerGeoPoint);
// snap for all snappables
final Point snapPoint = new Point();
final Projection pj = getProjection();
if (this.getOverlayManager().onSnapToItem((int) mMultiTouchScalePoint.x,
(int) mMultiTouchScalePoint.y, snapPoint, this)) {
IGeoPoint geoPoint = pj.fromPixels(snapPoint.x, snapPoint.y, null);
getController().animateTo(geoPoint);
}
mTileProvider.rescaleCache(pj, newZoomLevel, curZoomLevel, getScreenRect(null));
pauseFling = true; // issue 269, pause fling during zoom changes
}
// do callback on listener
if (newZoomLevel != curZoomLevel && mListener != null) {
final ZoomEvent event = new ZoomEvent(this, newZoomLevel);
mListener.onZoom(event);
}
// Allows any views fixed to a Location in the MapView to adjust
this.requestLayout();
return this.mZoomLevel;
}
@Deprecated
public void zoomToBoundingBox(final BoundingBoxE6 boundingBox) {
BoundingBox box = new BoundingBox(boundingBox.getLatNorthE6()/1e6, boundingBox.getLonEastE6()/1e6, boundingBox.getLatSouthE6()/1e6, boundingBox.getLonWestE6()/1e6);
zoomToBoundingBox(box, false);
}
/**
* Zoom the map to enclose the specified bounding box, as closely as possible. Must be called
* after display layout is complete, or screen dimensions are not known, and will always zoom to
* center of zoom level 0.<br>
* Suggestion: Check getScreenRect(null).getHeight() > 0
*/
public void zoomToBoundingBox(final BoundingBox boundingBox, final boolean animated) {
final BoundingBox currentBox = getBoundingBox();
// Calculated required zoom based on latitude span
final double maxZoomLatitudeSpan = mZoomLevel == getMaxZoomLevel() ?
currentBox.getLatitudeSpan() :
currentBox.getLatitudeSpan() / Math.pow(2, getMaxZoomLevel() - mZoomLevel);
final double requiredLatitudeZoom =
getMaxZoomLevel() -
Math.ceil(Math.log(boundingBox.getLatitudeSpan() / maxZoomLatitudeSpan) / Math.log(2));
// Calculated required zoom based on longitude span
final double maxZoomLongitudeSpan = mZoomLevel == getMaxZoomLevel() ?
currentBox.getLongitudeSpan() :
currentBox.getLongitudeSpan() / Math.pow(2, getMaxZoomLevel() - mZoomLevel);
final double requiredLongitudeZoom =
getMaxZoomLevel() -
Math.ceil(Math.log(boundingBox.getLongitudeSpan() / maxZoomLongitudeSpan) / Math.log(2));
if (Configuration.getInstance().isDebugMode()){
Log.d(LOGTAG, "current bounds " +currentBox.toString());
Log.d(LOGTAG, "ZoomToBoundingBox calculations: " + maxZoomLatitudeSpan + ","+maxZoomLongitudeSpan + ","+requiredLatitudeZoom + ","+requiredLongitudeZoom );
}
// Zoom to boundingBox center, at calculated maximum allowed zoom level
if(animated) {
getController().zoomTo((int) (
requiredLatitudeZoom < requiredLongitudeZoom ?
requiredLatitudeZoom : requiredLongitudeZoom));
} else {
getController().setZoom((int) (
requiredLatitudeZoom < requiredLongitudeZoom ?
requiredLatitudeZoom : requiredLongitudeZoom));
}
getController().setCenter(
new GeoPoint(boundingBox.getCenter().getLatitude(), boundingBox.getCenter()
.getLongitude()));
}
/**
* Get the current ZoomLevel for the map tiles.
*
* @return the current ZoomLevel between 0 (equator) and 18/19(closest), depending on the tile
* source chosen.
*/
@Override
public int getZoomLevel() {
return getZoomLevel(true);
}
/**
* Get the current ZoomLevel for the map tiles.
*
* @param aPending
* if true and we're animating then return the zoom level that we're animating
* towards, otherwise return the current zoom level
* @return the zoom level
*/
public int getZoomLevel(final boolean aPending) {
if (aPending && isAnimating()) {
return mTargetZoomLevel.get();
} else {
return mZoomLevel;
}
}
/**
* Get the minimum allowed zoom level for the maps.
*/
public int getMinZoomLevel() {
return mMinimumZoomLevel == null ? mMapOverlay.getMinimumZoomLevel() : mMinimumZoomLevel;
}
/**
* Get the maximum allowed zoom level for the maps.
*/
@Override
public int getMaxZoomLevel() {
return mMaximumZoomLevel == null ? mMapOverlay.getMaximumZoomLevel() : mMaximumZoomLevel;
}
/**
* Set the minimum allowed zoom level, or pass null to use the minimum zoom level from the tile
* provider.
*/
public void setMinZoomLevel(Integer zoomLevel) {
mMinimumZoomLevel = zoomLevel;
}
/**
* Set the maximum allowed zoom level, or pass null to use the maximum zoom level from the tile
* provider.
*/
public void setMaxZoomLevel(Integer zoomLevel) {
mMaximumZoomLevel = zoomLevel;
}
public boolean canZoomIn() {
final int maxZoomLevel = getMaxZoomLevel();
if ((isAnimating() ? mTargetZoomLevel.get() : mZoomLevel) >= maxZoomLevel) {
return false;
}
return true;
}
public boolean canZoomOut() {
final int minZoomLevel = getMinZoomLevel();
if ((isAnimating() ? mTargetZoomLevel.get() : mZoomLevel) <= minZoomLevel) {
return false;
}
return true;
}
/**
* Zoom in by one zoom level.
*/
boolean zoomIn() {
return getController().zoomIn();
}
@Deprecated
boolean zoomInFixing(final IGeoPoint point) {
Point coords = getProjection().toPixels(point, null);
return getController().zoomInFixing(coords.x, coords.y);
}
@Deprecated
boolean zoomInFixing(final int xPixel, final int yPixel) {
return getController().zoomInFixing(xPixel, yPixel);
}
/**
* Zoom out by one zoom level.
*/
boolean zoomOut() {
return getController().zoomOut();
}
@Deprecated
boolean zoomOutFixing(final IGeoPoint point) {
Point coords = getProjection().toPixels(point, null);
return zoomOutFixing(coords.x, coords.y);
}
@Deprecated
boolean zoomOutFixing(final int xPixel, final int yPixel) {
return getController().zoomOutFixing(xPixel, yPixel);
}
/**
* Returns the current center-point position of the map, as a GeoPoint (latitude and longitude).
*
* @return A GeoPoint of the map's center-point.
*/
@Override
public IGeoPoint getMapCenter() {
return getProjection().fromPixels(getWidth() / 2, getHeight() / 2, null);
}
/**
* rotates the map to the desired heading
* @param degrees
*/
public void setMapOrientation(float degrees) {
mapOrientation = degrees % 360.0f;
// Request a layout, so that children are correctly positioned according to map orientation
requestLayout();
invalidate();
}
public float getMapOrientation() {
return mapOrientation;
}
/**
* Whether to use the network connection if it's available.
*/
public boolean useDataConnection() {
return mMapOverlay.useDataConnection();
}
/**
* Set whether to use the network connection if it's available.
*
* @param aMode
* if true use the network connection if it's available. if false don't use the
* network connection even if it's available.
*/
public void setUseDataConnection(final boolean aMode) {
mMapOverlay.setUseDataConnection(aMode);
}
/**
* Set the map to limit it's scrollable view to the specified BoundingBoxE6. Note this does not
* limit zooming so it will be possible for the user to zoom to an area that is larger than the
* limited area.
*
* @param boundingBox
* A lat/long bounding box to limit scrolling to, or null to remove any scrolling
* limitations
*/
@Deprecated
public void setScrollableAreaLimit(BoundingBoxE6 boundingBox) {
mScrollableAreaBoundingBox = new BoundingBox(boundingBox.getLatNorthE6()/1E6,
boundingBox.getLonEastE6()/1E6, boundingBox.getLatSouthE6()/1E6, boundingBox.getLonWestE6()/1E6);
// Clear scrollable area limit if null passed.
if (boundingBox == null) {
mScrollableAreaLimit = null;
return;
}
// Get NW/upper-left
final Point upperLeft = TileSystem.LatLongToPixelXY(boundingBox.getLatNorthE6() / 1E6,
boundingBox.getLonWestE6() / 1E6,
microsoft.mappoint.TileSystem.getMaximumZoomLevel(), null);
// Get SE/lower-right
final Point lowerRight = TileSystem.LatLongToPixelXY(boundingBox.getLatSouthE6() / 1E6,
boundingBox.getLonEastE6() / 1E6,
microsoft.mappoint.TileSystem.getMaximumZoomLevel(), null);
mScrollableAreaLimit = new Rect(upperLeft.x, upperLeft.y, lowerRight.x, lowerRight.y);
}
/**
* Set the map to limit it's scrollable view to the specified BoundingBox. Note this does not
* limit zooming so it will be possible for the user to zoom to an area that is larger than the
* limited area.
*
* @param boundingBox
* A lat/long bounding box to limit scrolling to, or null to remove any scrolling
* limitations
*/
public void setScrollableAreaLimitDouble(BoundingBox boundingBox) {
mScrollableAreaBoundingBox = boundingBox;
// Clear scrollable area limit if null passed.
if (boundingBox == null) {
mScrollableAreaLimit = null;
return;
}
// Get NW/upper-left
final Point upperLeft = TileSystem.LatLongToPixelXY(boundingBox.getLatNorth(),
boundingBox.getLonWest(), MapViewConstants.MAXIMUM_ZOOMLEVEL, null);
// Get SE/lower-right
final Point lowerRight = TileSystem.LatLongToPixelXY(boundingBox.getLatSouth(),
boundingBox.getLonEast(), MapViewConstants.MAXIMUM_ZOOMLEVEL, null);
mScrollableAreaLimit = new Rect(upperLeft.x, upperLeft.y, lowerRight.x, lowerRight.y);
}
public BoundingBox getScrollableAreaLimit() {
return mScrollableAreaBoundingBox;
}
public void invalidateMapCoordinates(Rect dirty) {
invalidateMapCoordinates(dirty.left, dirty.top, dirty.right, dirty.bottom, false);
}
public void invalidateMapCoordinates(int left, int top, int right, int bottom) {
invalidateMapCoordinates(left, top, right, bottom, false);
}
public void postInvalidateMapCoordinates(int left, int top, int right, int bottom) {
invalidateMapCoordinates(left, top, right, bottom, true);
}
private void invalidateMapCoordinates(int left, int top, int right, int bottom, boolean post) {
mInvalidateRect.set(left, top, right, bottom);
mInvalidateRect.offset(getScrollX(), getScrollY());
int centerX = this.getScrollX() + getWidth() / 2;
int centerY = this.getScrollY() + getHeight() / 2;
if (this.getMapOrientation() != 0)
GeometryMath.getBoundingBoxForRotatatedRectangle(mInvalidateRect, centerX, centerY,
this.getMapOrientation() + 180, mInvalidateRect);
if (post)
super.postInvalidate(mInvalidateRect.left, mInvalidateRect.top, mInvalidateRect.right,
mInvalidateRect.bottom);
else
super.invalidate(mInvalidateRect);
}
/**
* Returns a set of layout parameters with a width of
* {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}, a height of
* {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} at the {@link GeoPoint} (0, 0) align
* with {@link MapView.LayoutParams#BOTTOM_CENTER}.
*/
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new MapView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT, null, MapView.LayoutParams.BOTTOM_CENTER, 0, 0);
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(final AttributeSet attrs) {
return new MapView.LayoutParams(getContext(), attrs);
}
// Override to allow type-checking of LayoutParams.
@Override
protected boolean checkLayoutParams(final ViewGroup.LayoutParams p) {
return p instanceof MapView.LayoutParams;
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(final ViewGroup.LayoutParams p) {
return new MapView.LayoutParams(p);
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
// Get the children to measure themselves so we know their size in onLayout()
measureChildren(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(final boolean changed, final int l, final int t, final int r,
final int b) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final MapView.LayoutParams lp = (MapView.LayoutParams) child.getLayoutParams();
final int childHeight = child.getMeasuredHeight();
final int childWidth = child.getMeasuredWidth();
getProjection().toPixels(lp.geoPoint, mLayoutPoint);
// Apply rotation of mLayoutPoint around the center of the map
if (getMapOrientation() != 0) {
Point p = getProjection().rotateAndScalePoint(mLayoutPoint.x, mLayoutPoint.y,
null);
mLayoutPoint.x = p.x;
mLayoutPoint.y = p.y;
}
getProjection().toMercatorPixels(mLayoutPoint.x, mLayoutPoint.y, mLayoutPoint);
final int x = mLayoutPoint.x;
final int y = mLayoutPoint.y;
int childLeft = x;
int childTop = y;
switch (lp.alignment) {
case MapView.LayoutParams.TOP_LEFT:
childLeft = getPaddingLeft() + x;
childTop = getPaddingTop() + y;
break;
case MapView.LayoutParams.TOP_CENTER:
childLeft = getPaddingLeft() + x - childWidth / 2;
childTop = getPaddingTop() + y;
break;
case MapView.LayoutParams.TOP_RIGHT:
childLeft = getPaddingLeft() + x - childWidth;
childTop = getPaddingTop() + y;
break;
case MapView.LayoutParams.CENTER_LEFT:
childLeft = getPaddingLeft() + x;
childTop = getPaddingTop() + y - childHeight / 2;
break;
case MapView.LayoutParams.CENTER:
childLeft = getPaddingLeft() + x - childWidth / 2;
childTop = getPaddingTop() + y - childHeight / 2;
break;
case MapView.LayoutParams.CENTER_RIGHT:
childLeft = getPaddingLeft() + x - childWidth;
childTop = getPaddingTop() + y - childHeight / 2;
break;
case MapView.LayoutParams.BOTTOM_LEFT:
childLeft = getPaddingLeft() + x;
childTop = getPaddingTop() + y - childHeight;
break;
case MapView.LayoutParams.BOTTOM_CENTER:
childLeft = getPaddingLeft() + x - childWidth / 2;
childTop = getPaddingTop() + y - childHeight;
break;
case MapView.LayoutParams.BOTTOM_RIGHT:
childLeft = getPaddingLeft() + x - childWidth;
childTop = getPaddingTop() + y - childHeight;
break;
}
childLeft += lp.offsetX;
childTop += lp.offsetY;
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
}
}
if (!isLayoutOccurred()) {
mLayoutOccurred = true;
for (OnFirstLayoutListener listener : mOnFirstLayoutListeners)
listener.onFirstLayout(this, l, t, r, b);
mOnFirstLayoutListeners.clear();
}
setProjection(null);
}
public void addOnFirstLayoutListener(OnFirstLayoutListener listener) {
// Don't add if we already have a layout
if (!isLayoutOccurred())
mOnFirstLayoutListeners.add(listener);
}
public void removeOnFirstLayoutListener(OnFirstLayoutListener listener) {
mOnFirstLayoutListeners.remove(listener);
}
public boolean isLayoutOccurred() {
return mLayoutOccurred;
}
public void onDetach() {
this.getOverlayManager().onDetach(this);
mTileProvider.detach();
mTileProvider.clearTileCache();
mZoomController.setVisible(false);
//https://github.com/osmdroid/osmdroid/issues/390
if (mTileRequestCompleteHandler instanceof SimpleInvalidationHandler) {
((SimpleInvalidationHandler) mTileRequestCompleteHandler).destroy();
}
mTileRequestCompleteHandler=null;
if (mProjection!=null)
mProjection.detach();
mProjection=null;
}
@Override
public boolean onKeyDown(final int keyCode, final KeyEvent event) {
final boolean result = this.getOverlayManager().onKeyDown(keyCode, event, this);
return result || super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(final int keyCode, final KeyEvent event) {
final boolean result = this.getOverlayManager().onKeyUp(keyCode, event, this);
return result || super.onKeyUp(keyCode, event);
}
@Override
public boolean onTrackballEvent(final MotionEvent event) {
if (this.getOverlayManager().onTrackballEvent(event, this)) {
return true;
}
scrollBy((int) (event.getX() * 25), (int) (event.getY() * 25));
return super.onTrackballEvent(event);
}
@Override
public boolean dispatchTouchEvent(final MotionEvent event) {
if (Configuration.getInstance().isDebugMapView()) {
Log.d(IMapView.LOGTAG,"dispatchTouchEvent(" + event + ")");
}
if (mZoomController.isVisible() && mZoomController.onTouch(this, event)) {
return true;
}
// Get rotated event for some touch listeners.
MotionEvent rotatedEvent = rotateTouchEvent(event);
try {
if (super.dispatchTouchEvent(event)) {
if (Configuration.getInstance().isDebugMapView()) {
Log.d(IMapView.LOGTAG,"super handled onTouchEvent");
}
return true;
}
if (this.getOverlayManager().onTouchEvent(rotatedEvent, this)) {
return true;
}
boolean handled = false;
if (mMultiTouchController != null && mMultiTouchController.onTouchEvent(event)) {
if (Configuration.getInstance().isDebugMapView()) {
Log.d(IMapView.LOGTAG,"mMultiTouchController handled onTouchEvent");
}
handled = true;
}
if (mGestureDetector.onTouchEvent(rotatedEvent)) {
if (Configuration.getInstance().isDebugMapView()) {
Log.d(IMapView.LOGTAG,"mGestureDetector handled onTouchEvent");
}
handled = true;
}
if (handled)
return true;
} finally {
if (rotatedEvent != event)
rotatedEvent.recycle();
}
if (Configuration.getInstance().isDebugMapView()) {
Log.d(IMapView.LOGTAG,"no-one handled onTouchEvent");
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return false;
}
private MotionEvent rotateTouchEvent(MotionEvent ev) {
if (this.getMapOrientation() == 0)
return ev;
MotionEvent rotatedEvent = MotionEvent.obtain(ev);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
getProjection().unrotateAndScalePoint((int) ev.getX(), (int) ev.getY(),
mRotateScalePoint);
rotatedEvent.setLocation(mRotateScalePoint.x, mRotateScalePoint.y);
} else {
// This method is preferred since it will rotate historical touch events too
try {
if (sMotionEventTransformMethod == null) {
sMotionEventTransformMethod = MotionEvent.class.getDeclaredMethod("transform",
new Class[] { Matrix.class });
}
sMotionEventTransformMethod.invoke(rotatedEvent, getProjection()
.getInvertedScaleRotateCanvasMatrix());
} catch (SecurityException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
return rotatedEvent;
}
@Override
public void computeScroll() {
if (mScroller!=null) //fix for edit mode in the IDE
if (mScroller.computeScrollOffset()) {
if (mScroller.isFinished()) {
// One last scrollTo to get to the final destination
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
// This will facilitate snapping-to any Snappable points.
setZoomLevel(mZoomLevel);
mIsFlinging = false;
} else {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
}
// Keep on drawing until the animation has finished.
postInvalidate();
}
}
@Override
public void scrollTo(int x, int y) {
final int worldSize = TileSystem.MapSize(this.getZoomLevel(false));
while (x < 0) {
x += worldSize;
}
while (x >= worldSize) {
x -= worldSize;
}
while (y < 0) {
y += worldSize;
}
while (y >= worldSize) {
y -= worldSize;
}
if (mScrollableAreaLimit != null) {
final int zoomDiff = microsoft.mappoint.TileSystem.getMaximumZoomLevel()
- getZoomLevel(false);
final int minX = (mScrollableAreaLimit.left >> zoomDiff);
final int minY = (mScrollableAreaLimit.top >> zoomDiff);
final int maxX = (mScrollableAreaLimit.right >> zoomDiff);
final int maxY = (mScrollableAreaLimit.bottom >> zoomDiff);
final int scrollableWidth = maxX - minX;
final int scrollableHeight = maxY - minY;
final int width = this.getWidth();
final int height = this.getHeight();
// Adjust if we are outside the scrollable area
if (scrollableWidth <= width) {
if (x > minX)
x = minX;
else if (x + width < maxX)
x = maxX - width;
} else if (x < minX)
x = minX;
else if (x + width > maxX)
x = maxX - width;
if (scrollableHeight <= height) {
if (y > minY)
y = minY;
else if (y + height < maxY)
y = maxY - height;
} else if (y - (0) < minY)
y = minY + (0);
else if (y + (height) > maxY)
y = maxY - (height);
}
super.scrollTo(x, y);
setProjection(null);
// Force a layout, so that children are correctly positioned according to map orientation
if (getMapOrientation() != 0f)
onLayout(true, getLeft(), getTop(), getRight(), getBottom());
// do callback on listener
if (mListener != null) {
final ScrollEvent event = new ScrollEvent(this, x, y);
mListener.onScroll(event);
}
}
@Override
public void setBackgroundColor(final int pColor) {
mMapOverlay.setLoadingBackgroundColor(pColor);
invalidate();
}
@Override
protected void dispatchDraw(final Canvas c) {
final long startMs = System.currentTimeMillis();
// Save the current canvas matrix
c.save();
//calculate previous angle
float previousAngle=0f;
mRotateScaleMatrix.reset();
// Make the upper-left corner 0,0
c.translate(getScrollX(), getScrollY());
// Scale the canvas
mRotateScaleMatrix.preScale(mMultiTouchScale, mMultiTouchScale,
mMultiTouchScalePoint.x, mMultiTouchScalePoint.y);
// Rotate the canvas
mRotateScaleMatrix.preRotate(mapOrientation, getWidth() / 2, getHeight() / 2);
// Apply the scale and rotate operations
c.concat(mRotateScaleMatrix);
// Reset the projection
setProjection(null);
/* Draw background */
// c.drawColor(mBackgroundColor);
try {
/* Draw all Overlays. */
this.getOverlayManager().onDraw(c, this);
// Restore the canvas matrix
c.restore();
super.dispatchDraw(c);
}catch (Exception ex){
//for edit mode
Log.e(IMapView.LOGTAG, "error dispatchDraw, probably in edit mode", ex);
}
if (Configuration.getInstance().isDebugMapView()) {
final long endMs = System.currentTimeMillis();
Log.d(IMapView.LOGTAG,"Rendering overall: " + (endMs - startMs) + "ms");
}
}
@Override
protected void onDetachedFromWindow() {
this.mZoomController.setVisible(false);
this.onDetach();
super.onDetachedFromWindow();
}
// ===========================================================
// Animation
// ===========================================================
/**
* Determines if maps are animating a zoom operation. Useful for overlays to avoid recalculating
* during an animation sequence.
*
* @return boolean indicating whether view is animating.
*/
public boolean isAnimating() {
return mIsAnimating.get();
}
// ===========================================================
// Implementation of MultiTouchObjectCanvas
// ===========================================================
@Override
public Object getDraggableObjectAtPoint(final PointInfo pt) {
if (this.isAnimating()) {
// Zoom animations use the mMultiTouchScale variables to perform their animations so we
// don't want to step on that.
return null;
} else {
mMultiTouchScalePoint.x = pt.getX();
mMultiTouchScalePoint.y = pt.getY();
return this;
}
}
@Override
public void getPositionAndScale(final Object obj, final PositionAndScale objPosAndScaleOut) {
objPosAndScaleOut.set(0, 0, true, mMultiTouchScale, false, 0, 0, false, 0);
}
@Override
public void selectObject(final Object obj, final PointInfo pt) {
// if obj is null it means we released the pointers
// if scale is not 1 it means we pinched
if (obj == null && mMultiTouchScale != 1.0f) {
final float scaleDiffFloat = (float) (Math.log(mMultiTouchScale) * ZOOM_LOG_BASE_INV);
final int scaleDiffInt = Math.round(scaleDiffFloat);
// If we are changing zoom levels,
// adjust the center point in respect to the scaling point
if (scaleDiffInt != 0) {
final Rect screenRect = getProjection().getScreenRect();
getProjection().unrotateAndScalePoint(screenRect.centerX(), screenRect.centerY(),
mRotateScalePoint);
Point p = getProjection().toMercatorPixels(mRotateScalePoint.x,
mRotateScalePoint.y, null);
scrollTo(p.x - getWidth() / 2, p.y - getHeight() / 2);
}
// Adjust the zoomLevel
setZoomLevel(mZoomLevel + scaleDiffInt);
}
// reset scale
mMultiTouchScale = 1.0f;
}
@Override
public boolean setPositionAndScale(final Object obj, final PositionAndScale aNewObjPosAndScale,
final PointInfo aTouchPoint) {
float multiTouchScale = aNewObjPosAndScale.getScale();
// If we are at the first or last zoom level, prevent pinching/expanding
if (multiTouchScale > 1 && !canZoomIn()) {
multiTouchScale = 1;
}
if (multiTouchScale < 1 && !canZoomOut()) {
multiTouchScale = 1;
}
mMultiTouchScale = multiTouchScale;
// Request a layout, so that children are correctly positioned according to scale
requestLayout();
invalidate(); // redraw
return true;
}
/*
* Set the MapListener for this view
*/
public void setMapListener(final MapListener ml) {
mListener = ml;
}
// ===========================================================
// Methods
// ===========================================================
private void checkZoomButtons() {
this.mZoomController.setZoomInEnabled(canZoomIn());
this.mZoomController.setZoomOutEnabled(canZoomOut());
}
public void setBuiltInZoomControls(final boolean on) {
this.mEnableZoomController = on;
this.checkZoomButtons();
}
public void setMultiTouchControls(final boolean on) {
mMultiTouchController = on ? new MultiTouchController<Object>(this, false) : null;
}
private ITileSource getTileSourceFromAttributes(final AttributeSet aAttributeSet) {
ITileSource tileSource = TileSourceFactory.DEFAULT_TILE_SOURCE;
if (aAttributeSet != null) {
final String tileSourceAttr = aAttributeSet.getAttributeValue(null, "tilesource");
if (tileSourceAttr != null) {
try {
final ITileSource r = TileSourceFactory.getTileSource(tileSourceAttr);
Log.i(IMapView.LOGTAG,"Using tile source specified in layout attributes: " + r);
tileSource = r;
} catch (final IllegalArgumentException e) {
Log.w(IMapView.LOGTAG,"Invalid tile source specified in layout attributes: " + tileSource);
}
}
}
if (aAttributeSet != null && tileSource instanceof IStyledTileSource) {
final String style = aAttributeSet.getAttributeValue(null, "style");
if (style == null) {
Log.i(IMapView.LOGTAG,"Using default style: 1");
} else {
Log.i(IMapView.LOGTAG,"Using style specified in layout attributes: " + style);
((IStyledTileSource<?>) tileSource).setStyle(style);
}
}
Log.i(IMapView.LOGTAG,"Using tile source: " + tileSource.name());
return tileSource;
}
private boolean enableFling = true;
private boolean pauseFling = false; // issue 269, boolean used for disabling fling during zoom changes
public void setFlingEnabled(final boolean b){
enableFling = b;
}
public boolean isFlingEnabled(){
return enableFling;
}
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
private class MapViewGestureDetectorListener implements OnGestureListener {
@Override
public boolean onDown(final MotionEvent e) {
// Stop scrolling if we are in the middle of a fling!
if (mIsFlinging) {
if (mScroller!=null) //fix for edit mode in the IDE
mScroller.abortAnimation();
mIsFlinging = false;
}
if (MapView.this.getOverlayManager().onDown(e, MapView.this)) {
return true;
}
mZoomController.setVisible(mEnableZoomController);
return true;
}
@Override
public boolean onFling(final MotionEvent e1, final MotionEvent e2,
final float velocityX, final float velocityY) {
if (!enableFling || pauseFling) {
// issue 269, if fling occurs during zoom changes, pauseFling is equals to true, so fling is canceled. But need to reactivate fling for next time.
pauseFling = false;
return false;
}
if (MapView.this.getOverlayManager()
.onFling(e1, e2, velocityX, velocityY, MapView.this)) {
return true;
}
final int worldSize = TileSystem.MapSize(MapView.this.getZoomLevel(false));
mIsFlinging = true;
if (mScroller!=null) //fix for edit mode in the IDE
mScroller.fling(getScrollX(), getScrollY(), (int) -velocityX, (int) -velocityY,
-worldSize, worldSize, -worldSize, worldSize);
return true;
}
@Override
public void onLongPress(final MotionEvent e) {
if (mMultiTouchController != null && mMultiTouchController.isPinching()) {
return;
}
MapView.this.getOverlayManager().onLongPress(e, MapView.this);
}
@Override
public boolean onScroll(final MotionEvent e1, final MotionEvent e2, final float distanceX,
final float distanceY) {
if (MapView.this.getOverlayManager().onScroll(e1, e2, distanceX, distanceY,
MapView.this)) {
return true;
}
scrollBy((int) distanceX, (int) distanceY);
return true;
}
@Override
public void onShowPress(final MotionEvent e) {
MapView.this.getOverlayManager().onShowPress(e, MapView.this);
}
@Override
public boolean onSingleTapUp(final MotionEvent e) {
if (MapView.this.getOverlayManager().onSingleTapUp(e, MapView.this)) {
return true;
}
return false;
}
}
private class MapViewDoubleClickListener implements GestureDetector.OnDoubleTapListener {
@Override
public boolean onDoubleTap(final MotionEvent e) {
if (MapView.this.getOverlayManager().onDoubleTap(e, MapView.this)) {
return true;
}
// final IGeoPoint center = getProjection().fromPixels((int) e.getX(), (int) e.getY(),
// null);
getProjection().rotateAndScalePoint((int) e.getX(), (int) e.getY(), mRotateScalePoint);
return zoomInFixing(mRotateScalePoint.x, mRotateScalePoint.y);
}
@Override
public boolean onDoubleTapEvent(final MotionEvent e) {
if (MapView.this.getOverlayManager().onDoubleTapEvent(e, MapView.this)) {
return true;
}
return false;
}
@Override
public boolean onSingleTapConfirmed(final MotionEvent e) {
if (MapView.this.getOverlayManager().onSingleTapConfirmed(e, MapView.this)) {
return true;
}
return false;
}
}
private class MapViewZoomListener implements OnZoomListener {
@Override
public void onZoom(final boolean zoomIn) {
if (zoomIn) {
getController().zoomIn();
} else {
getController().zoomOut();
}
}
@Override
public void onVisibilityChanged(final boolean visible) {
}
}
// ===========================================================
// Public Classes
// ===========================================================
/**
* Per-child layout information associated with OpenStreetMapView.
*/
public static class LayoutParams extends ViewGroup.LayoutParams {
/**
* Special value for the alignment requested by a View. TOP_LEFT means that the location
* will at the top left the View.
*/
public static final int TOP_LEFT = 1;
/**
* Special value for the alignment requested by a View. TOP_RIGHT means that the location
* will be centered at the top of the View.
*/
public static final int TOP_CENTER = 2;
/**
* Special value for the alignment requested by a View. TOP_RIGHT means that the location
* will at the top right the View.
*/
public static final int TOP_RIGHT = 3;
/**
* Special value for the alignment requested by a View. CENTER_LEFT means that the location
* will at the center left the View.
*/
public static final int CENTER_LEFT = 4;
/**
* Special value for the alignment requested by a View. CENTER means that the location will
* be centered at the center of the View.
*/
public static final int CENTER = 5;
/**
* Special value for the alignment requested by a View. CENTER_RIGHT means that the location
* will at the center right the View.
*/
public static final int CENTER_RIGHT = 6;
/**
* Special value for the alignment requested by a View. BOTTOM_LEFT means that the location
* will be at the bottom left of the View.
*/
public static final int BOTTOM_LEFT = 7;
/**
* Special value for the alignment requested by a View. BOTTOM_CENTER means that the
* location will be centered at the bottom of the view.
*/
public static final int BOTTOM_CENTER = 8;
/**
* Special value for the alignment requested by a View. BOTTOM_RIGHT means that the location
* will be at the bottom right of the View.
*/
public static final int BOTTOM_RIGHT = 9;
/**
* The location of the child within the map view.
*/
public IGeoPoint geoPoint;
/**
* The alignment the alignment of the view compared to the location.
*/
public int alignment;
public int offsetX;
public int offsetY;
/**
* Creates a new set of layout parameters with the specified width, height and location.
*
* @param width
* the width, either {@link #FILL_PARENT}, {@link #WRAP_CONTENT} or a fixed size
* in pixels
* @param height
* the height, either {@link #FILL_PARENT}, {@link #WRAP_CONTENT} or a fixed size
* in pixels
* @param geoPoint
* the location of the child within the map view
* @param alignment
* the alignment of the view compared to the location {@link #BOTTOM_CENTER},
* {@link #BOTTOM_LEFT}, {@link #BOTTOM_RIGHT} {@link #TOP_CENTER},
* {@link #TOP_LEFT}, {@link #TOP_RIGHT}
* @param offsetX
* the additional X offset from the alignment location to draw the child within
* the map view
* @param offsetY
* the additional Y offset from the alignment location to draw the child within
* the map view
*/
public LayoutParams(final int width, final int height, final IGeoPoint geoPoint,
final int alignment, final int offsetX, final int offsetY) {
super(width, height);
if (geoPoint != null) {
this.geoPoint = geoPoint;
} else {
this.geoPoint = new GeoPoint(0, 0);
}
this.alignment = alignment;
this.offsetX = offsetX;
this.offsetY = offsetY;
}
/**
* Since we cannot use XML files in this project this constructor is useless. Creates a new
* set of layout parameters. The values are extracted from the supplied attributes set and
* context.
*
* @param c
* the application environment
* @param attrs
* the set of attributes fom which to extract the layout parameters values
*/
public LayoutParams(final Context c, final AttributeSet attrs) {
super(c, attrs);
this.geoPoint = new GeoPoint(0, 0);
this.alignment = BOTTOM_CENTER;
}
public LayoutParams(final ViewGroup.LayoutParams source) {
super(source);
}
}
/**
* enables you to programmatically set the tile provider (zip, assets, sqlite, etc)
* @since 4.4
* @param base
* @see MapTileProviderBasic
*/
public void setTileProvider(final MapTileProviderBase base){
this.mTileProvider.detach();
mTileProvider.clearTileCache();
this.mTileProvider=base;
mTileProvider.setTileRequestCompleteHandler(mTileRequestCompleteHandler);
updateTileSizeForDensity(mTileProvider.getTileSource());
this.mMapOverlay = new TilesOverlay(mTileProvider, this.getContext());
mOverlayManager.setTilesOverlay(mMapOverlay);
invalidate();
}
}