All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mapbox.mapboxsdk.views.MapView Maven / Gradle / Ivy

There is a newer version: 9.2.1
Show newest version
package com.mapbox.mapboxsdk.views;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.os.Build;
import android.os.Handler;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
import com.almeros.android.multitouch.RotateGestureDetector;
import com.cocoahero.android.geojson.FeatureCollection;
import com.mapbox.mapboxsdk.R;
import com.mapbox.mapboxsdk.api.ILatLng;
import com.mapbox.mapboxsdk.constants.MapboxConstants;
import com.mapbox.mapboxsdk.events.MapListener;
import com.mapbox.mapboxsdk.events.ScrollEvent;
import com.mapbox.mapboxsdk.events.ZoomEvent;
import com.mapbox.mapboxsdk.geometry.BoundingBox;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.overlay.GeoJSONPainter;
import com.mapbox.mapboxsdk.overlay.GpsLocationProvider;
import com.mapbox.mapboxsdk.overlay.ItemizedIconOverlay;
import com.mapbox.mapboxsdk.overlay.ItemizedOverlay;
import com.mapbox.mapboxsdk.overlay.MapEventsOverlay;
import com.mapbox.mapboxsdk.overlay.MapEventsReceiver;
import com.mapbox.mapboxsdk.overlay.Marker;
import com.mapbox.mapboxsdk.overlay.Overlay;
import com.mapbox.mapboxsdk.overlay.OverlayManager;
import com.mapbox.mapboxsdk.overlay.TilesOverlay;
import com.mapbox.mapboxsdk.overlay.UserLocationOverlay;
import com.mapbox.mapboxsdk.tileprovider.MapTileLayerBase;
import com.mapbox.mapboxsdk.tileprovider.MapTileLayerBasic;
import com.mapbox.mapboxsdk.tileprovider.constants.TileLayerConstants;
import com.mapbox.mapboxsdk.tileprovider.tilesource.ITileLayer;
import com.mapbox.mapboxsdk.tileprovider.tilesource.MapboxTileLayer;
import com.mapbox.mapboxsdk.tileprovider.util.SimpleInvalidationHandler;
import com.mapbox.mapboxsdk.util.BitmapUtils;
import com.mapbox.mapboxsdk.util.DataLoadingUtils;
import com.mapbox.mapboxsdk.util.GeometryMath;
import com.mapbox.mapboxsdk.util.MapboxUtils;
import com.mapbox.mapboxsdk.util.NetworkUtils;
import com.mapbox.mapboxsdk.util.constants.UtilConstants;
import com.mapbox.mapboxsdk.views.util.OnMapOrientationChangeListener;
import com.mapbox.mapboxsdk.views.util.Projection;
import com.mapbox.mapboxsdk.views.util.TileLoadedListener;
import com.mapbox.mapboxsdk.views.util.TilesLoadedListener;
import com.mapbox.mapboxsdk.views.util.constants.MapViewConstants;
import com.mapbox.mapboxsdk.views.util.constants.MapViewLayouts;
import org.json.JSONException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * The MapView class manages all of the content and
 * state of a single map, including layers, markers,
 * and interaction code.
 */
public class MapView extends ViewGroup implements MapViewConstants, MapEventsReceiver, MapboxConstants {
    /**
     * The default marker Overlay, automatically added to the view to add markers directly.
     */
    private ItemizedIconOverlay defaultMarkerOverlay;
    /**
     * List linked to the default marker overlay.
     */
    private ArrayList defaultMarkerList = new ArrayList();
    /**
     * Overlay for basic map touch events.
     */
    private MapEventsOverlay eventsOverlay;
    /**
     * A copy of the app context.
     */
    private Context context;
    /**
     * Whether or not a marker has been placed already.
     */
    private boolean firstMarker = true;

    private static final String TAG = "MapBox MapView";
    private static Method sMotionEventTransformMethod;

    /**
     * Current zoom level for map tiles.
     */
    private float mZoomLevel = 11;
    protected float mRequestedMinimumZoomLevel = 0;
    private float mMinimumZoomLevel = 0;
    private float mMaximumZoomLevel = 22;

    /**
     * The MapView listener
     */
    private MapViewListener mMapViewListener;

    private final OverlayManager mOverlayManager;

    private Projection mProjection;
    private boolean mLayedOut;

    private final TilesOverlay mTilesOverlay;

    private final GestureDetector mGestureDetector;

    /**
     * Handles map scrolling
     */
    protected final Scroller mScroller;
    protected boolean mIsFlinging;

    private final AtomicInteger mTargetZoomLevel = new AtomicInteger();
    private final AtomicBoolean mIsAnimating = new AtomicBoolean(false);

    private final MapController mController;

    protected ScaleGestureDetector mScaleGestureDetector;
    protected RotateGestureDetector mRotateGestureDetector;
    protected boolean mMapRotationEnabled;
    protected OnMapOrientationChangeListener mOnMapOrientationChangeListener;

    protected float mMultiTouchScale = 1.0f;
    protected PointF mMultiTouchScalePoint = new PointF();
    protected Matrix mInvTransformMatrix = new Matrix();

    protected List mListeners = new ArrayList();

    private float mapOrientation = 0;
    private final float[] mRotatePoints = new float[2];
    private final Rect mInvalidateRect = new Rect();

    protected BoundingBox mScrollableAreaBoundingBox = null;
    protected RectF mScrollableAreaLimit = null;
    private boolean mConstraintRegionFit;
    protected RectF mTempRect = new RectF();

    private BoundingBox mBoundingBoxToZoomOn = null;
    private boolean mBoundingBoxToZoomOnRegionFit = false;

    // for speed (avoiding allocations)
    protected final MapTileLayerBase mTileProvider;

    private final Handler mTileRequestCompleteHandler;

    /* a point that will be reused to design added views */
    private final PointF mPoint = new PointF();

    private TilesLoadedListener tilesLoadedListener;
    TileLoadedListener tileLoadedListener;
    private InfoWindow currentTooltip;

    private int mDefaultPinRes = R.drawable.defpin;
    private Drawable mDefaultPinDrawable;
    private PointF mDefaultPinAnchor = DEFAULT_PIN_ANCHOR;

    private UserLocationOverlay mLocationOverlay;

    /**
     * Constructor for XML layout calls. Should not be used programmatically.
     *
     * @param aContext A copy of the app context
     * @param attrs    An AttributeSet object to get extra info from the XML, such as mapbox id or
     *                 type
     *                 of baselayer
     */
    protected MapView(final Context aContext, final int tileSizePixels,
                      MapTileLayerBase tileProvider, final Handler tileRequestCompleteHandler,
                      final AttributeSet attrs) {
        super(aContext, attrs);
        setWillNotDraw(false);
        mLayedOut = false;
        mConstraintRegionFit = false;
        this.mController = new MapController(this);
        this.mScroller = new Scroller(aContext);
        Projection.setTileSize(tileSizePixels);

        if (tileProvider == null) {
            tileProvider = new MapTileLayerBasic(aContext, null, this);
        }

        mTileRequestCompleteHandler =
                tileRequestCompleteHandler == null ? new SimpleInvalidationHandler(this)
                        : tileRequestCompleteHandler;
        mTileProvider = tileProvider;
        mTileProvider.setTileRequestCompleteHandler(mTileRequestCompleteHandler);

        mTilesOverlay = new TilesOverlay(mTileProvider);
        mOverlayManager = new OverlayManager(mTilesOverlay);

        this.mGestureDetector =
                new GestureDetector(aContext, new MapViewGestureDetectorListener(this));

        this.mScaleGestureDetector =
                new ScaleGestureDetector(aContext, new MapViewScaleGestureDetectorListener(this));
        this.mRotateGestureDetector =
                new RotateGestureDetector(aContext, new MapViewRotateGestureDetectorListener(this));
        this.context = aContext;
        eventsOverlay = new MapEventsOverlay(aContext, this);
        this.getOverlays().add(eventsOverlay);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MapView);
        String mapid = a.getString(R.styleable.MapView_mapid);
        MapboxUtils.setAccessToken(a.getString(R.styleable.MapView_accessToken));
        if (!TextUtils.isEmpty(mapid)) {
            setTileSource(new MapboxTileLayer(mapid));
        } else {
            Log.w(TAG, "mapid not set.");
        }
        String centerLat = a.getString(R.styleable.MapView_centerLat);
        String centerLng = a.getString(R.styleable.MapView_centerLng);
        if (centerLat != null && centerLng != null) {
            double lat, lng;
            lat = Double.parseDouble(centerLat);
            lng = Double.parseDouble(centerLng);
            this.setCenter(new LatLng(lat, lng));
        } else {
            Log.d(TAG, "centerLatLng is not specified in XML.");
        }
        String zoomLvl = a.getString(R.styleable.MapView_zoomLevel);
        if (zoomLvl != null) {
            float lvl = Float.parseFloat(zoomLvl);
            this.setZoom(lvl);
        } else {
            Log.d(TAG, "zoomLevel is not specified in XML.");
        }
        a.recycle();
    }

    public MapView(final Context aContext) {
        this(aContext, 256, null, null, null);
    }

    public MapView(final Context aContext, AttributeSet attrs) {
        this(aContext, 256, null, null, attrs);
    }

    protected MapView(Context aContext, int tileSizePixels, MapTileLayerBase aTileProvider) {
        this(aContext, tileSizePixels, aTileProvider, null, null);
    }

    /**
     * Add a new MapListener that observes changes in this map.
     * @param listener
     */
    public void addListener(final MapListener listener) {
        if (!mListeners.contains(listener)) {
            mListeners.add(listener);
        }
    }

    /**
     * Remove a listener object that observed changes in this map.
     * @param listener
     */
    public void removeListener(MapListener listener) {
        if (mListeners.contains(listener)) {
            mListeners.remove(listener);
        }
    }

    /**
     * Add an overlay to this map. If the overlay is already included,
     * does nothing. After adding the overlay, invalidates the map to
     * redraw it.
     * @param overlay
     */
    public void addOverlay(final Overlay overlay) {
        if (!mOverlayManager.contains(overlay)) {
            mOverlayManager.add(overlay);
            if (overlay instanceof MapListener) {
                addListener((MapListener) overlay);
            }
        }
        invalidate();
    }

    /**
     * Remove an overlay from displaying in this map and invalidates
     * the map to trigger a redraw.
     * @param overlay
     */
    public void removeOverlay(final Overlay overlay) {
        if (mOverlayManager.contains(overlay)) {
            mOverlayManager.remove(overlay);
            if (overlay instanceof MapListener) {
                removeListener((MapListener) overlay);
            }
        }
        invalidate();
    }

    private void updateAfterSourceChange() {
        Projection.setTileSize(mTileProvider.getTileSizePixels());
        this.setScrollableAreaLimit(mTileProvider.getBoundingBox());
        this.setMinZoomLevel(mTileProvider.getMinimumZoomLevel());
        this.setMaxZoomLevel(mTileProvider.getMaximumZoomLevel());
        this.setZoom(mZoomLevel);
        if (!isLayedOut()) {
            return;
        }
        this.scrollTo(mDScroll.x, mDScroll.y);
        postInvalidate();
    }

    /**
     * Set the tile source of this map as an array of tile layers,
     * which will be presented on top of each other.
     * @param value
     */
    public void setTileSource(final ITileLayer[] value) {
        if (value != null && mTileProvider != null && mTileProvider instanceof MapTileLayerBasic) {
            ((MapTileLayerBasic) mTileProvider).setTileSources(value);
            updateAfterSourceChange();
        }
    }

    /**
     * Set the tile source of this map as a single source, and trigger
     * an update.
     * @param aTileSource
     */
    public void setTileSource(final ITileLayer aTileSource) {
        if (aTileSource != null && mTileProvider != null && mTileProvider instanceof MapTileLayerBasic) {
            mTileProvider.setTileSource(aTileSource);
            updateAfterSourceChange();
        }
    }

    public void addTileSource(final ITileLayer aTileSource) {
        if (aTileSource != null && mTileProvider != null && mTileProvider instanceof MapTileLayerBasic) {
            ((MapTileLayerBasic) mTileProvider).addTileSource(aTileSource);
            updateAfterSourceChange();
        }
    }

    public void addTileSource(final ITileLayer aTileSource, final int index) {
        if (aTileSource != null && mTileProvider != null && mTileProvider instanceof MapTileLayerBasic) {
            ((MapTileLayerBasic) mTileProvider).addTileSource(aTileSource, index);
            updateAfterSourceChange();
        }
    }

    public void removeTileSource(final ITileLayer aTileSource) {
        if (aTileSource != null && mTileProvider != null && mTileProvider instanceof MapTileLayerBasic) {
            ((MapTileLayerBasic) mTileProvider).removeTileSource(aTileSource);
            updateAfterSourceChange();
        }
    }

    public void removeTileSource(final int index) {
        if (mTileProvider != null && mTileProvider instanceof MapTileLayerBasic) {
            ((MapTileLayerBasic) mTileProvider).removeTileSource(index);
            updateAfterSourceChange();
        }
    }

    /**
     * Adds a marker to the default marker overlay.
     *
     * @param marker the marker object to be added
     * @return the marker object
     */
    public Marker addMarker(final Marker marker) {
        if (firstMarker) {
            defaultMarkerList.add(marker);
            setDefaultItemizedOverlay();
        } else {
            defaultMarkerOverlay.addItem(marker);
        }
        marker.addTo(this);
        firstMarker = false;
        return marker;
    }

    /**
     * Remove a marker from the map's display.
     */
    public void removeMarker(final Marker marker) {
        defaultMarkerList.remove(marker);
        defaultMarkerOverlay.removeItem(marker);
        this.invalidate();
    }

    /**
     * Remove all markers from the map's display.
     */
    public void clear() {
        defaultMarkerList.clear();
        if (defaultMarkerOverlay != null) {
            defaultMarkerOverlay.removeAllItems();
        }
        this.invalidate();
    }

    /**
     * Select a marker, showing a tooltip if the marker has content that would appear within it.
     */
    public void selectMarker(final Marker marker) {
        InfoWindow toolTip = marker.getToolTip(MapView.this);

        if (mMapViewListener != null) {
            mMapViewListener.onTapMarker(MapView.this, marker);
        }
        closeCurrentTooltip();
        if (toolTip != currentTooltip && marker.hasContent()) {
            if (mMapViewListener != null) {
                mMapViewListener.onShowMarker(MapView.this, marker);
            }
            currentTooltip = toolTip;
            marker.showBubble(currentTooltip, MapView.this, true);
        }
    }

    /**
     * Adds a new ItemizedOverlay to the MapView
     *
     * @param itemizedOverlay the itemized overlay
     */
    public void addItemizedOverlay(final ItemizedOverlay itemizedOverlay) {
        if (itemizedOverlay instanceof ItemizedIconOverlay) {
            // Make sure Markers are added to MapView
            ItemizedIconOverlay overlay = (ItemizedIconOverlay) itemizedOverlay;
            for (int lc = 0; lc < overlay.size(); lc++) {
                overlay.getItem(lc).addTo(this);
            }
        }

        this.getOverlays().add(itemizedOverlay);
    }

    /**
     * Get all itemized overlays on the map as an ArrayList.
     * @return
     */
    public ArrayList getItemizedOverlays() {
        ArrayList list = new ArrayList();
        for (Overlay overlay : getOverlays()) {
            if (overlay instanceof ItemizedOverlay) {
                list.add((ItemizedIconOverlay) overlay);
            }
        }
        return list;
    }

    /**
     * Load and parse a GeoJSON file at a given URL
     *
     * @param URL the URL from which to load the GeoJSON file
     */
    public void loadFromGeoJSONURL(String URL) {
        if (NetworkUtils.isNetworkAvailable(getContext())) {
            new GeoJSONPainter(this, null).loadFromURL(URL);
        }
    }

    /**
     * Parse a GeoJSON file at a given URL
     * @param url The URL of GeoJSON string to parse
     * @return FeatureCollection Parsed GeoJSON
     */
    public FeatureCollection parseFromGeoJSONURL(final String url) throws IOException, JSONException {
        return DataLoadingUtils.loadGeoJSONFromUrl(url);
    }

    /**
     * Close the currently-displayed tooltip, if any.
     */
    public void closeCurrentTooltip() {
        if (currentTooltip != null) {
            if (mMapViewListener != null) {
                mMapViewListener.onHideMarker(this, currentTooltip.getBoundMarker());
            }
            currentTooltip.close();
            currentTooltip = null;
        }
    }

    /**
     * Get the current tooltip of this map if there is one being displayed.
     * @return
     */
    public InfoWindow getCurrentTooltip() {
        return currentTooltip;
    }

    /**
     * Sets the default itemized overlay.
     */
    private void setDefaultItemizedOverlay() {
        defaultMarkerOverlay = new ItemizedIconOverlay(getContext(), defaultMarkerList,
                new ItemizedIconOverlay.OnItemGestureListener() {
                    public boolean onItemSingleTapUp(final int index, final Marker item) {
                        selectMarker(item);
                        return true;
                    }

                    public boolean onItemLongPress(final int index, final Marker item) {
                        if (mMapViewListener != null) {
                            mMapViewListener.onLongPressMarker(MapView.this, item);
                        }
                        return true;
                    }
                }
        );
        addItemizedOverlay(defaultMarkerOverlay);
    }

    /**
     * @param p the position where the event occurred.
     * @return whether the event action is triggered or not
     */
    public boolean singleTapUpHelper(final ILatLng p) {
        closeCurrentTooltip();
        onTap(p);
        return true;
    }

    /**
     * @param p the position where the event occurred.
     * @return whether the event action is triggered or not
     */
    public boolean longPressHelper(final ILatLng p) {
        onLongPress(p);
        return false;
    }

    public void onLongPress(final ILatLng p) {
        if (mMapViewListener != null) {
            mMapViewListener.onLongPressMap(MapView.this, p);
        }
    }

    public void onTap(final ILatLng p) {
        if (mMapViewListener != null) {
            mMapViewListener.onTapMap(MapView.this, p);
        }
    }

    /**
     * Returns the map's controller
     */
    public MapController getController() {
        return this.mController;
    }

    /**
     * Returns the map's overlay
     */
    public TilesOverlay getMapOverlay() {
        return mTilesOverlay;
    }

    /**
     * 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 getOverlays() {
        return this.getOverlayManager();
    }

    public OverlayManager getOverlayManager() {
        return mOverlayManager;
    }

    public MapTileLayerBase getTileProvider() {
        return mTileProvider;
    }

    /**
     * Returns the map's scroller
     */
    public Scroller getScroller() {
        return mScroller;
    }

    public Handler getTileRequestCompleteHandler() {
        return mTileRequestCompleteHandler;
    }

    public BoundingBox getBoundingBoxInternal() {
        if (getMeasuredWidth() == 0 || getMeasuredHeight() == 0) {
            return null;
        }
        final Rect screenRect = GeometryMath.viewPortRect(getProjection(), null);
        ILatLng neGeoPoint =
                Projection.pixelXYToLatLong(screenRect.right, screenRect.top, mZoomLevel);
        ILatLng swGeoPoint =
                Projection.pixelXYToLatLong(screenRect.left, screenRect.bottom, mZoomLevel);

        return new BoundingBox(neGeoPoint.getLatitude(), neGeoPoint.getLongitude(),
                swGeoPoint.getLatitude(), swGeoPoint.getLongitude());
    }

    /**
     * Returns the current bounding box of the map.
     */
    public BoundingBox getBoundingBox() {
        return getProjection().getBoundingBox();
    }

    /**
     * Get centerpoint of the phone as latitude and longitude.
     *
     * @return centerpoint
     */
    public LatLng getCenter() {
        final int worldSize_current_2 = Projection.mapSize(mZoomLevel) >> 1;
        return Projection.pixelXYToLatLong((float) mDScroll.x
                        + worldSize_current_2, (float) mDScroll.y + worldSize_current_2,
                mZoomLevel
        );
    }

    public Rect getIntrinsicScreenRect(Rect reuse) {
        if (reuse == null) {
            reuse = new Rect();
        }
        final int width_2 = getMeasuredWidth() >> 1;
        final int height_2 = getMeasuredHeight() >> 1;
        final int scrollX = getScrollX();
        final int scrollY = getScrollY();
        reuse.set(scrollX - width_2, scrollY - height_2, scrollX + width_2, scrollY + height_2);
        return reuse;
    }

    /**
     * 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.
     */
    public Projection getProjection() {
        if (mProjection == null) {
            mProjection = new Projection(this);
        }
        return mProjection;
    }

    /**
     * Set the centerpoint of the map view, given a latitude and
     * longitude position.
     *
     * @return the map view, for chaining
     */
    public MapView setCenter(final ILatLng aCenter) {
        return setCenter(aCenter, false);
    }

    public MapView setCenter(final ILatLng aCenter, final boolean userAction) {
        getController().setCurrentlyInUserAction(userAction);
        getController().setCenter(aCenter);
        getController().setCurrentlyInUserAction(false);
        return this;
    }


    /**
     * Pan the map by a given number of pixels in the x and y dimensions.
     */
    public MapView panBy(int x, int y) {
        this.mController.panBy(x, y);
        return this;
    }

    public MapView setScale(float scale) {
        float zoomDelta = (float) (Math.log(scale) / Math.log(2d));
        float newZoom = mZoomLevel + zoomDelta;
        if (newZoom <= mMaximumZoomLevel && newZoom >= mMinimumZoomLevel) {
            mMultiTouchScale = scale;
            updateInversedTransformMatrix();
            invalidate();
        }
        return this;
    }

    public float getScale() {
        return mMultiTouchScale;
    }

    private final void updateInversedTransformMatrix() {
        mInvTransformMatrix.reset();
        mInvTransformMatrix.preScale(1 / mMultiTouchScale, 1 / mMultiTouchScale, mMultiTouchScalePoint.x,
                mMultiTouchScalePoint.y);
    }

    public final Matrix getInversedTransformMatrix() {
        return mInvTransformMatrix;
    }


    private void snapItems() {
        // snap for all snappables
        final Point snapPoint = new Point();
        if (this.getOverlayManager().onSnapToItem(getScrollX(), getScrollY(), snapPoint, this)) {
            scrollTo(snapPoint.x, snapPoint.y);
        }
    }

    /**
     * @param aZoomLevel the zoom level bound by the tile source
     * @return the map view, for chaining
     */
    public MapView setZoom(final float aZoomLevel) {
        return this.mController.setZoom(aZoomLevel);
    }

    protected MapView setZoomInternal(final float aZoomLevel) {
        return setZoomInternal(aZoomLevel, null, null);
    }

    protected MapView setZoomInternal(final float aZoomLevel, ILatLng center, final PointF decale) {

        if (center == null) {
            center = getCenter();
        }

        final float newZoomLevel = getClampedZoomLevel(aZoomLevel);
        final float curZoomLevel = this.mZoomLevel;

        // reset the touchScale because from now on the zoom is the new one
        mMultiTouchScale = 1.0f;
        mInvTransformMatrix.reset();

        if (newZoomLevel != curZoomLevel) {
            this.mZoomLevel = newZoomLevel;
            // just to be sure any one got the right one
            setAnimatedZoom(this.mZoomLevel);
            mScroller.forceFinished(true);
            mIsFlinging = false;
            updateScrollableAreaLimit();
        }

        if (center != null) {
            // we cant use the mProjection because the values are not the right
            // one yet
            final PointF centerPoint = Projection.toMapPixels(
                    center.getLatitude(), center.getLongitude(), newZoomLevel,
                    mDScroll.x, mDScroll.y, null);
            if (decale != null) {
                centerPoint.offset(decale.x, decale.y);
            }
            scrollTo(centerPoint.x, centerPoint.y);
        } else {
            if (newZoomLevel > curZoomLevel) {
                // We are going from a lower-resolution plane to a higher-resolution plane, so we have
                // to do it the hard way.
                final int worldSize_new_2 = Projection.mapSize(newZoomLevel) >> 1;
                final ILatLng centerGeoPoint = getCenter();
                final PointF centerPoint = Projection.latLongToPixelXY(centerGeoPoint.getLatitude(),
                        centerGeoPoint.getLongitude(), newZoomLevel, null);
                scrollTo((int) centerPoint.x - worldSize_new_2, (int) centerPoint.y - worldSize_new_2);
            } else if (newZoomLevel < curZoomLevel) {
                // We are going from a higher-resolution plane to a lower-resolution plane, so we can do
                // it the easy way.
                scrollTo((int) (GeometryMath.rightShift(getScrollX(), curZoomLevel - newZoomLevel)),
                        (int) (GeometryMath.rightShift(getScrollY(), curZoomLevel - newZoomLevel)));
            }
        }


        mProjection = new Projection(this);
        // snap for all snappables
        snapItems();

        if (isLayedOut()) {
            getMapOverlay().rescaleCache(newZoomLevel, curZoomLevel, getProjection());
        }

        // do callback on listener
        if (newZoomLevel != curZoomLevel && mListeners.size() > 0) {
            final ZoomEvent event = new ZoomEvent(this, newZoomLevel, mController.currentlyInUserAction());
            for (MapListener listener : mListeners) {
                listener.onZoom(event);
            }
        }

        // Allows any views fixed to a Location in the MapView to adjust
        this.requestLayout();
        return this;
    }

    /**
     * compute the minimum zoom necessary to show a BoundingBox
     *
     * @param boundingBox the box to compute the zoom for
     * @param regionFit   if true computed zoom will make sure the whole box is visible
     * @param roundedZoom if true the required zoom will be rounded (for better
     *                    graphics)
     * @return the minimum zoom necessary to show the bounding box
     */
    private double minimumZoomForBoundingBox(final BoundingBox boundingBox,
                                             final boolean regionFit, final boolean roundedZoom) {
        final RectF rect = Projection.toMapPixels(boundingBox,
                TileLayerConstants.MAXIMUM_ZOOMLEVEL, mTempRect);
        final float requiredLatitudeZoom = TileLayerConstants.MAXIMUM_ZOOMLEVEL
                - (float) ((Math.log(rect.height() / getMeasuredHeight()) / Math
                .log(2)));
        final float requiredLongitudeZoom = TileLayerConstants.MAXIMUM_ZOOMLEVEL
                - (float) ((Math.log(rect.width() / getMeasuredWidth()) / Math
                .log(2)));
        double result = regionFit ? Math.min(requiredLatitudeZoom,
                requiredLongitudeZoom) : Math.max(requiredLatitudeZoom,
                requiredLongitudeZoom);
        if (roundedZoom) {
            result = regionFit ? Math.floor(result) : Math.round(result);
        }
        return result;
    }

    /**
     * Zoom the map to enclose the specified bounding box, as closely as
     * possible.
     *
     * @param boundingBox the box to compute the zoom for
     * @param regionFit   if true computed zoom will make sure the whole box is visible
     * @param animated    if true the zoom will be animated
     * @param roundedZoom if true the required zoom will be rounded (for better
     *                    graphics)
     * @param userAction  set to true if it comes from a userAction
     * @return the map view, for chaining
     */
    public MapView zoomToBoundingBox(final BoundingBox boundingBox,
                                     final boolean regionFit, final boolean animated,
                                     final boolean roundedZoom, final boolean userAction) {
        BoundingBox inter = (mScrollableAreaBoundingBox != null) ? mScrollableAreaBoundingBox
                .intersect(boundingBox) : boundingBox;
        if (inter == null || !inter.isValid()) {
            return this;
        }
        if (!mLayedOut) {
            mBoundingBoxToZoomOn = inter;
            mBoundingBoxToZoomOnRegionFit = regionFit;
            return this;
        }

        // Zoom to boundingBox center, at calculated maximum allowed zoom level
        final LatLng center = inter.getCenter();
        final float zoom = (float) minimumZoomForBoundingBox(inter, regionFit,
                roundedZoom);

        if (animated) {
            getController().setZoomAnimated(zoom, center, true, userAction);
        } else {
            getController().setZoom(zoom, center, userAction);
        }
        return this;
    }

    /**
     * Zoom the map to enclose the specified bounding box, as closely as
     * possible.
     *
     * @param boundingBox the box to compute the zoom for
     * @param regionFit   if true computed zoom will make sure the whole box is visible
     * @param animated    if true the zoom will be animated
     * @param roundedZoom if true the required zoom will be rounded (for better
     *                    graphics)
     * @return the map view, for chaining
     */
    public MapView zoomToBoundingBox(final BoundingBox boundingBox,
                                     final boolean regionFit, final boolean animated,
                                     final boolean roundedZoom) {
        return zoomToBoundingBox(boundingBox, regionFit, animated, roundedZoom,
                false);
    }

    /**
     * Zoom the map to enclose the specified bounding box, as closely as
     * possible.
     *
     * @param boundingBox the box to compute the zoom for
     * @param regionFit   if true computed zoom will make sure the whole box is visible
     * @param animated    if true the zoom will be animated
     * @return the map view, for chaining
     */
    public MapView zoomToBoundingBox(final BoundingBox boundingBox,
                                     final boolean regionFit, final boolean animated) {
        return zoomToBoundingBox(boundingBox, regionFit, animated, false, false);
    }

    /**
     * Zoom the map to enclose the specified bounding box, as closely as
     * possible.
     *
     * @param boundingBox the box to compute the zoom for
     * @param regionFit   if true computed zoom will make sure the whole box is visible
     * @return the map view, for chaining
     */
    public MapView zoomToBoundingBox(final BoundingBox boundingBox,
                                     final boolean regionFit) {
        return zoomToBoundingBox(boundingBox, regionFit, false, false);
    }

    /**
     * Zoom the map to enclose the specified bounding box, as closely as
     * possible.
     *
     * @param boundingBox the box to compute the zoom for
     * @return the map view, for chaining
     */
    public MapView zoomToBoundingBox(final BoundingBox boundingBox) {
        return zoomToBoundingBox(boundingBox, false);
    }

    public float getClampedZoomLevel(float zoom) {
        final float minZoomLevel = getMinZoomLevel();
        final float maxZoomLevel = getMaxZoomLevel();

        return Math.max(minZoomLevel, Math.min(maxZoomLevel, zoom));
    }

    /**
     * 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.
     */
    public float getZoomLevel() {
        return getZoomLevel(true);
    }

    protected float getAnimatedZoom() {
        return Float.intBitsToFloat(mTargetZoomLevel.get());
    }
    protected void setAnimatedZoom(float value) {
        mTargetZoomLevel.set(Float.floatToIntBits(value));
    }

    protected void clearAnimatedZoom(float value) {
        Float.floatToIntBits(-1);
    }

    protected boolean isAnimatedZoomSet() {
        return  Float.intBitsToFloat(mTargetZoomLevel.get()) != -1;
    }

    /**
     * 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 float getZoomLevel(final boolean aPending) {
        if (aPending && isAnimating()) {
            return getAnimatedZoom();
        } else {
            return mZoomLevel;
        }
    }

    /**
     * Get the minimum allowed zoom level for the maps.
     */
    public float getMinZoomLevel() {
        return Math.max(mMinimumZoomLevel, 0);
    }

    /**
     * Get the maximum allowed zoom level for the maps.
     */
    public float getMaxZoomLevel() {
        return mMaximumZoomLevel;
    }

    /**
     * Set the minimum allowed zoom level, or pass null to use the minimum zoom level from the tile
     * provider.
     */
    public void setMinZoomLevel(float zoomLevel) {
        mRequestedMinimumZoomLevel = mMinimumZoomLevel = zoomLevel;
        updateMinZoomLevel();
    }

    /**
     * Set the maximum allowed zoom level, or pass null to use the maximum zoom level from the tile
     * provider.
     */
    public void setMaxZoomLevel(float zoomLevel) {
        mMaximumZoomLevel = zoomLevel;
    }

    /**
     * Determine whether the map is at its maximum zoom
     *
     * @return whether the map can zoom in
     */
    protected boolean canZoomIn() {
        final float maxZoomLevel = getMaxZoomLevel();
        if ((isAnimating() ? getAnimatedZoom() : mZoomLevel) >= maxZoomLevel) {
            return false;
        }
        return true;
    }

    /**
     * Determine whether the map is at its minimum zoom
     *
     * @return whether the map can zoom out
     */
    protected boolean canZoomOut() {
        final float minZoomLevel = getMinZoomLevel();
        if ((isAnimating() ? getAnimatedZoom() : mZoomLevel) <= minZoomLevel) {
            return false;
        }
        return true;
    }

    /**
     * Zoom in by one zoom level.
     */
    public boolean zoomIn() {
        return getController().zoomIn();
    }


    public boolean zoomInFixing(final ILatLng point, final boolean userAction) {
        return getController().zoomInAbout(point, userAction);
    }

    public boolean zoomInFixing(final ILatLng point) {
        return zoomInFixing(point, false);
    }

    /**
     * Zoom out by one zoom level.
     */
    public boolean zoomOut() {
        return getController().zoomOut();
    }

    public boolean zoomOutFixing(final ILatLng point, final boolean userAction) {
        return getController().zoomOutAbout(point, userAction);
    }

    public boolean zoomOutFixing(final ILatLng point) {
        return zoomOutFixing(point, false);
    }

    /**
     * Set the rotation of the map, in degrees. A value of 0, meaning straight up, is default.
     *
     * @param degrees the angle of the map
     */
    public void setMapOrientation(float degrees) {
        this.mapOrientation = degrees % 360.0f;
        this.mProjection = null;
        this.invalidate();
    }

    /**
     * Gets the current angle of rotation of the map
     *
     * @return the current angle in degrees.
     */
    public float getMapOrientation() {
        return mapOrientation;
    }

    /**
     * Gets whether the current map rotation feature is enabled or not
     * default: disabled
     */
    public boolean isMapRotationEnabled() {
        return mMapRotationEnabled;
    }

    /**
     * Sets whether to enable or disable the map rotation features
     * default: disabled
     */
    public void setMapRotationEnabled(boolean enable) {
        mMapRotationEnabled = enable;
    }

    /**
     * Gets the mapView onMapOrientationChangeListener
     * @return the onMapOrientationChangeListener
     */
    public OnMapOrientationChangeListener getOnMapOrientationChangeListener() {
        return mOnMapOrientationChangeListener;
    }

    /**
     * Gets the mapView onMapOrientationChangeListener
     * @Param l the onMapOrientationChangeListener
     */
    public void setOnMapOrientationChangeListener(OnMapOrientationChangeListener l) {
        this.mOnMapOrientationChangeListener = l;
    }

    /**
     * Whether to use the network connection if it's available.
     */
    public boolean useDataConnection() {
        return mTilesOverlay.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) {
        mTilesOverlay.setUseDataConnection(aMode);
    }

    private void updateMinZoomLevel() {
        if (mScrollableAreaBoundingBox == null || !mLayedOut) {
            return;
        }
        mMinimumZoomLevel = (float) Math.max(
                mRequestedMinimumZoomLevel,
                minimumZoomForBoundingBox(mScrollableAreaBoundingBox, mConstraintRegionFit,
                        false)
        );
        if (mZoomLevel < mMinimumZoomLevel) {
            setZoom(mMinimumZoomLevel);
        }
    }

    /**
     * Everytime we update the zoom or the view size we must re compute the real scrollable area
     * limit in pixels
     */
    public void updateScrollableAreaLimit() {
        if (mScrollableAreaBoundingBox == null || !isLayedOut()) {
            return;
        }
        if (mScrollableAreaLimit == null) {
            mScrollableAreaLimit = new RectF();
        }
        Projection.toMapPixels(mScrollableAreaBoundingBox, getZoomLevel(false),
                mScrollableAreaLimit);
//        if (mConstraintRegionFit) {
//            int width = getMeasuredWidth();
//            int height = getMeasuredHeight();
//            float ratioX = mScrollableAreaLimit.width() / (float) width;
//            float ratioY = mScrollableAreaLimit.height() / (float) height;
//
//            if (ratioX != ratioY) {
//                if (ratioX < ratioY)
//                {
//                    float newWidth_2 = mScrollableAreaLimit.height() * width / (float) height / 2;
//                    float centerX = mScrollableAreaLimit.centerX();
//                    mScrollableAreaLimit.set(centerX - newWidth_2, mScrollableAreaLimit.top, centerX + newWidth_2, mScrollableAreaLimit.bottom);
//                } else {
//                    float newHeight_2 = width * ratioX / 2;
//                    float centerY = mScrollableAreaLimit.centerY();
//                    mScrollableAreaLimit.set(mScrollableAreaLimit.left, centerY - newHeight_2, mScrollableAreaLimit.right, centerY + newHeight_2);
//                }
//            }
//
//        }

    }

    /**
     * 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 setScrollableAreaLimit(BoundingBox boundingBox) {

        mScrollableAreaBoundingBox = boundingBox;

        // Clear scrollable area limit if null passed.
        if (mScrollableAreaBoundingBox == null) {
            mMinimumZoomLevel = mRequestedMinimumZoomLevel;
            mScrollableAreaLimit = null;
        } else {
            updateScrollableAreaLimit();
            updateMinZoomLevel();
        }
    }

    /**
     * Returns if the map can go to a specified geo point
     */
    public boolean canGoTo(ILatLng point) {
        return (mScrollableAreaBoundingBox == null || mScrollableAreaBoundingBox.contains(point));
    }

    /**
     * Returns if the map can go to a specified point (in map coordinates)
     */
    public boolean canGoTo(final float x, final float y) {
        return (mScrollableAreaLimit == null || mScrollableAreaLimit.contains(x, y));
    }

    /**
     * Returns the map current scrollable bounding box
     */
    public BoundingBox getScrollableAreaBoundingBox() {
        return mScrollableAreaBoundingBox;
    }

    /**
     * Returns the map current scrollable bounding limit int map PX
     */
    public RectF getScrollableAreaLimit() {
        return mScrollableAreaLimit;
    }


    /**
     * Returns true if the view has been layed out
     */
    public boolean isLayedOut() {
        return mLayedOut;
    }

    public void invalidateMapCoordinates(final Rect dirty) {
        mInvalidateRect.set(dirty);
        final int width_2 = this.getWidth() / 2;
        final int height_2 = this.getHeight() / 2;

        // Since the canvas is shifted by getWidth/2, we can just return our natural scrollX/Y value
        // since that is the same as the shifted center.
        int centerX = this.getScrollX();
        int centerY = this.getScrollY();

        if (this.getMapOrientation() != 0) {
            GeometryMath.getBoundingBoxForRotatedRectangle(mInvalidateRect, centerX, centerY,
                    this.getMapOrientation() + 180, mInvalidateRect);
        }
        mInvalidateRect.offset(width_2, height_2);

        super.invalidate(mInvalidateRect);
    }

    public void invalidateMapCoordinates(final RectF dirty) {
        dirty.roundOut(mInvalidateRect);
        final int width_2 = this.getWidth() / 2;
        final int height_2 = this.getHeight() / 2;

        // Since the canvas is shifted by getWidth/2, we can just return our natural scrollX/Y value
        // since that is the same as the shifted center.
        int centerX = this.getScrollX();
        int centerY = this.getScrollY();

        if (this.getMapOrientation() != 0) {
            GeometryMath.getBoundingBoxForRotatedRectangle(mInvalidateRect, centerX, centerY,
                    this.getMapOrientation() + 180, mInvalidateRect);
        }
        mInvalidateRect.offset(width_2, height_2);

        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
     * com.mapbox.mapboxsdk.geometry.LatLng} (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) {
        final int count = getChildCount();

        int maxHeight = 0;
        int maxWidth = 0;

        // Find out how big everyone wants to be
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        final Projection projection = getProjection();
        // Find rightmost and bottom-most child
        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();
                projection.toMapPixels(lp.geoPoint, mPoint);
                final int x = (int) mPoint.x + getWidth() / 2;
                final int y = (int) mPoint.y + getHeight() / 2;
                int childRight = x;
                int childBottom = y;
                switch (lp.alignment) {
                    case MapView.LayoutParams.TOP_LEFT:
                        childRight = x + childWidth;
                        childBottom = y;
                        break;
                    case MapView.LayoutParams.TOP_CENTER:
                        childRight = x + childWidth / 2;
                        childBottom = y;
                        break;
                    case MapView.LayoutParams.TOP_RIGHT:
                        childRight = x;
                        childBottom = y;
                        break;
                    case MapView.LayoutParams.CENTER_LEFT:
                        childRight = x + childWidth;
                        childBottom = y + childHeight / 2;
                        break;
                    case MapView.LayoutParams.CENTER:
                        childRight = x + childWidth / 2;
                        childBottom = y + childHeight / 2;
                        break;
                    case MapView.LayoutParams.CENTER_RIGHT:
                        childRight = x;
                        childBottom = y + childHeight / 2;
                        break;
                    case MapView.LayoutParams.BOTTOM_LEFT:
                        childRight = x + childWidth;
                        childBottom = y + childHeight;
                        break;
                    case MapView.LayoutParams.BOTTOM_CENTER:
                        childRight = x + childWidth / 2;
                        childBottom = y + childHeight;
                        break;
                    case MapView.LayoutParams.BOTTOM_RIGHT:
                        childRight = x;
                        childBottom = y + childHeight;
                        break;
                }
                childRight += lp.offsetX;
                childBottom += lp.offsetY;

                maxWidth = Math.max(maxWidth, childRight);
                maxHeight = Math.max(maxHeight, childBottom);
            }
        }

        // Account for padding too
        maxWidth += getPaddingLeft() + getPaddingRight();
        maxHeight += getPaddingTop() + getPaddingBottom();

        // Check against minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec),
                resolveSize(maxHeight, heightMeasureSpec));
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w != 0 && h != 0) {
            mProjection = null;
            if (!mLayedOut) {
                mLayedOut = true;
                //first layout: if some actions were triggered before, they were enqueued
                //let's trigger them again!
                mController.mapViewLayedOut();
            }
            updateScrollableAreaLimit();
            updateMinZoomLevel();

            if (mBoundingBoxToZoomOn != null) {
                zoomToBoundingBox(mBoundingBoxToZoomOn, mBoundingBoxToZoomOnRegionFit);
                mBoundingBoxToZoomOn = null;
            }
        }
    }

    @Override
    protected void onLayout(final boolean changed, final int l, final int t, final int r,
                            final int b) {
        final int count = getChildCount();

        final Projection projection = getProjection();
        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();
                projection.toMapPixels(lp.geoPoint, mPoint);
                final int x = (int) mPoint.x + getWidth() / 2;
                final int y = (int) mPoint.y + getHeight() / 2;
                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);
            }
        }
    }

    public void onDetach() {
        this.getOverlayManager().onDetach(this);
        mTileProvider.detach();
    }

    @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);
    }

    private boolean canTapTwoFingers = false;
    private int multiTouchDownCount = 0;

    private boolean handleTwoFingersTap(MotionEvent event) {
        int pointerCount = event.getPointerCount();
        for (int i = 0; i < pointerCount; i++) {
            int action = event.getActionMasked();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    multiTouchDownCount = 0;
                    break;
                case MotionEvent.ACTION_UP:
                    if (!isAnimating() && canTapTwoFingers) {
                        final ILatLng center =
                                getProjection().fromPixels(event.getX(), event.getY());
                        mController.zoomOutAbout(center);
                        canTapTwoFingers = false;
                        multiTouchDownCount = 0;
                        return true;
                    }
                    canTapTwoFingers = false;
                    multiTouchDownCount = 0;
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    multiTouchDownCount++;
                    canTapTwoFingers = multiTouchDownCount > 1;
                    break;
                case MotionEvent.ACTION_POINTER_UP:
                    multiTouchDownCount--;
                    //                    canTapTwoFingers = multiTouchDownCount > 1;
                    break;
                default:
            }
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // If map rotation is enabled, propagate onTouchEvent to the rotate gesture detector
        if (mMapRotationEnabled) {
            mRotateGestureDetector.onTouchEvent(event);
        }
        // Get rotated event for some touch listeners.
        MotionEvent rotatedEvent = rotateTouchEvent(event);

        try {
            if (this.getOverlayManager().onTouchEvent(rotatedEvent, this)) {
                Log.d(TAG, "OverlayManager handled onTouchEvent");
                return true;
            }

            // can't use the scale detector's onTouchEvent() result as it always returns true (Android issue #42591)
            //Android seems to be able to recognize a scale with one pointer ...
            // what a smart guy... let's prevent this
            if (rotatedEvent.getPointerCount() != 1) {
                mScaleGestureDetector.onTouchEvent(rotatedEvent);
            }
            boolean result = mScaleGestureDetector.isInProgress();
            if (!result) {
                result = mGestureDetector.onTouchEvent(rotatedEvent);
            } else {
                //needs to cancel two fingers tap
                canTapTwoFingers = false;
            }
            //handleTwoFingersTap should always be called because it counts pointers up/down
            result |= handleTwoFingersTap(rotatedEvent);

            return result;
        } finally {
            if (rotatedEvent != event) {
                rotatedEvent.recycle();
            }
        }
    }

    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) {
            mRotatePoints[0] = ev.getX();
            mRotatePoints[1] = ev.getY();
            getProjection().rotatePoints(mRotatePoints);
            rotatedEvent.setLocation(mRotatePoints[0], mRotatePoints[1]);
        } 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().getRotationMatrix());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return rotatedEvent;
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            if (mScroller.isFinished()) {
                // One last scrollTo to get to the final destination
                scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
                // snapping-to any Snappable points.
                if (!isAnimating()) {
                    snapItems();
                }
                mIsFlinging = false;
            } else {
                scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            }
            postInvalidate(); // Keep on drawing until the animation has
            // finished.
        }
    }

    public void updateScrollDuringAnimation() {
        // updateScrollableAreaLimit();
        // scrollTo(getScrollX(), getScrollY());
    }

    private PointF mDScroll = new PointF();

    public final PointF getScrollPoint() {
        return mDScroll;
    }

    public final void setScrollPoint(final PointF point) {
        scrollTo((double) point.x, (double) point.y);
    }

    public final PointF getScalePoint() {
        return mMultiTouchScalePoint;
    }

    public final void setScalePoint(final PointF point) {
        mMultiTouchScalePoint.set(point);
        updateInversedTransformMatrix();
    }

    @Override
    public void scrollTo(int x, int y) {
        // a trick for everyone to go through the double version of the method
        scrollTo((double) x, (double) y);
    }

    public void scrollBy(double x, double y) {
        scrollTo(mDScroll.x + x, mDScroll.y + y);
    }

    public void scrollTo(double x, double y) {
        if (mScrollableAreaLimit != null) {
            final RectF currentLimit = mScrollableAreaLimit;

            final double xToTestWith = x;
            final double yToTestWith = y;
            final float width_2 = this.getMeasuredWidth() / 2;
            final float height_2 = this.getMeasuredHeight() / 2;
            // Adjust if we are outside the scrollable area
            if (currentLimit.width() <= width_2 * 2) {
                x = currentLimit.centerX();
            } else if (xToTestWith - width_2 < currentLimit.left) {
                x = (currentLimit.left + width_2);
            } else if (xToTestWith + width_2 > currentLimit.right) {
                x = (currentLimit.right - width_2);
            }

            if (currentLimit.height() <= height_2 * 2) {
                y = currentLimit.centerY();
            } else if (yToTestWith - height_2 < currentLimit.top) {
                y = (currentLimit.top + height_2);
            } else if (yToTestWith + height_2 > currentLimit.bottom) {
                y = (currentLimit.bottom - height_2);
            }
        }

        if (!isAnimating()) {
            float deltaX = (float) (x - mDScroll.x);
            float deltaY = (float) (y - mDScroll.y);
            mController.offsetDeltaScroll(deltaX, deltaY);
        }
        mDScroll.set((float) x, (float) y);

        final int intX = (int) Math.round(x);
        final int intY = (int) Math.round(y);

        // make sure the next time someone wants the projection it is the
        // correct one!
        mProjection = null;

        super.scrollTo(intX, intY);

        // do callback on listener
        if (mListeners.size() > 0) {
            final ScrollEvent event = new ScrollEvent(this, intX, intY, mController.currentlyInUserAction());
            for (MapListener listener : mListeners) {
                listener.onScroll(event);
            }
        }
    }

    @Override
    public void setBackgroundColor(final int pColor) {
        mTilesOverlay.setLoadingBackgroundColor(pColor);
        invalidate();
    }

    @Override
    protected void onDraw(final Canvas c) {
        super.onDraw(c);

        mProjection = updateProjection();

        // Save the current canvas matrix
        c.save();

        c.translate(getWidth() / 2, getHeight() / 2);
        c.scale(mMultiTouchScale, mMultiTouchScale, mMultiTouchScalePoint.x,
                mMultiTouchScalePoint.y);

        // rotate Canvas
        c.rotate(mapOrientation, mProjection.getScreenRect().exactCenterX(),
                mProjection.getScreenRect().exactCenterY());

        // Draw all Overlays.
        this.getOverlayManager().draw(c, this);

        c.restore();
    }

    /**
     * Private Helper Method for onDraw().
     *
     * @return New Projection object
     */
    private Projection updateProjection() {
        return new Projection(this);
    }

    /**
     * Returns true if the safe drawing canvas is being used.
     *
     * @see {@link com.mapbox.mapboxsdk.views.safecanvas.ISafeCanvas}
     */
    public boolean isUsingSafeCanvas() {
        return this.getOverlayManager().isUsingSafeCanvas();
    }

    /**
     * Sets whether the safe drawing canvas is being used.
     *
     * @see {@link com.mapbox.mapboxsdk.views.safecanvas.ISafeCanvas}
     */
    public void setUseSafeCanvas(boolean useSafeCanvas) {
        this.getOverlayManager().setUseSafeCanvas(useSafeCanvas);
    }

    /**
     * Sets whether the scrollable area limit should take the view
     * ratio into account (keeping the same ratio as the screen)
     * If yes you will be able to zoom out to see the whole area
     * whatever the screen ratio.
     */
    public MapView setConstraintRegionFit(boolean value) {
        this.mConstraintRegionFit = value;
        if (isLayedOut()) {
            updateScrollableAreaLimit();
            updateMinZoomLevel();
        }
        return this;
    }

    private UserLocationOverlay getOrCreateLocationOverlay() {
        if (mLocationOverlay == null) {
            mLocationOverlay = new UserLocationOverlay(new GpsLocationProvider(getContext()), this);
            addOverlay(mLocationOverlay);
        }
        return mLocationOverlay;
    }

    /**
     * Show or hide the user location overlay
     */
    public MapView setUserLocationEnabled(final boolean value) {
        if (value) {
            getOrCreateLocationOverlay().enableMyLocation();
        } else if (mLocationOverlay != null) {
            mLocationOverlay.disableMyLocation();
            removeOverlay(mLocationOverlay);
            mLocationOverlay = null;
        }
        return this;
    }

    /**
     * Show or hide the user location overlay
     */
    public final boolean getUserLocationEnabled() {
        if (mLocationOverlay != null) {
            return mLocationOverlay.isMyLocationEnabled();
        }
        return false;
    }

    /**
     * Set the user location tracking mode
     */
    public MapView setUserLocationTrackingMode(final UserLocationOverlay.TrackingMode mode) {
        getOrCreateLocationOverlay().setTrackingMode(mode);
        return this;
    }

    /**
     * Set the user location tracking mode
     */
    public MapView setUserLocationRequiredZoom(final float zoomLevel) {
        getOrCreateLocationOverlay().setRequiredZoom(zoomLevel);
        return this;
    }

    /**
     * get the user location tracking mode
     */
    public UserLocationOverlay.TrackingMode getUserLocationTrackingMode() {
        if (mLocationOverlay != null) {
            return mLocationOverlay.getTrackingMode();
        }
        return UserLocationOverlay.TrackingMode.NONE;
    }

    /**
     * Go to user location
     */
    public void goToUserLocation(final boolean animated) {
        if (mLocationOverlay != null) {
            mLocationOverlay.goToMyPosition(animated);
        }
    }

    /**
     * Get the user location overlay if created
     */
    public UserLocationOverlay getUserLocationOverlay() {
        return mLocationOverlay;
    }

    /**
     * Get the user location overlay if created
     */
    public LatLng getUserLocation() {
        if (mLocationOverlay != null) {
            return mLocationOverlay.getMyLocation();
        }
        return null;
    }

    public boolean isUserLocationVisible() {
        if (mLocationOverlay != null) {
            final Location pos = mLocationOverlay.getLastFix();
            if (pos != null && isLayedOut()) {
                final Projection projection = getProjection();
                final float accuracyInPixels = pos.getAccuracy() / (float) projection.groundResolution(
                        pos.getLatitude());
                final PointF point = projection.toMapPixels(pos.getLatitude(), pos.getLongitude(), null);
                return projection.getScreenRect().intersects((int) (point.x - accuracyInPixels),
                        (int) (point.y - accuracyInPixels),
                        (int) (point.x + accuracyInPixels),
                        (int) (point.y + accuracyInPixels));
            }
        }
        return false;
    }

    /**
     * Enable or disable the diskCache
     */
    public void setDiskCacheEnabled(final boolean enabled) {
        if (mTileProvider != null) {
            mTileProvider.setDiskCacheEnabled(enabled);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        this.onDetach();
        super.onDetachedFromWindow();
    }

    /**
     * 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();
    }

    protected void setIsAnimating(final boolean value) {
        mIsAnimating.set(value);
    }

    public TileLoadedListener getTileLoadedListener() {
        return tileLoadedListener;
    }

    public static void setDebugMode(boolean value) {
        UtilConstants.setDebugMode(value);
    }

    /**
     * Per-child layout information associated with OpenStreetMapView.
     */
    public static class LayoutParams extends ViewGroup.LayoutParams implements MapViewLayouts {
        /**
         * The location of the child within the map view.
         */
        public ILatLng 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 aGeoPoint  the location of the child within the map view
         * @param aAlignment 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 aOffsetX   the additional X offset from the alignment location to draw the child
         *                   within
         *                   the map view
         * @param aOffsetY   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 ILatLng aGeoPoint,
                            final int aAlignment, final int aOffsetX, final int aOffsetY) {
            super(width, height);
            if (aGeoPoint != null) {
                this.geoPoint = aGeoPoint;
            } else {
                this.geoPoint = new LatLng(0, 0);
            }
            this.alignment = aAlignment;
            this.offsetX = aOffsetX;
            this.offsetY = aOffsetY;
        }

        /**
         * 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 LatLng(0, 0);
            this.alignment = BOTTOM_CENTER;
        }

        public LayoutParams(final ViewGroup.LayoutParams source) {
            super(source);
        }
    }

    public void setMapViewListener(MapViewListener listener) {
        this.mMapViewListener = listener;
    }

    public void setOnTileLoadedListener(TileLoadedListener aTileLoadedListener) {
        this.tileLoadedListener = aTileLoadedListener;
    }

    public void setOnTilesLoadedListener(TilesLoadedListener aTilesLoadedListener) {
        this.tilesLoadedListener = aTilesLoadedListener;
    }

    public TilesLoadedListener getTilesLoadedListener() {
        return tilesLoadedListener;
    }

    @Override
    public String toString() {
        return "MapView {" + getTileProvider() + "}";
    }

    public void setDefaultPinRes(int res) {
        mDefaultPinRes = res;
    }

    public void setDefaultPinDrawable(Drawable drawable) {
        mDefaultPinDrawable = drawable;
    }

    public Drawable getDefaultPinDrawable() {
        if (mDefaultPinDrawable == null && mDefaultPinRes != 0) {
            BitmapFactory.Options opts =
                    BitmapUtils.getBitmapOptions(getResources().getDisplayMetrics());
            mDefaultPinDrawable = new BitmapDrawable(getResources(),
                    BitmapFactory.decodeResource(context.getResources(), mDefaultPinRes, opts));
        }
        return mDefaultPinDrawable;
    }

    public void setDefaultPinAnchor(PointF point) {
        mDefaultPinAnchor = point;
    }

    public PointF getDefaultPinAnchor() {
        return mDefaultPinAnchor;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy