summaryrefslogtreecommitdiff
path: root/android/MapboxGLAndroidSDK/src/main/java/com/mapbox
diff options
context:
space:
mode:
Diffstat (limited to 'android/MapboxGLAndroidSDK/src/main/java/com/mapbox')
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapFragment.java129
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Annotation.java81
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindow.java222
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindowTipView.java40
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindowView.java39
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Marker.java147
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MarkerOptions.java84
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MultiPoint.java49
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polygon.java34
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolygonOptions.java108
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polyline.java39
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolylineOptions.java106
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Sprite.java40
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/SpriteFactory.java132
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/package-info.java4
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeoConstants.java10
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java13
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MathConstants.java8
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/Style.java54
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/package-info.java4
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidAccessTokenException.java19
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/SpriteBitmapChangedException.java26
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/TooManySpritesException.java20
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/package-info.java4
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/BoundingBox.java304
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateBounds.java58
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateRegion.java30
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateSpan.java43
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ILatLng.java12
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/IProjectedMeters.java7
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLng.java176
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngZoom.java93
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ProjectedMeters.java89
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/package-info.java5
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HTTPContext.java104
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/package-info.java4
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/package-info.java5
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ApiAccess.java60
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/package-info.java5
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/CompassView.java151
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/MapView.java3317
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/NativeMapView.java632
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/UserLocationView.java473
-rw-r--r--android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/package-info.java5
44 files changed, 6985 insertions, 0 deletions
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapFragment.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapFragment.java
new file mode 100644
index 0000000000..92405dc5cc
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapFragment.java
@@ -0,0 +1,129 @@
+package com.mapbox.mapboxsdk;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.mapbox.mapboxsdk.utils.ApiAccess;
+import com.mapbox.mapboxsdk.views.MapView;
+
+public class MapFragment extends Fragment {
+
+ //
+ // Static members
+ //
+
+ // Tag used for logging
+ private static final String TAG = "MapFragment";
+
+ //
+ // Instance members
+ //
+
+ // The map
+ private MapView mMap;
+
+ //
+ // Lifecycle events
+ //
+
+ // Called when the fragment is created
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ Log.v(TAG, "onCreateView");
+
+ // Create the map
+ mMap = (MapView) inflater.inflate(R.layout.mapview, container, false);
+
+ // Set accessToken
+ mMap.setAccessToken(ApiAccess.getToken(container.getContext()));
+
+ // Need to pass on any saved state to the map
+ mMap.onCreate(savedInstanceState);
+
+ // Return the map as the root view
+ return mMap;
+ }
+
+ // Called when the fragment is destroyed
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ Log.v(TAG, "onDestroyView");
+
+ // Need to pass on to view
+ mMap.onDestroy();
+ mMap = null;
+ }
+
+ // Called when the fragment is visible
+ @Override
+ public void onStart() {
+ super.onStart();
+ Log.v(TAG, "onStart");
+
+ // Need to pass on to view
+ mMap.onStart();
+ }
+
+ // Called when the fragment is invisible
+ @Override
+ public void onStop() {
+ super.onStop();
+ Log.v(TAG, "onStop");
+
+ // Need to pass on to view
+ mMap.onStop();
+ }
+
+ // Called when the fragment is in the background
+ @Override
+ public void onPause() {
+ super.onPause();
+ Log.v(TAG, "onPause");
+
+ // Need to pass on to view
+ mMap.onPause();
+ }
+
+ // Called when the fragment is no longer in the background
+ @Override
+ public void onResume() {
+ super.onResume();
+ Log.v(TAG, "onResume");
+
+ // Need to pass on to view
+ mMap.onResume();
+ }
+
+ // Called before fragment is destroyed
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ Log.v(TAG, "onSaveInstanceState");
+
+ // Need to retrieve any saved state from the map
+ mMap.onSaveInstanceState(outState);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onLowMemory() {
+ Log.v(TAG, "OnLowMemory");
+
+ // Need to pass on to view
+ mMap.onLowMemory();
+ super.onLowMemory();
+ }
+
+ //
+ // Property methods
+ //
+
+ public MapView getMap() {
+ return mMap;
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Annotation.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Annotation.java
new file mode 100644
index 0000000000..a83079ac41
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Annotation.java
@@ -0,0 +1,81 @@
+package com.mapbox.mapboxsdk.annotations;
+
+import android.support.annotation.NonNull;
+
+import com.mapbox.mapboxsdk.views.MapView;
+
+public abstract class Annotation implements Comparable<Annotation> {
+
+ /**
+ * The annotation id
+ * <p/>
+ * Internal C++ id is stored as unsigned int.
+ */
+ private long id = -1; // -1 unless added to a MapView
+ private MapView mapView;
+
+ protected Annotation() {
+ }
+
+ /**
+ * Do not use this method. Used internally by the SDK.
+ */
+ public long getId() {
+ return id;
+ }
+
+ public void remove() {
+ if (mapView == null) {
+ return;
+ }
+ mapView.removeAnnotation(this);
+ }
+
+ /**
+ * Do not use this method. Used internally by the SDK.
+ */
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ /**
+ * Do not use this method. Used internally by the SDK.
+ */
+ public void setMapView(MapView mapView) {
+ this.mapView = mapView;
+ }
+
+ protected MapView getMapView() {
+ if (mapView == null) {
+ return null;
+ }
+ return mapView;
+ }
+
+ @Override
+ public int compareTo(@NonNull Annotation annotation) {
+ if (id < annotation.getId()) {
+ return 1;
+ } else if (id > annotation.getId()) {
+ return -1;
+ }
+
+ // Equal
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Annotation that = (Annotation) o;
+
+ return getId() == that.getId();
+ }
+
+ @Override
+ public int hashCode() {
+ return (int) (getId() ^ (getId() >>> 32));
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindow.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindow.java
new file mode 100644
index 0000000000..799d01d5c0
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindow.java
@@ -0,0 +1,222 @@
+package com.mapbox.mapboxsdk.annotations;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.mapbox.mapboxsdk.R;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.views.MapView;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * A tooltip view
+ */
+final class InfoWindow {
+
+ private WeakReference<Marker> mBoundMarker;
+ private WeakReference<MapView> mMapView;
+ private boolean mIsVisible;
+ protected View mView;
+
+ static int mTitleId = 0;
+ static int mDescriptionId = 0;
+ static int mSubDescriptionId = 0;
+ static int mImageId = 0;
+
+ InfoWindow(int layoutResId, MapView mapView) {
+ View view = LayoutInflater.from(mapView.getContext()).inflate(layoutResId, mapView, false);
+
+ if (mTitleId == 0) {
+ setResIds(mapView.getContext());
+ }
+
+ initialize(view, mapView);
+ }
+
+ InfoWindow(View view, MapView mapView) {
+ initialize(view, mapView);
+ }
+
+ private void initialize(View view, MapView mapView) {
+ mMapView = new WeakReference<>(mapView);
+ mIsVisible = false;
+ mView = view;
+
+ // default behavior: close it when clicking on the tooltip:
+ mView.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent e) {
+ if (e.getAction() == MotionEvent.ACTION_UP) {
+ boolean handledDefaultClick = false;
+ MapView.OnInfoWindowClickListener onInfoWindowClickListener =
+ mMapView.get().getOnInfoWindowClickListener();
+ if (onInfoWindowClickListener != null) {
+ handledDefaultClick = onInfoWindowClickListener.onMarkerClick(getBoundMarker());
+ }
+
+ if (!handledDefaultClick) {
+ close();
+ }
+ }
+ return true;
+ }
+ });
+ }
+
+
+ /**
+ * open the window at the specified position.
+ *
+ * @param boundMarker the marker on which is hooked the view
+ * @param position to place the window on the map
+ * @param offsetX (&offsetY) the offset of the view to the position, in pixels.
+ * This allows to offset the view from the object position.
+ * @return this infowindow
+ */
+ InfoWindow open(Marker boundMarker, LatLng position, int offsetX, int offsetY) {
+ setBoundMarker(boundMarker);
+
+ MapView.LayoutParams lp = new MapView.LayoutParams(MapView.LayoutParams.WRAP_CONTENT, MapView.LayoutParams.WRAP_CONTENT);
+ mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+
+ // Calculate default Android x,y coordinate
+ PointF coords = mMapView.get().toScreenLocation(position);
+ float x = coords.x - (mView.getMeasuredWidth() / 2) + offsetX;
+ float y = coords.y - mView.getMeasuredHeight() + offsetY;
+
+ // get right/left popup window
+ float right = x + mView.getMeasuredWidth();
+ float left = x;
+
+ // get right/left map view
+ float mapRight = mMapView.get().getRight();
+ float mapLeft = mMapView.get().getLeft();
+
+ if (mView instanceof InfoWindowView) {
+ // only apply repositioning/margin for InfoWindowView
+ Resources resources = mMapView.get().getContext().getResources();
+ float margin = resources.getDimension(R.dimen.infowindow_margin);
+ float tipViewOffset = resources.getDimension(R.dimen.infowindow_tipview_width) / 2;
+ float tipViewMarginLeft = mView.getMeasuredWidth() / 2 - tipViewOffset;
+
+ // fit screen on right
+ if (right > mMapView.get().getRight()) {
+ x -= right - mapRight;
+ tipViewMarginLeft += right - mapRight + tipViewOffset;
+ right = x + mView.getMeasuredWidth();
+ }
+
+ // fit screen left
+ if (left < mMapView.get().getLeft()) {
+ x += mapLeft - left;
+ tipViewMarginLeft -= mapLeft - left + tipViewOffset;
+ left = x;
+ }
+
+ // Add margin right
+ if (mapRight - right < margin) {
+ x -= margin - (mapRight - right);
+ tipViewMarginLeft += margin - (mapRight - right) - tipViewOffset;
+ left = x;
+ }
+
+ // Add margin left
+ if (left - mapLeft < margin) {
+ x += margin - (left - mapLeft);
+ tipViewMarginLeft -= (margin - (left - mapLeft)) - tipViewOffset;
+ }
+
+ // Adjust tipView
+ InfoWindowView infoWindowView = (InfoWindowView) mView;
+ infoWindowView.setTipViewMarginLeft((int) tipViewMarginLeft);
+ }
+
+ // set anchor popupwindowview
+ mView.setX(x);
+ mView.setY(y);
+
+ close(); //if it was already opened
+ mMapView.get().addView(mView, lp);
+ mIsVisible = true;
+ return this;
+ }
+
+ /**
+ * Close this InfoWindow if it is visible, otherwise don't do anything.
+ *
+ * @return this info window
+ */
+ InfoWindow close() {
+ if (mIsVisible) {
+ mIsVisible = false;
+ ((ViewGroup) mView.getParent()).removeView(mView);
+ setBoundMarker(null);
+ onClose();
+ }
+ return this;
+ }
+
+ /**
+ * Constructs the view that is displayed when the InfoWindow opens.
+ * This retrieves data from overlayItem and shows it in the tooltip.
+ *
+ * @param overlayItem the tapped overlay item
+ */
+ void adaptDefaultMarker(Marker overlayItem) {
+ String title = overlayItem.getTitle();
+ ((TextView) mView.findViewById(mTitleId /*R.id.title*/)).setText(title);
+ String snippet = overlayItem.getSnippet();
+ ((TextView) mView.findViewById(mDescriptionId /*R.id.description*/)).setText(snippet);
+
+/*
+ //handle sub-description, hiding or showing the text view:
+ TextView subDescText = (TextView) mView.findViewById(mSubDescriptionId);
+ String subDesc = overlayItem.getSubDescription();
+ if ("".equals(subDesc)) {
+ subDescText.setVisibility(View.GONE);
+ } else {
+ subDescText.setText(subDesc);
+ subDescText.setVisibility(View.VISIBLE);
+ }
+*/
+ }
+
+ private void onClose() {
+ mMapView.get().deselectMarker();
+ }
+
+ InfoWindow setBoundMarker(Marker boundMarker) {
+ mBoundMarker = new WeakReference<>(boundMarker);
+ return this;
+ }
+
+ Marker getBoundMarker() {
+ if (mBoundMarker == null) {
+ return null;
+ }
+ return mBoundMarker.get();
+ }
+
+ /**
+ * Given a context, set the resource ids for the layout
+ * of the InfoWindow.
+ *
+ * @param context the apps Context
+ */
+ private static void setResIds(Context context) {
+ String packageName = context.getPackageName(); //get application package name
+ mTitleId = context.getResources().getIdentifier("id/infowindow_title", null, packageName);
+ mDescriptionId =
+ context.getResources().getIdentifier("id/infowindow_description", null, packageName);
+ mSubDescriptionId = context.getResources()
+ .getIdentifier("id/infowindow_subdescription", null, packageName);
+ mImageId = context.getResources().getIdentifier("id/infowindow_image", null, packageName);
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindowTipView.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindowTipView.java
new file mode 100644
index 0000000000..106e11820a
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindowTipView.java
@@ -0,0 +1,40 @@
+package com.mapbox.mapboxsdk.annotations;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.view.View;
+
+final class InfoWindowTipView extends View {
+
+ private Paint mPaint;
+ private Path mPath;
+
+ public InfoWindowTipView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mPath = new Path();
+
+ this.mPaint = new Paint();
+ this.mPaint.setColor(Color.WHITE);
+ this.mPaint.setAntiAlias(true);
+ this.mPaint.setStrokeWidth(0.0f);
+ this.mPaint.setStyle(Paint.Style.FILL);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ int height = getMeasuredHeight();
+ int width = getMeasuredWidth();
+
+ mPath.rewind();
+ mPath.moveTo((width / 2) - height, 0);
+ mPath.lineTo((width / 2) + height, 0);
+ mPath.lineTo((width / 2), height);
+ mPath.lineTo((width / 2) - height, 0);
+ canvas.drawPath(mPath, this.mPaint);
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindowView.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindowView.java
new file mode 100644
index 0000000000..9b0a339252
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindowView.java
@@ -0,0 +1,39 @@
+package com.mapbox.mapboxsdk.annotations;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.RelativeLayout;
+
+import com.mapbox.mapboxsdk.R;
+
+class InfoWindowView extends RelativeLayout {
+
+ private InfoWindowTipView mTipView;
+
+ public InfoWindowView(Context context) {
+ this(context, null);
+ }
+
+ public InfoWindowView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public InfoWindowView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize(context);
+ }
+
+ private void initialize(Context context) {
+ LayoutInflater.from(context).inflate(R.layout.infowindow_content, this);
+ mTipView = (InfoWindowTipView) findViewById(R.id.infowindow_tipview);
+ }
+
+ void setTipViewMarginLeft(int marginLeft) {
+ RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) mTipView.getLayoutParams();
+ layoutParams.leftMargin = marginLeft;
+ // This is a bit of a hack but prevents an occasional 1 pixel gap between the InfoWindow and
+ // the tip
+ layoutParams.topMargin = -1;
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Marker.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Marker.java
new file mode 100644
index 0000000000..4f43f9bfa2
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Marker.java
@@ -0,0 +1,147 @@
+package com.mapbox.mapboxsdk.annotations;
+
+import android.support.annotation.Nullable;
+import android.view.View;
+
+import com.mapbox.mapboxsdk.R;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.views.MapView;
+
+public final class Marker extends Annotation {
+
+ private LatLng position;
+ private String snippet;
+ private Sprite icon;
+ private String title;
+ private InfoWindow infoWindow = null;
+ private boolean infoWindowShown = false;
+ private int topOffsetPixels;
+
+ /**
+ * Constructor
+ */
+ Marker() {
+ super();
+ }
+
+ public LatLng getPosition() {
+ return position;
+ }
+
+ public String getSnippet() {
+ return snippet;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Do not use this method. Used internally by the SDK.
+ */
+ public void hideInfoWindow() {
+ if (infoWindow != null) {
+ infoWindow.close();
+ }
+ infoWindowShown = false;
+ }
+
+ /**
+ * Do not use this method. Used internally by the SDK.
+ */
+ public boolean isInfoWindowShown() {
+ return infoWindowShown;
+ }
+
+ void setPosition(LatLng position) {
+ this.position = position;
+ }
+
+ void setSnippet(String snippet) {
+ this.snippet = snippet;
+ }
+
+ /**
+ * Do not use this method. Used internally by the SDK.
+ */
+ public void setIcon(@Nullable Sprite icon) {
+ this.icon = icon;
+ }
+
+ public Sprite getIcon() {
+ return icon;
+ }
+
+ void setTitle(String title) {
+ this.title = title;
+ }
+
+ /**
+ * Do not use this method. Used internally by the SDK.
+ */
+ public void showInfoWindow() {
+ if (getMapView() == null) {
+ return;
+ }
+
+ MapView.InfoWindowAdapter infoWindowAdapter = getMapView().getInfoWindowAdapter();
+ if (infoWindowAdapter != null) {
+ // end developer is using a custom InfoWindowAdapter
+ View content = infoWindowAdapter.getInfoWindow(this);
+ if (content != null) {
+ infoWindow = new InfoWindow(content, getMapView());
+ showInfoWindow(infoWindow);
+ return;
+ }
+ }
+
+ getInfoWindow().adaptDefaultMarker(this);
+ showInfoWindow(getInfoWindow());
+ }
+
+ private void showInfoWindow(InfoWindow iw) {
+ iw.open(this, getPosition(), 0, topOffsetPixels);
+ infoWindowShown = true;
+ }
+
+ private InfoWindow getInfoWindow() {
+ if (infoWindow == null) {
+ infoWindow = new InfoWindow(R.layout.infowindow_view, getMapView());
+ }
+ return infoWindow;
+ }
+
+ /*
+ @Override
+ void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (!visible && infoWindowShown) {
+ hideInfoWindow();
+ }
+ }
+ */
+
+ /**
+ * Do not use this method. Used internally by the SDK.
+ */
+ public void setTopOffsetPixels(int topOffsetPixels) {
+ this.topOffsetPixels = topOffsetPixels;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+
+ Marker marker = (Marker) o;
+ return !(getPosition() != null ? !getPosition().equals(marker.getPosition()) : marker.getPosition() != null);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (getPosition() != null ? getPosition().hashCode() : 0);
+ return result;
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MarkerOptions.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MarkerOptions.java
new file mode 100644
index 0000000000..f0c15906e0
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MarkerOptions.java
@@ -0,0 +1,84 @@
+package com.mapbox.mapboxsdk.annotations;
+
+import android.support.annotation.Nullable;
+
+import com.mapbox.mapboxsdk.geometry.LatLng;
+
+public final class MarkerOptions {
+
+ private Marker marker;
+
+ public MarkerOptions() {
+ marker = new Marker();
+ }
+
+ /**
+ * Do not use this method. Used internally by the SDK.
+ */
+ public Marker getMarker() {
+ return marker;
+ }
+
+ public LatLng getPosition() {
+ return marker.getPosition();
+ }
+
+ public String getSnippet() {
+ return marker.getSnippet();
+ }
+
+ public String getTitle() {
+ return marker.getTitle();
+ }
+
+ public Sprite getIcon() {
+ return marker.getIcon();
+ }
+
+ public MarkerOptions position(LatLng position) {
+ marker.setPosition(position);
+ return this;
+ }
+
+ public MarkerOptions snippet(String snippet) {
+ marker.setSnippet(snippet);
+ return this;
+ }
+
+ public MarkerOptions icon(@Nullable Sprite icon) {
+ marker.setIcon(icon);
+ return this;
+ }
+
+ public MarkerOptions title(String title) {
+ marker.setTitle(title);
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ MarkerOptions marker = (MarkerOptions) o;
+
+ if (getPosition() != null ? !getPosition().equals(marker.getPosition()) : marker.getPosition() != null)
+ return false;
+ if (getSnippet() != null ? !getSnippet().equals(marker.getSnippet()) : marker.getSnippet() != null)
+ return false;
+ if (getIcon() != null ? !getIcon().equals(marker.getIcon()) : marker.getIcon() != null)
+ return false;
+ return !(getTitle() != null ? !getTitle().equals(marker.getTitle()) : marker.getTitle() != null);
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + (getPosition() != null ? getPosition().hashCode() : 0);
+ result = 31 * result + (getSnippet() != null ? getSnippet().hashCode() : 0);
+ result = 31 * result + (getIcon() != null ? getIcon().hashCode() : 0);
+ result = 31 * result + (getTitle() != null ? getTitle().hashCode() : 0);
+ return result;
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MultiPoint.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MultiPoint.java
new file mode 100644
index 0000000000..5c1dfb119f
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MultiPoint.java
@@ -0,0 +1,49 @@
+package com.mapbox.mapboxsdk.annotations;
+
+import com.mapbox.mapboxsdk.geometry.LatLng;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class MultiPoint extends Annotation {
+
+ private List<LatLng> points;
+ private float alpha = 1.0f;
+
+ protected MultiPoint() {
+ super();
+ points = new ArrayList<>();
+ }
+
+ /**
+ * Returns a copy of the points.
+ *
+ * @return points - as a copy
+ */
+ public List<LatLng> getPoints() {
+ return new ArrayList<>(points);
+ }
+
+ /**
+ * Sets the points of this polyline. This method will take a copy
+ * of the points, so further mutations to points will have no effect
+ * on this polyline.
+ *
+ * @param points the points of the polyline
+ */
+ void setPoints(List<LatLng> points) {
+ this.points = new ArrayList<>(points);
+ }
+
+ void addPoint(LatLng point) {
+ points.add(point);
+ }
+
+ public float getAlpha() {
+ return alpha;
+ }
+
+ void setAlpha(float alpha) {
+ this.alpha = alpha;
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polygon.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polygon.java
new file mode 100644
index 0000000000..32258943ab
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polygon.java
@@ -0,0 +1,34 @@
+package com.mapbox.mapboxsdk.annotations;
+
+import android.graphics.Color;
+
+import com.mapbox.mapboxsdk.geometry.LatLng;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class Polygon extends MultiPoint {
+
+ private int fillColor = Color.BLACK; // default fillColor is black
+ private int strokeColor = Color.BLACK; // default strokeColor is black
+
+ Polygon() {
+ super();
+ }
+
+ public int getFillColor() {
+ return fillColor;
+ }
+
+ public int getStrokeColor() {
+ return strokeColor;
+ }
+
+ void setFillColor(int color) {
+ fillColor = color;
+ }
+
+ void setStrokeColor(int color) {
+ strokeColor = color;
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolygonOptions.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolygonOptions.java
new file mode 100644
index 0000000000..42e80edc2c
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolygonOptions.java
@@ -0,0 +1,108 @@
+package com.mapbox.mapboxsdk.annotations;
+
+
+import com.mapbox.mapboxsdk.geometry.LatLng;
+
+import java.util.List;
+
+public final class PolygonOptions {
+
+ private Polygon polygon;
+
+ public PolygonOptions() {
+ polygon = new Polygon();
+ }
+
+ public PolygonOptions add(LatLng point) {
+ polygon.addPoint(point);
+ return this;
+ }
+
+ public PolygonOptions add(LatLng... points) {
+ for (LatLng point : points) {
+ add(point);
+ }
+ return this;
+ }
+
+ public PolygonOptions addAll(Iterable<LatLng> points) {
+ for (LatLng point : points) {
+ add(point);
+ }
+ return this;
+ }
+
+ public PolygonOptions alpha(float alpha) {
+ polygon.setAlpha(alpha);
+ return this;
+ }
+
+ public float getAlpha() {
+ return polygon.getAlpha();
+ }
+
+ /**
+ * Sets the color of the polygon.
+ *
+ * @param color - the color in ARGB format
+ * @return PolygonOptions - the options object
+ */
+ public PolygonOptions fillColor(int color) {
+ polygon.setFillColor(color);
+ return this;
+ }
+
+ public int getFillColor() {
+ return polygon.getFillColor();
+ }
+
+ /**
+ * Do not use this method. Used internally by the SDK.
+ */
+ public Polygon getPolygon() {
+ return polygon;
+ }
+
+ /**
+ * Sets the color of the stroke of the polygon.
+ *
+ * @param color - the color in ARGB format
+ * @return PolygonOptions - the options object
+ */
+ public PolygonOptions strokeColor(int color) {
+ polygon.setStrokeColor(color);
+ return this;
+ }
+
+ public int getStrokeColor() {
+ return polygon.getStrokeColor();
+ }
+
+ public List<LatLng> getPoints() {
+ // the getter gives us a copy, which is the safe thing to do...
+ return polygon.getPoints();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PolygonOptions polygon = (PolygonOptions) o;
+
+ if (Float.compare(polygon.getAlpha(), getAlpha()) != 0) return false;
+ if (getFillColor() != polygon.getFillColor()) return false;
+ if (getStrokeColor() != polygon.getStrokeColor()) return false;
+ return !(getPoints() != null ? !getPoints().equals(polygon.getPoints()) : polygon.getPoints() != null);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + (getAlpha() != +0.0f ? Float.floatToIntBits(getAlpha()) : 0);
+ result = 31 * result + getFillColor();
+ result = 31 * result + getStrokeColor();
+ result = 31 * result + (getPoints() != null ? getPoints().hashCode() : 0);
+ return result;
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polyline.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polyline.java
new file mode 100644
index 0000000000..331974c9c6
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polyline.java
@@ -0,0 +1,39 @@
+package com.mapbox.mapboxsdk.annotations;
+
+import android.graphics.Color;
+
+public final class Polyline extends MultiPoint {
+
+ private int color = Color.BLACK; // default color is black
+ private float width = 10; // As specified by Google API Docs (in pixels)
+
+ Polyline() {
+ super();
+ }
+
+ public int getColor() {
+ return color;
+ }
+
+ public float getWidth() {
+ return width;
+ }
+
+ /**
+ * Sets the color of the polyline.
+ *
+ * @param color - the color in ARGB format
+ */
+ void setColor(int color) {
+ this.color = color;
+ }
+
+ /**
+ * Sets the width of the polyline.
+ *
+ * @param width in pixels
+ */
+ void setWidth(float width) {
+ this.width = width;
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolylineOptions.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolylineOptions.java
new file mode 100644
index 0000000000..089646696d
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolylineOptions.java
@@ -0,0 +1,106 @@
+package com.mapbox.mapboxsdk.annotations;
+
+import com.mapbox.mapboxsdk.geometry.LatLng;
+
+import java.util.List;
+
+public final class PolylineOptions {
+
+ private Polyline polyline;
+
+ public PolylineOptions() {
+ polyline = new Polyline();
+ }
+
+ public PolylineOptions add(LatLng point) {
+ polyline.addPoint(point);
+ return this;
+ }
+
+ public PolylineOptions add(LatLng... points) {
+ for (LatLng point : points) {
+ add(point);
+ }
+ return this;
+ }
+
+ public PolylineOptions addAll(Iterable<LatLng> points) {
+ for (LatLng point : points) {
+ add(point);
+ }
+ return this;
+ }
+
+ public PolylineOptions alpha(float alpha) {
+ polyline.setAlpha(alpha);
+ return this;
+ }
+
+ public float getAlpha() {
+ return polyline.getAlpha();
+ }
+
+ /**
+ * Sets the color of the polyline.
+ *
+ * @param color - the color in ARGB format
+ */
+ public PolylineOptions color(int color) {
+ polyline.setColor(color);
+ return this;
+ }
+
+ public int getColor() {
+ return polyline.getColor();
+ }
+
+ /**
+ * Do not use this method. Used internally by the SDK.
+ */
+ public Polyline getPolyline() {
+ return polyline;
+ }
+
+ public float getWidth() {
+ return polyline.getWidth();
+ }
+
+ /**
+ * Sets the width of the polyline.
+ *
+ * @param width in pixels
+ * @return a new PolylineOptions
+ */
+ public PolylineOptions width(float width) {
+ polyline.setWidth(width);
+ return this;
+ }
+
+ public List<LatLng> getPoints() {
+ // the getter gives us a copy, which is the safe thing to do...
+ return polyline.getPoints();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PolylineOptions polyline = (PolylineOptions) o;
+
+ if (Float.compare(polyline.getAlpha(), getAlpha()) != 0) return false;
+ if (getColor() != polyline.getColor()) return false;
+ if (Float.compare(polyline.getWidth(), getWidth()) != 0) return false;
+ return !(getPoints() != null ? !getPoints().equals(polyline.getPoints()) : polyline.getPoints() != null);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + (getAlpha() != +0.0f ? Float.floatToIntBits(getAlpha()) : 0);
+ result = 31 * result + getColor();
+ result = 31 * result + (getWidth() != +0.0f ? Float.floatToIntBits(getWidth()) : 0);
+ result = 31 * result + (getPoints() != null ? getPoints().hashCode() : 0);
+ return result;
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Sprite.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Sprite.java
new file mode 100644
index 0000000000..597c196d2a
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Sprite.java
@@ -0,0 +1,40 @@
+package com.mapbox.mapboxsdk.annotations;
+
+import android.graphics.Bitmap;
+
+public final class Sprite {
+ private Bitmap mBitmap;
+ private String mId;
+
+ Sprite(String id, Bitmap bitmap) {
+ mId = id;
+ mBitmap = bitmap;
+ }
+
+ public String getId() {
+ return mId;
+ }
+
+ public Bitmap getBitmap() {
+ return mBitmap;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Sprite sprite = (Sprite) o;
+
+ if (!mBitmap.equals(sprite.mBitmap)) return false;
+ return mId.equals(sprite.mId);
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mBitmap.hashCode();
+ result = 31 * result + mId.hashCode();
+ return result;
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/SpriteFactory.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/SpriteFactory.java
new file mode 100644
index 0000000000..78f7673f89
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/SpriteFactory.java
@@ -0,0 +1,132 @@
+package com.mapbox.mapboxsdk.annotations;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+import com.mapbox.mapboxsdk.R;
+import com.mapbox.mapboxsdk.exceptions.TooManySpritesException;
+import com.mapbox.mapboxsdk.views.MapView;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+public final class SpriteFactory {
+
+ private static final String SPRITE_ID_PREFIX = "com.mapbox.sprites.sprite_";
+
+ private MapView mMapView;
+ private Sprite mDefaultMarker;
+ private BitmapFactory.Options mOptions;
+
+ private int mNextId = 0;
+
+ public SpriteFactory(MapView mapView) {
+ mMapView = mapView;
+ DisplayMetrics realMetrics = null;
+ DisplayMetrics metrics = new DisplayMetrics();
+ WindowManager wm = (WindowManager) mMapView.getContext().getSystemService(Context.WINDOW_SERVICE);
+
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ realMetrics = new DisplayMetrics();
+ wm.getDefaultDisplay().getRealMetrics(realMetrics);
+ }
+ wm.getDefaultDisplay().getMetrics(metrics);
+
+ mOptions = new BitmapFactory.Options();
+ mOptions.inScaled = true;
+ mOptions.inDensity = DisplayMetrics.DENSITY_DEFAULT;
+ mOptions.inTargetDensity = metrics.densityDpi;
+ if (realMetrics != null) {
+ mOptions.inScreenDensity = realMetrics.densityDpi;
+ }
+
+ }
+
+ public Sprite fromBitmap(Bitmap bitmap) {
+ if (bitmap == null) {
+ return null;
+ }
+
+ if (mNextId < 0) {
+ throw new TooManySpritesException();
+ }
+ String id = SPRITE_ID_PREFIX + ++mNextId;
+
+ return new Sprite(id, bitmap);
+ }
+
+ public Sprite fromDrawable(Drawable drawable) {
+ int width = drawable.getIntrinsicWidth();
+ int height = drawable.getIntrinsicHeight();
+
+ return fromDrawable(drawable, width, height);
+ }
+
+
+ public Sprite fromDrawable(Drawable drawable, int width, int height) {
+ if ((width < 0) || (height < 0)) {
+ return null;
+ }
+
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ Rect temp = drawable.getBounds();
+ Rect bounds = new Rect(0, 0, width, height);
+ drawable.setBounds(bounds);
+ drawable.draw(canvas);
+ drawable.setBounds(temp);
+
+ return fromBitmap(bitmap);
+ }
+
+ public Sprite fromResource(int resourceId) {
+ Bitmap bitmap = BitmapFactory.decodeResource(mMapView.getResources(), resourceId);
+ return fromBitmap(bitmap);
+ }
+
+ public Sprite defaultMarker() {
+ if (mDefaultMarker == null) {
+ mDefaultMarker = fromResource(R.drawable.default_marker);
+ }
+ return mDefaultMarker;
+ }
+
+ private Sprite fromInputStream(InputStream is) {
+ Bitmap bitmap = BitmapFactory.decodeStream(is, null, mOptions);
+ return fromBitmap(bitmap);
+ }
+
+ public Sprite fromAsset(String assetName) {
+ InputStream is;
+ try {
+ is = mMapView.getContext().getAssets().open(assetName);
+ } catch (IOException e) {
+ return null;
+ }
+ return fromInputStream(is);
+ }
+
+ public Sprite fromPath(String absolutePath) {
+ Bitmap bitmap = BitmapFactory.decodeFile(absolutePath, mOptions);
+ return fromBitmap(bitmap);
+ }
+
+ public Sprite fromFile(String fileName) {
+ FileInputStream is;
+ try {
+ is = mMapView.getContext().openFileInput(fileName);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ return fromInputStream(is);
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/package-info.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/package-info.java
new file mode 100644
index 0000000000..575d10c564
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * This package contains classes to add and manage annotations and markers in your map.
+ */
+package com.mapbox.mapboxsdk.annotations;
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeoConstants.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeoConstants.java
new file mode 100644
index 0000000000..9e6812eb6b
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeoConstants.java
@@ -0,0 +1,10 @@
+package com.mapbox.mapboxsdk.constants;
+
+public class GeoConstants {
+ // http://en.wikipedia.org/wiki/Earth_radius#Equatorial_radius
+ public static final int RADIUS_EARTH_METERS = 6378137;
+ public static final double MIN_LATITUDE = -85.05112878;
+ public static final double MAX_LATITUDE = 85.05112878;
+ public static final double MIN_LONGITUDE = -180;
+ public static final double MAX_LONGITUDE = 180;
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java
new file mode 100644
index 0000000000..3cd542bb17
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java
@@ -0,0 +1,13 @@
+package com.mapbox.mapboxsdk.constants;
+
+import java.util.Locale;
+
+public class MapboxConstants {
+
+ // Default Locale for data processing (ex: String.toLowerCase(MAPBOX_LOCALE, "foo"))
+ public static final Locale MAPBOX_LOCALE = Locale.US;
+
+ // Key used to store access token in AndroidManifest.xml
+ public static final String KEY_META_DATA_MANIFEST = "com.mapbox.AccessToken";
+
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MathConstants.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MathConstants.java
new file mode 100644
index 0000000000..a875fada22
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MathConstants.java
@@ -0,0 +1,8 @@
+package com.mapbox.mapboxsdk.constants;
+
+public class MathConstants {
+ public static final double DEG2RAD = (Math.PI / 180.0);
+ public static final double RAD2DEG = (180.0 / Math.PI);
+
+ public static final double PI = Math.PI;
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/Style.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/Style.java
new file mode 100644
index 0000000000..e2dad97832
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/Style.java
@@ -0,0 +1,54 @@
+package com.mapbox.mapboxsdk.constants;
+
+import android.support.annotation.StringDef;
+
+import com.mapbox.mapboxsdk.views.MapView;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+/**
+* Style provides URLs to several professional styles designed by Mapbox.
+* <p/>
+* These styles are all ready to go in your app. To load one, pass it into {@link MapView#setStyleUrl(String)}
+*
+* @see MapView#setStyleUrl(String)
+*/
+public class Style {
+
+ /**
+ * Indicates the parameter accepts one of the values from {@link Style}.
+ */
+ @StringDef({MAPBOX_STREETS, EMERALD, LIGHT, DARK, SATELLITE, SATELLITE_STREETS})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StyleUrl {
+ }
+
+ // IMPORTANT: If you change any of these you also need to edit them in strings.xml
+
+ /**
+ * Mapbox Streets: A complete basemap, perfect for incorporating your own data.
+ */
+ public static final String MAPBOX_STREETS = "asset://styles/streets-v8.json";
+ /**
+ * Emerald: A versatile style, with emphasis on road networks and public transit.
+ */
+ public static final String EMERALD = "asset://styles/emerald-v8.json";
+ /**
+ * Light: Subtle light backdrop for data visualizations.
+ */
+ public static final String LIGHT = "asset://styles/light-v8.json";
+ /**
+ * Dark: Subtle dark backdrop for data visualizations.
+ */
+ public static final String DARK = "asset://styles/dark-v8.json";
+ /**
+ * Satellite: A beautiful global satellite and aerial imagery layer.
+ */
+ public static final String SATELLITE = "asset://styles/satellite-v8.json";
+
+ /**
+ * Satellite Streets: Global satellite and aerial imagery with unobtrusive labels.
+ */
+ public static final String SATELLITE_STREETS = "asset://styles/satellite-hybrid-v8.json";
+
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/package-info.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/package-info.java
new file mode 100644
index 0000000000..b99119db0a
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * This package provides access to several map related constants and bundled styles.
+ */
+package com.mapbox.mapboxsdk.constants;
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidAccessTokenException.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidAccessTokenException.java
new file mode 100644
index 0000000000..af5a8525a5
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidAccessTokenException.java
@@ -0,0 +1,19 @@
+package com.mapbox.mapboxsdk.exceptions;
+
+import android.os.Bundle;
+import com.mapbox.mapboxsdk.views.MapView;
+
+/**
+ * A {@code InvalidAccessTokenException} is thrown by {@link MapView} when there is either no access
+ * token set before {@link MapView#onCreate(Bundle)} or an invalid access token is set in {@link MapView#setAccessToken(String)}
+ *
+ * @see MapView#onCreate(Bundle)
+ * @see MapView#setAccessToken(String)
+ */
+public class InvalidAccessTokenException extends RuntimeException {
+
+ public InvalidAccessTokenException() {
+ super("Using MapView requires setting a valid access token. See the README.md");
+ }
+
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/SpriteBitmapChangedException.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/SpriteBitmapChangedException.java
new file mode 100644
index 0000000000..15c6d7eec6
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/SpriteBitmapChangedException.java
@@ -0,0 +1,26 @@
+package com.mapbox.mapboxsdk.exceptions;
+
+import android.graphics.Bitmap;
+
+import com.mapbox.mapboxsdk.annotations.Marker;
+import com.mapbox.mapboxsdk.annotations.Sprite;
+import com.mapbox.mapboxsdk.views.MapView;
+
+/**
+ * A {@code SpriteBitmapChangedException} is thrown by {@link MapView} when a {@link Marker} is added
+ * that has a {@link Sprite} with a {@link Bitmap} that has been modified.
+ * <p/>
+ * You cannot modify a {@code Sprite} after it has been added to the map in a {@code Marker}
+ *
+ * @see MapView
+ * @see Sprite
+ * @see Marker
+ */
+public class SpriteBitmapChangedException extends RuntimeException {
+
+ public SpriteBitmapChangedException() {
+ super("The added Marker has a Sprite with a Bitmap that has been modified. You cannot modufy" +
+ "a Sprite after it has been added in a Marker.");
+ }
+
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/TooManySpritesException.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/TooManySpritesException.java
new file mode 100644
index 0000000000..02a57ba225
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/TooManySpritesException.java
@@ -0,0 +1,20 @@
+package com.mapbox.mapboxsdk.exceptions;
+
+import com.mapbox.mapboxsdk.annotations.Sprite;
+import com.mapbox.mapboxsdk.annotations.SpriteFactory;
+
+/**
+ * A {@code TooManySpritesException} is thrown by {@link SpriteFactory} when it
+ * cannot create a {@link Sprite} because there are already too many.
+ * <p/>
+ * You should try to reuse Sprite objects whenever possible.
+ *
+ * @see SpriteFactory
+ */
+public class TooManySpritesException extends RuntimeException {
+
+ public TooManySpritesException() {
+ super("Cannot create a Sprite because there are already too many. Try reusing Sprites.");
+ }
+
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/package-info.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/package-info.java
new file mode 100644
index 0000000000..d593884ee3
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * This package contains exceptions thrown in this SDK.
+ */
+package com.mapbox.mapboxsdk.exceptions;
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/BoundingBox.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/BoundingBox.java
new file mode 100644
index 0000000000..e778e30aad
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/BoundingBox.java
@@ -0,0 +1,304 @@
+package com.mapbox.mapboxsdk.geometry;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * A rectangular geographical area defined in latitude and longitude units.
+ */
+public final class BoundingBox implements Parcelable, Serializable {
+
+ private final double mLatNorth;
+ private final double mLatSouth;
+ private final double mLonEast;
+ private final double mLonWest;
+
+ private final boolean mIsValid;
+
+ /**
+ * Construct a new bounding box based on its corners, given in NESW
+ * order.
+ *
+ * @param northLatitude Northern Latitude
+ * @param eastLongitude Eastern Longitude
+ * @param southLatitude Southern Latitude
+ * @param westLongitude Western Longitude
+ */
+ public BoundingBox(final double northLatitude, final double eastLongitude, final double southLatitude, final double westLongitude) {
+ this.mLatNorth = northLatitude;
+ this.mLonEast = eastLongitude;
+ this.mLatSouth = southLatitude;
+ this.mLonWest = westLongitude;
+ this.mIsValid = ((this.mLonWest < this.mLonEast) && (this.mLatNorth > this.mLatSouth));
+ }
+
+ /**
+ * Construct a new bounding box based on its corners, given in NESW order.
+ *
+ * @param northEast Coordinate
+ * @param southWest Coordinate
+ */
+ public BoundingBox(final LatLng northEast, final LatLng southWest) {
+ this(northEast.getLatitude(), northEast.getLongitude(), southWest.getLatitude(), southWest.getLongitude());
+ }
+
+ /**
+ * Create a bounding box from another bounding box
+ *
+ * @param other the other bounding box
+ */
+ public BoundingBox(final BoundingBox other) {
+ this.mLatNorth = other.getLatNorth();
+ this.mLonEast = other.getLonEast();
+ this.mLatSouth = other.getLatSouth();
+ this.mLonWest = other.getLonWest();
+ this.mIsValid = other.isValid();
+ }
+
+ /**
+ * Create a new BoundingBox with no size centered at 0, 0, also known as null island
+ */
+ public BoundingBox() {
+ this(0, 0, 0, 0);
+ }
+
+ /**
+ * Calculates the centerpoint of this bounding box by simple interpolation and returns
+ * it as a point. This is a non-geodesic calculation which is not the geographic center.
+ *
+ * @return LatLng center of this BoundingBox
+ */
+ public LatLng getCenter() {
+ return new LatLng((this.mLatNorth + this.mLatSouth) / 2,
+ (this.mLonEast + this.mLonWest) / 2);
+ }
+
+ public double getLatNorth() {
+ return this.mLatNorth;
+ }
+
+ public double getLatSouth() {
+ return this.mLatSouth;
+ }
+
+ public double getLonEast() {
+ return this.mLonEast;
+ }
+
+ public double getLonWest() {
+ return this.mLonWest;
+ }
+
+ public boolean isValid() {
+ return this.mIsValid;
+ }
+
+ /**
+ * Get the area spanned by this bounding box
+ *
+ * @return CoordinateSpan area
+ */
+ public CoordinateSpan getSpan() {
+ return new CoordinateSpan(getLatitudeSpan(), getLongitudeSpan());
+ }
+
+ /**
+ * Get the absolute distance, in degrees, between the north and
+ * south boundaries of this bounding box
+ *
+ * @return Span distance
+ */
+ public double getLatitudeSpan() {
+ return Math.abs(this.mLatNorth - this.mLatSouth);
+ }
+
+ /**
+ * Get the absolute distance, in degrees, between the west and
+ * east boundaries of this bounding box
+ *
+ * @return Span distance
+ */
+ public double getLongitudeSpan() {
+ return Math.abs(this.mLonEast - this.mLonWest);
+ }
+
+
+ /**
+ * Validate if bounding box is empty, determined if absolute distance is
+ *
+ * @return boolean indicating if span is empty
+ */
+ public boolean isEmpty() {
+ return getLongitudeSpan() == 0.0 || getLatitudeSpan() == 0.0;
+ }
+
+ @Override
+ public String toString() {
+ return "N:" + this.mLatNorth + "; E:" + this.mLonEast + "; S:" + this.mLatSouth + "; W:" + this.mLonWest;
+ }
+
+ /**
+ * Constructs a bounding box that contains all of a list of LatLng
+ * objects. Empty lists will yield invalid bounding boxes.
+ *
+ * @param latLngs List of LatLng objects
+ * @return BoundingBox
+ */
+ public static BoundingBox fromLatLngs(final List<? extends ILatLng> latLngs) {
+ double minLat = 90,
+ minLon = 180,
+ maxLat = -90,
+ maxLon = -180;
+
+ for (final ILatLng gp : latLngs) {
+ final double latitude = gp.getLatitude();
+ final double longitude = gp.getLongitude();
+
+ minLat = Math.min(minLat, latitude);
+ minLon = Math.min(minLon, longitude);
+ maxLat = Math.max(maxLat, latitude);
+ maxLon = Math.max(maxLon, longitude);
+ }
+
+ return new BoundingBox(maxLat, maxLon, minLat, minLon);
+ }
+
+ /**
+ * Determines whether this bounding box matches another one via coordinates.
+ *
+ * @param o another object
+ * @return a boolean indicating whether the bounding boxes are equal
+ */
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o instanceof BoundingBox) {
+ BoundingBox other = (BoundingBox) o;
+ return mLatNorth == other.getLatNorth()
+ && mLatSouth == other.getLatSouth()
+ && mLonEast == other.getLonEast()
+ && mLonWest == other.getLonWest();
+ }
+ return false;
+ }
+
+ /**
+ * Determines whether this bounding box contains a point and the point
+ * does not touch its boundary.
+ *
+ * @param pGeoPoint the point which may be contained
+ * @return true, if the point is contained within the box.
+ */
+ public boolean contains(final ILatLng pGeoPoint) {
+ final double latitude = pGeoPoint.getLatitude();
+ final double longitude = pGeoPoint.getLongitude();
+ return ((latitude < this.mLatNorth)
+ && (latitude > this.mLatSouth))
+ && ((longitude < this.mLonEast)
+ && (longitude > this.mLonWest));
+ }
+
+ /**
+ * Returns a new BoundingBox that stretches to contain both this and another BoundingBox.
+ *
+ * @param box BoundingBox to add
+ * @return BoundingBox
+ */
+ public BoundingBox union(BoundingBox box) {
+ return union(box.getLatNorth(), box.getLonEast(), box.getLatSouth(), box.getLonWest());
+ }
+
+ /**
+ * Returns a new BoundingBox that stretches to include another bounding box,
+ * given by corner points.
+ *
+ * @param lonNorth Northern Longitude
+ * @param latEast Eastern Latitude
+ * @param lonSouth Southern Longitude
+ * @param latWest Western Longitude
+ * @return BoundingBox
+ */
+ public BoundingBox union(final double lonNorth, final double latEast, final double lonSouth, final double latWest) {
+ return new BoundingBox((this.mLatNorth < lonNorth) ? lonNorth : this.mLatNorth,
+ (this.mLonEast < latEast) ? latEast : this.mLonEast,
+ (this.mLatSouth > lonSouth) ? lonSouth : this.mLatSouth,
+ (this.mLonWest > latWest) ? latWest : this.mLonWest);
+ }
+
+ /**
+ * Returns a new BoundingBox that is the intersection of this with another box
+ *
+ * @param box BoundingBox to intersect with
+ * @return BoundingBox
+ */
+ public BoundingBox intersect(BoundingBox box) {
+ double minLatWest = Math.max(getLonWest(), box.getLonWest());
+ double maxLatEast = Math.min(getLonEast(), box.getLonEast());
+ if (maxLatEast > minLatWest) {
+ double minLonSouth = Math.max(getLatSouth(), box.getLatSouth());
+ double maxLonNorth = Math.min(getLatNorth(), box.getLatNorth());
+ if (maxLonNorth > minLonSouth) {
+ return new BoundingBox(maxLonNorth, maxLatEast, minLonSouth, minLatWest);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a new BoundingBox that is the intersection of this with another box
+ *
+ * @param northLongitude Northern Longitude
+ * @param eastLatitude Eastern Latitude
+ * @param southLongitude Southern Longitude
+ * @param westLatitude Western Latitude
+ * @return BoundingBox
+ */
+ public BoundingBox intersect(double northLongitude, double eastLatitude, double southLongitude, double westLatitude) {
+ return intersect(new BoundingBox(northLongitude, eastLatitude, southLongitude, westLatitude));
+ }
+
+ public static final Parcelable.Creator<BoundingBox> CREATOR =
+ new Parcelable.Creator<BoundingBox>() {
+ @Override
+ public BoundingBox createFromParcel(final Parcel in) {
+ return readFromParcel(in);
+ }
+
+ @Override
+ public BoundingBox[] newArray(final int size) {
+ return new BoundingBox[size];
+ }
+ };
+
+ @Override
+ public int hashCode() {
+ return (int) ((mLatNorth + 90)
+ + ((mLatSouth + 90) * 1000)
+ + ((mLonEast + 180) * 1000000)
+ + ((mLonEast + 180) * 1000000000));
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(final Parcel out, final int arg1) {
+ out.writeDouble(this.mLatNorth);
+ out.writeDouble(this.mLonEast);
+ out.writeDouble(this.mLatSouth);
+ out.writeDouble(this.mLonWest);
+ }
+
+ private static BoundingBox readFromParcel(final Parcel in) {
+ final double lonNorth = in.readDouble();
+ final double latEast = in.readDouble();
+ final double lonSouth = in.readDouble();
+ final double latWest = in.readDouble();
+ return new BoundingBox(lonNorth, latEast, lonSouth, latWest);
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateBounds.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateBounds.java
new file mode 100644
index 0000000000..e5b4b2fdcc
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateBounds.java
@@ -0,0 +1,58 @@
+package com.mapbox.mapboxsdk.geometry;
+
+/**
+ * Implementation of iOS MGLCoordinateBounds
+ */
+public class CoordinateBounds {
+
+ private LatLng southWest;
+ private LatLng northEast;
+
+ public CoordinateBounds(LatLng southWest, LatLng northEast) {
+ this.southWest = southWest;
+ this.northEast = northEast;
+ }
+
+ public LatLng getSouthWest() {
+ return southWest;
+ }
+
+ public void setSouthWest(LatLng southWest) {
+ this.southWest = southWest;
+ }
+
+ public LatLng getNorthEast() {
+ return northEast;
+ }
+
+ public void setNorthEast(LatLng northEast) {
+ this.northEast = northEast;
+ }
+
+ @Override
+ public int hashCode() {
+ int result;
+ long temp;
+ temp = southWest.hashCode();
+ result = (int) (temp ^ (temp >>> 32));
+ temp = northEast.hashCode();
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o instanceof CoordinateBounds) {
+ CoordinateBounds other = (CoordinateBounds) o;
+ return getNorthEast().equals(other.getNorthEast())
+ && getSouthWest() == other.getSouthWest();
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "CoordinateBounds [northEast[" + getNorthEast() + "], southWest[]" + getSouthWest() + "]";
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateRegion.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateRegion.java
new file mode 100644
index 0000000000..a70bb05a41
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateRegion.java
@@ -0,0 +1,30 @@
+package com.mapbox.mapboxsdk.geometry;
+
+/**
+ * Implementation of iOS MKCoordinateRegion
+ */
+public class CoordinateRegion {
+ private LatLng center;
+ private CoordinateSpan span;
+
+ public CoordinateRegion(final LatLng center, final CoordinateSpan span) {
+ this.center = center;
+ this.span = span;
+ }
+
+ public LatLng getCenter() {
+ return center;
+ }
+
+ public void setCenter(final LatLng center) {
+ this.center = center;
+ }
+
+ public CoordinateSpan getSpan() {
+ return span;
+ }
+
+ public void setSpan(final CoordinateSpan span) {
+ this.span = span;
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateSpan.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateSpan.java
new file mode 100644
index 0000000000..79361f725a
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/CoordinateSpan.java
@@ -0,0 +1,43 @@
+package com.mapbox.mapboxsdk.geometry;
+
+/**
+ * Implementation of iOS MKCoordinateSpan
+ */
+public class CoordinateSpan {
+
+ private double latitudeSpan;
+ private double longitudeSpan;
+
+ public CoordinateSpan(final double latitudeSpan, final double longitudeSpan) {
+ this.latitudeSpan = latitudeSpan;
+ this.longitudeSpan = longitudeSpan;
+ }
+
+ public double getLatitudeSpan() {
+ return latitudeSpan;
+ }
+
+ public void setLatitudeSpan(final double latitudeSpan) {
+ this.latitudeSpan = latitudeSpan;
+ }
+
+ public double getLongitudeSpan() {
+ return longitudeSpan;
+ }
+
+ public void setLongitudeSpan(final double longitudeSpan) {
+ this.longitudeSpan = longitudeSpan;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o instanceof CoordinateSpan) {
+ CoordinateSpan other = (CoordinateSpan) o;
+ return longitudeSpan == other.getLongitudeSpan()
+ && latitudeSpan == other.getLatitudeSpan();
+ }
+ return false;
+ }
+
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ILatLng.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ILatLng.java
new file mode 100644
index 0000000000..892d0ad4ae
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ILatLng.java
@@ -0,0 +1,12 @@
+package com.mapbox.mapboxsdk.geometry;
+
+/**
+ * A Latitude, Longitude point.
+ */
+public interface ILatLng {
+ double getLatitude();
+
+ double getLongitude();
+
+ double getAltitude();
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/IProjectedMeters.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/IProjectedMeters.java
new file mode 100644
index 0000000000..5aa5f607eb
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/IProjectedMeters.java
@@ -0,0 +1,7 @@
+package com.mapbox.mapboxsdk.geometry;
+
+public interface IProjectedMeters {
+ double getNorthing();
+
+ double getEasting();
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLng.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLng.java
new file mode 100644
index 0000000000..b2a0a02b15
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLng.java
@@ -0,0 +1,176 @@
+package com.mapbox.mapboxsdk.geometry;
+
+import android.location.Location;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.mapbox.mapboxsdk.constants.GeoConstants;
+import com.mapbox.mapboxsdk.constants.MathConstants;
+
+import java.io.Serializable;
+
+public class LatLng implements ILatLng, Parcelable, Serializable {
+
+ public static final Parcelable.Creator<LatLng> CREATOR = new Parcelable.Creator<LatLng>() {
+ public LatLng createFromParcel(Parcel in) {
+ return new LatLng(in);
+ }
+
+ public LatLng[] newArray(int size) {
+ return new LatLng[size];
+ }
+ };
+
+ private double latitude;
+ private double longitude;
+ private double altitude = 0.0;
+
+ /**
+ * Construct a new latitude, longitude point at (0, 0)
+ */
+ public LatLng() {
+ this.latitude = 0.0;
+ this.longitude = 0.0;
+ }
+
+ /**
+ * Construct a new latitude, longitude point given float arguments
+ * @param latitude Latitude in degrees
+ * @param longitude Longitude in degrees
+ */
+ public LatLng(double latitude, double longitude) {
+ this.latitude = latitude;
+ this.longitude = longitude;
+ }
+
+ /**
+ * Construct a new latitude, longitude, altitude point given float arguments
+ * @param latitude Latitude in degrees
+ * @param longitude Longitude in degress
+ * @param altitude Altitude in meters
+ */
+ public LatLng(double latitude, double longitude, double altitude) {
+ this.latitude = latitude;
+ this.longitude = longitude;
+ this.altitude = altitude;
+ }
+
+ /**
+ * Transform a Location into a LatLng point
+ * @param location Android Location
+ */
+ public LatLng(Location location) {
+ this(location.getLatitude(), location.getLongitude(), location.getAltitude());
+ }
+
+ /**
+ * Clone an existing latitude longitude point
+ * @param aLatLng LatLng
+ */
+ public LatLng(LatLng aLatLng) {
+ this.latitude = aLatLng.latitude;
+ this.longitude = aLatLng.longitude;
+ this.altitude = aLatLng.altitude;
+ }
+
+ protected LatLng(Parcel in) {
+ latitude = in.readDouble();
+ longitude = in.readDouble();
+ altitude = in.readDouble();
+ }
+
+ public void setLatitude(double latitude) {
+ this.latitude = latitude;
+ }
+
+ @Override
+ public double getLatitude() {
+ return latitude;
+ }
+
+ public void setLongitude(double longitude) {
+ this.longitude = longitude;
+ }
+
+ @Override
+ public double getLongitude() {
+ return longitude;
+ }
+
+ public void setAltitude(double altitude) {
+ this.altitude = altitude;
+ }
+
+ @Override
+ public double getAltitude() {
+ return altitude;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ LatLng latLng = (LatLng) o;
+
+ return Double.compare(latLng.altitude, altitude) == 0 && Double.compare(latLng.latitude, latitude) == 0 && Double.compare(latLng.longitude, longitude) == 0;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result;
+ long temp;
+ temp = Double.doubleToLongBits(latitude);
+ result = (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(longitude);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(altitude);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "LatLng [longitude=" + longitude + ", latitude=" + latitude + ", altitude=" + altitude + "]";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeDouble(latitude);
+ out.writeDouble(longitude);
+ out.writeDouble(altitude);
+ }
+
+ /**
+ * Calculate distance between two points
+ * @param other Other LatLng to compare to
+ * @return distance in meters
+ */
+ public double distanceTo(LatLng other) {
+
+ final double a1 = MathConstants.DEG2RAD * this.latitude;
+ final double a2 = MathConstants.DEG2RAD * this.longitude;
+ final double b1 = MathConstants.DEG2RAD * other.getLatitude();
+ final double b2 = MathConstants.DEG2RAD * other.getLongitude();
+
+ final double cosa1 = Math.cos(a1);
+ final double cosb1 = Math.cos(b1);
+
+ final double t1 = cosa1 * Math.cos(a2) * cosb1 * Math.cos(b2);
+ final double t2 = cosa1 * Math.sin(a2) * cosb1 * Math.sin(b2);
+ final double t3 = Math.sin(a1) * Math.sin(b1);
+ final double tt = Math.acos(t1 + t2 + t3);
+
+ return GeoConstants.RADIUS_EARTH_METERS * tt;
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngZoom.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngZoom.java
new file mode 100644
index 0000000000..9e453a391c
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngZoom.java
@@ -0,0 +1,93 @@
+package com.mapbox.mapboxsdk.geometry;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.Serializable;
+
+public class LatLngZoom extends LatLng implements Parcelable, Serializable {
+
+ public static final Parcelable.Creator<LatLngZoom> CREATOR = new Parcelable.Creator<LatLngZoom>() {
+ public LatLngZoom createFromParcel(Parcel in) {
+ return new LatLngZoom(in);
+ }
+
+ public LatLngZoom[] newArray(int size) {
+ return new LatLngZoom[size];
+ }
+ };
+
+ private double zoom;
+
+ public LatLngZoom() {
+ super();
+ this.zoom = 0.0;
+ }
+
+ public LatLngZoom(double latitude, double longitude, double zoom) {
+ super(latitude, longitude);
+ this.zoom = zoom;
+ }
+
+ public LatLngZoom(LatLng latLng, double zoom) {
+ super(latLng.getLatitude(), latLng.getLongitude());
+ this.zoom = zoom;
+ }
+
+ private LatLngZoom(Parcel in) {
+ super(in);
+ zoom = in.readDouble();
+ }
+
+ public double getZoom() {
+ return zoom;
+ }
+
+ public void setZoom(double zoom) {
+ this.zoom = zoom;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ LatLngZoom that = (LatLngZoom) o;
+
+ return Double.compare(that.zoom, zoom) == 0;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ long temp;
+ temp = Double.doubleToLongBits(zoom);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "LatLngZoom [latitude=" + super.getLatitude() + ", longitude=" + super.getLongitude() + ", altitude=" + super.getAltitude() + ", zoom=" + zoom + "]";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeDouble(zoom);
+ }
+
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ProjectedMeters.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ProjectedMeters.java
new file mode 100644
index 0000000000..be4819a74e
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ProjectedMeters.java
@@ -0,0 +1,89 @@
+package com.mapbox.mapboxsdk.geometry;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.Serializable;
+
+public class ProjectedMeters implements IProjectedMeters, Parcelable, Serializable {
+
+ public static final Creator<ProjectedMeters> CREATOR = new Creator<ProjectedMeters>() {
+ public ProjectedMeters createFromParcel(Parcel in) {
+ return new ProjectedMeters(in);
+ }
+
+ public ProjectedMeters[] newArray(int size) {
+ return new ProjectedMeters[size];
+ }
+ };
+
+ private double northing;
+ private double easting;
+
+ public ProjectedMeters(double northing, double easting) {
+ this.northing = northing;
+ this.easting = easting;
+ }
+
+ public ProjectedMeters(ProjectedMeters aProjectedMeters) {
+ this.northing = aProjectedMeters.northing;
+ this.easting = aProjectedMeters.easting;
+ }
+
+ protected ProjectedMeters(Parcel in) {
+ northing = in.readDouble();
+ easting = in.readDouble();
+ }
+
+ @Override
+ public double getNorthing() {
+ return northing;
+ }
+
+ @Override
+ public double getEasting() {
+ return easting;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ ProjectedMeters projectedMeters = (ProjectedMeters) o;
+
+ return Double.compare(projectedMeters.easting, easting) == 0 && Double.compare(projectedMeters.northing, northing) == 0;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result;
+ long temp;
+ temp = Double.doubleToLongBits(easting);
+ result = (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(northing);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "ProjectedMeters [northing=" + northing + ", easting=" + easting + "]";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeDouble(northing);
+ out.writeDouble(easting);
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/package-info.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/package-info.java
new file mode 100644
index 0000000000..bf7dae12a8
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains classes that deal with geometry and map coordinates. Many SDK functions
+ * accept or return these classes.
+ */
+package com.mapbox.mapboxsdk.geometry;
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HTTPContext.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HTTPContext.java
new file mode 100644
index 0000000000..358af8997c
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HTTPContext.java
@@ -0,0 +1,104 @@
+package com.mapbox.mapboxsdk.http;
+
+import com.mapbox.mapboxsdk.constants.MapboxConstants;
+import com.squareup.okhttp.Call;
+import com.squareup.okhttp.Callback;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.ProtocolException;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+
+import javax.net.ssl.SSLException;
+
+class HTTPContext {
+
+ private static final int CONNECTION_ERROR = 0;
+ private static final int TEMPORARY_ERROR = 1;
+ private static final int PERMANENT_ERROR = 2;
+ private static final int CANCELED_ERROR = 3;
+
+ private static HTTPContext mInstance = null;
+
+ private OkHttpClient mClient;
+
+ private HTTPContext() {
+ super();
+ mClient = new OkHttpClient();
+ }
+
+ public static HTTPContext getInstance() {
+ if (mInstance == null) {
+ mInstance = new HTTPContext();
+ }
+
+ return mInstance;
+ }
+
+ public HTTPRequest createRequest(long nativePtr, String resourceUrl, String userAgent, String etag, String modified) {
+ return new HTTPRequest(nativePtr, resourceUrl, userAgent, etag, modified);
+ }
+
+ public class HTTPRequest implements Callback {
+ private long mNativePtr = 0;
+
+ private Call mCall;
+ private Request mRequest;
+
+ private native void nativeOnFailure(long nativePtr, int type, String message);
+ private native void nativeOnResponse(long nativePtr, int code, String message, String etag, String modified, String cacheControl, String expires, byte[] body);
+
+ private HTTPRequest(long nativePtr, String resourceUrl, String userAgent, String etag, String modified) {
+ mNativePtr = nativePtr;
+ Request.Builder builder = new Request.Builder().url(resourceUrl).tag(resourceUrl.toLowerCase(MapboxConstants.MAPBOX_LOCALE)).addHeader("User-Agent", userAgent);
+ if (etag.length() > 0) {
+ builder = builder.addHeader("If-None-Match", etag);
+ } else if (modified.length() > 0) {
+ builder = builder.addHeader("If-Modified-Since", modified);
+ }
+ mRequest = builder.build();
+ }
+
+ public void start() {
+ mCall = HTTPContext.getInstance().mClient.newCall(mRequest);
+ mCall.enqueue(this);
+ }
+
+ public void cancel() {
+ mCall.cancel();
+ }
+
+ @Override
+ public void onFailure(Request request, IOException e) {
+ int type = PERMANENT_ERROR;
+ if ((e instanceof UnknownHostException) || (e instanceof SocketException) || (e instanceof ProtocolException) || (e instanceof SSLException)) {
+ type = CONNECTION_ERROR;
+ } else if ((e instanceof InterruptedIOException)) {
+ type = TEMPORARY_ERROR;
+ } else if (mCall.isCanceled()) {
+ type = CANCELED_ERROR;
+ }
+
+ nativeOnFailure(mNativePtr, type, e.getMessage());
+ }
+
+ @Override
+ public void onResponse(Response response) throws IOException {
+ byte[] body;
+ try {
+ body = response.body().bytes();
+ } catch (IOException e) {
+ onFailure(mRequest, e);
+ return;
+ } finally {
+ response.body().close();
+ }
+
+ nativeOnResponse(mNativePtr, response.code(), response.message(), response.header("ETag"), response.header("Last-Modified"), response.header("Cache-Control"), response.header("Expires"), body);
+ }
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/package-info.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/package-info.java
new file mode 100644
index 0000000000..a5dbf30463
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Do not use this package. Used internally by the SDK.
+ */
+package com.mapbox.mapboxsdk.http;
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/package-info.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/package-info.java
new file mode 100644
index 0000000000..474a554463
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains the {@link com.mapbox.mapboxsdk.MapFragment} class. {@code MapFragment}
+ * provides a quick and easy way to add a map to your app.
+ */
+package com.mapbox.mapboxsdk;
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ApiAccess.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ApiAccess.java
new file mode 100644
index 0000000000..354bb01855
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ApiAccess.java
@@ -0,0 +1,60 @@
+package com.mapbox.mapboxsdk.utils;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.mapbox.mapboxsdk.R;
+import com.mapbox.mapboxsdk.constants.MapboxConstants;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * {@code ApiAccess} provides a method to load the Mapbox access token.
+ */
+public final class ApiAccess {
+
+ /**
+ * Returns the Mapbox access token set in the app resources.
+ * <p/>
+ * It will first search the application manifest for a {@link MapboxConstants#KEY_META_DATA_MANIFEST}
+ * meta-data value. If not found it will then attempt to load the access token from the
+ * {@code res/raw/token.txt} development file.
+ *
+ * @param context The {@link Context} of the {@link android.app.Activity} or {@link android.app.Fragment}.
+ * @return The Mapbox access token or null if not found.
+ * @see MapboxConstants#KEY_META_DATA_MANIFEST
+ */
+ @Nullable
+ public static String getToken(@NonNull Context context) {
+ String accessToken = getReleaseToken(context);
+ if (TextUtils.isEmpty(accessToken)) {
+ accessToken = getDevelopmentToken(context);
+ }
+ return accessToken;
+ }
+
+ private static String getReleaseToken(@NonNull Context context) {
+ try {
+ PackageManager packageManager = context.getPackageManager();
+ ApplicationInfo appInfo = packageManager.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
+ return appInfo.metaData.getString(MapboxConstants.KEY_META_DATA_MANIFEST);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private static String getDevelopmentToken(@NonNull Context context) {
+ try {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.token)));
+ return reader.readLine();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/package-info.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/package-info.java
new file mode 100644
index 0000000000..9d9c0bf4bb
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains the {@link com.mapbox.mapboxsdk.utils.ApiAccess} class. {@code ApiAccess}
+ * provides a methods to load a Mapbox access token.
+ */
+package com.mapbox.mapboxsdk.utils;
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/CompassView.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/CompassView.java
new file mode 100644
index 0000000000..174f949916
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/CompassView.java
@@ -0,0 +1,151 @@
+package com.mapbox.mapboxsdk.views;
+
+import android.content.Context;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.ViewPropertyAnimatorCompat;
+import android.support.v4.view.ViewPropertyAnimatorListenerAdapter;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import com.mapbox.mapboxsdk.R;
+
+import java.lang.ref.WeakReference;
+import java.util.Timer;
+import java.util.TimerTask;
+
+final class CompassView extends ImageView {
+
+ private Timer mNorthTimer;
+ private double mDirection = 0.0f;
+ private ViewPropertyAnimatorCompat mFadeAnimator;
+
+ public CompassView(Context context) {
+ super(context);
+ initialize(context);
+ }
+
+ public CompassView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialize(context);
+ }
+
+ public CompassView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize(context);
+ }
+
+ private void initialize(Context context) {
+
+ // View configuration
+ setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.compass));
+ setContentDescription(getResources().getString(R.string.compassContentDescription));
+ setEnabled(false);
+
+ // Layout params
+ float mScreenDensity = context.getResources().getDisplayMetrics().density;
+ ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams((int) (48 * mScreenDensity), (int) (48 * mScreenDensity));
+ setLayoutParams(lp);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ if (enabled) {
+ if (mDirection != 0.0) {
+ if (mNorthTimer != null){
+ mNorthTimer.cancel();
+ mNorthTimer = null;
+ }
+ if (mFadeAnimator != null) {
+ mFadeAnimator.cancel();
+ }
+ mFadeAnimator = null;
+ setAlpha(1.0f);
+ setVisibility(View.VISIBLE);
+ }
+ } else {
+ if (mNorthTimer != null){
+ mNorthTimer.cancel();
+ mNorthTimer = null;
+ }
+ if (mFadeAnimator != null) {
+ mFadeAnimator.cancel();
+ }
+ mFadeAnimator = null;
+ setVisibility(View.INVISIBLE);
+ }
+ }
+
+ public void update(double direction) {
+ mDirection = direction;
+ setRotation((float) direction);
+
+ if (!isEnabled()) {
+ return;
+ }
+
+ if (direction == 0.0) {
+ if (getVisibility() == View.INVISIBLE) {
+ return;
+ }
+
+ if (mNorthTimer == null) {
+ if (mFadeAnimator != null) {
+ mFadeAnimator.cancel();
+ }
+ mFadeAnimator = null;
+
+ mNorthTimer = new Timer("CompassView North timer");
+ mNorthTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ setAlpha(1.0f);
+ mFadeAnimator = ViewCompat.animate(CompassView.this).alpha(0.0f).setDuration(1000).withLayer();
+ mFadeAnimator.setListener(new ViewPropertyAnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(View view) {
+ setVisibility(View.INVISIBLE);
+ mNorthTimer = null;
+ }
+ });
+ }
+ });
+ }
+ }, 1000);
+ }
+ } else {
+ if (mNorthTimer != null){
+ mNorthTimer.cancel();
+ mNorthTimer = null;
+ }
+ if (mFadeAnimator != null) {
+ mFadeAnimator.cancel();
+ }
+ setAlpha(1.0f);
+ setVisibility(View.VISIBLE);
+ }
+ }
+
+ public static class CompassClickListener implements View.OnClickListener {
+
+ private WeakReference<MapView> mMapView;
+
+ public CompassClickListener(final MapView mapView) {
+ mMapView = new WeakReference<>(mapView);
+ }
+
+ @Override
+ public void onClick(View v) {
+ final MapView mapView = mMapView.get();
+ if (mapView != null) {
+ mapView.resetNorth();
+ }
+ }
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/MapView.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/MapView.java
new file mode 100644
index 0000000000..e3c3599b68
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/MapView.java
@@ -0,0 +1,3317 @@
+package com.mapbox.mapboxsdk.views;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.Dialog;
+import android.app.Fragment;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.graphics.SurfaceTexture;
+import android.location.Location;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.CallSuper;
+import android.support.annotation.FloatRange;
+import android.support.annotation.IntDef;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.view.GestureDetectorCompat;
+import android.support.v4.view.ScaleGestureDetectorCompat;
+import android.support.v7.app.AlertDialog;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ZoomButtonsController;
+
+import com.almeros.android.multitouch.gesturedetectors.RotateGestureDetector;
+import com.almeros.android.multitouch.gesturedetectors.TwoFingerGestureDetector;
+import com.mapbox.mapboxsdk.R;
+import com.mapbox.mapboxsdk.annotations.Annotation;
+import com.mapbox.mapboxsdk.annotations.Marker;
+import com.mapbox.mapboxsdk.annotations.MarkerOptions;
+import com.mapbox.mapboxsdk.annotations.Polygon;
+import com.mapbox.mapboxsdk.annotations.PolygonOptions;
+import com.mapbox.mapboxsdk.annotations.Polyline;
+import com.mapbox.mapboxsdk.annotations.PolylineOptions;
+import com.mapbox.mapboxsdk.annotations.Sprite;
+import com.mapbox.mapboxsdk.annotations.SpriteFactory;
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.exceptions.InvalidAccessTokenException;
+import com.mapbox.mapboxsdk.exceptions.SpriteBitmapChangedException;
+import com.mapbox.mapboxsdk.geometry.BoundingBox;
+import com.mapbox.mapboxsdk.geometry.CoordinateBounds;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngZoom;
+import com.mapbox.mapboxsdk.utils.ApiAccess;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A {@code MapView} provides an embeddable map interface, similar to the one provided by the Google Maps API.
+ * You use this class to display map information and to manipulate the map contents from your application.
+ * You can center the map on a given coordinate, specify the size of the area you want to display,
+ * and style the features of the map to fit your application's use case.
+ * <p/>
+ * Use of {@code MapView} requires a Mapbox API access token.
+ * Obtain an access token on the <a href="https://www.mapbox.com/account/apps/">Mapbox account page</a>.
+ * <p/>
+ * <strong>Warning:</strong> Please note that you are responsible for getting permission to use the map data,
+ * and for ensuring your use adheres to the relevant terms of use.
+ *
+ * @see MapView#setAccessToken(String)
+ */
+public final class MapView extends FrameLayout {
+
+ //
+ // Static members
+ //
+
+ // Used for logging
+ private static final String TAG = "MapView";
+
+ // Used for animation
+ private static final long ANIMATION_DURATION = 300;
+
+ // Used for saving instance state
+ private static final String STATE_CENTER_COORDINATE = "centerCoordinate";
+ private static final String STATE_CENTER_DIRECTION = "centerDirection";
+ private static final String STATE_ZOOM_LEVEL = "zoomLevel";
+ private static final String STATE_DIRECTION = "direction";
+ private static final String STATE_ZOOM_ENABLED = "zoomEnabled";
+ private static final String STATE_SCROLL_ENABLED = "scrollEnabled";
+ private static final String STATE_ROTATE_ENABLED = "rotateEnabled";
+ private static final String STATE_DEBUG_ACTIVE = "debugActive";
+ private static final String STATE_STYLE_URL = "styleUrl";
+ private static final String STATE_ACCESS_TOKEN = "accessToken";
+ private static final String STATE_STYLE_CLASSES = "styleClasses";
+ private static final String STATE_DEFAULT_TRANSITION_DURATION = "defaultTransitionDuration";
+ private static final String STATE_MY_LOCATION_ENABLED = "myLocationEnabled";
+ private static final String STATE_COMPASS_ENABLED = "compassEnabled";
+ private static final String STATE_COMPASS_GRAVITY = "compassGravity";
+ private static final String STATE_COMPASS_MARGIN_LEFT = "compassMarginLeft";
+ private static final String STATE_COMPASS_MARGIN_TOP = "compassMarginTop";
+ private static final String STATE_COMPASS_MARGIN_RIGHT = "compassMarginRight";
+ private static final String STATE_COMPASS_MARGIN_BOTTOM = "compassMarginBottom";
+ private static final String STATE_LOGO_GRAVITY = "logoGravity";
+ private static final String STATE_LOGO_MARGIN_LEFT = "logoMarginLeft";
+ private static final String STATE_LOGO_MARGIN_TOP = "logoMarginTop";
+ private static final String STATE_LOGO_MARGIN_RIGHT = "logoMarginRight";
+ private static final String STATE_LOGO_MARGIN_BOTTOM = "logoMarginBottom";
+ private static final String STATE_LOGO_VISIBILITY = "logoVisibility";
+ private static final String STATE_ATTRIBUTION_GRAVITY = "attrGravity";
+ private static final String STATE_ATTRIBUTION_MARGIN_LEFT = "attrMarginLeft";
+ private static final String STATE_ATTRIBUTION_MARGIN_TOP = "attrMarginTop";
+ private static final String STATE_ATTRIBUTION_MARGIN_RIGHT = "attrMarginRight";
+ private static final String STATE_ATTRIBUTION_MARGIN_BOTTOM = "atrrMarginBottom";
+ private static final String STATE_ATTRIBUTION_VISIBILITY = "atrrVisibility";
+
+ // Used for positioning views
+ private static final float DIMENSION_SEVEN_DP = 7f;
+ private static final float DIMENSION_TEN_DP = 10f;
+ private static final float DIMENSION_SIXTEEN_DP = 16f;
+ private static final float DIMENSION_SEVENTYSIX_DP = 76f;
+
+ // Used to select "Improve this map" link in the attribution dialog
+ // Index into R.arrays.attribution_links
+ private static final int ATTRIBUTION_INDEX_IMPROVE_THIS_MAP = 2;
+
+ /**
+ * The currently supported maximum zoom level.
+ *
+ * @see MapView#setZoomLevel(double)
+ */
+ public static final double MAXIMUM_ZOOM_LEVEL = 18.0;
+
+ //
+ // Instance members
+ //
+
+ // Used to call JNI NativeMapView
+ private NativeMapView mNativeMapView;
+
+ // Used to track rendering
+ private TextureView mTextureView;
+
+ // Used to handle DPI scaling
+ private float mScreenDensity = 1.0f;
+
+ // Touch gesture detectors
+ private GestureDetectorCompat mGestureDetector;
+ private ScaleGestureDetector mScaleGestureDetector;
+ private RotateGestureDetector mRotateGestureDetector;
+ private boolean mTwoTap = false;
+
+ // Shows zoom buttons
+ private ZoomButtonsController mZoomButtonsController;
+
+ // Used to track trackball long presses
+ private TrackballLongPressTimeOut mCurrentTrackballLongPressTimeOut;
+
+ // Receives changes to network connectivity
+ private ConnectivityReceiver mConnectivityReceiver;
+
+ // Holds the context
+ private Context mContext;
+
+ // Used for user location
+ private UserLocationView mUserLocationView;
+
+ // Used for the compass
+ private CompassView mCompassView;
+
+ // Used for displaying annotations
+ // Every annotation that has been added to the map
+ private final List<Annotation> mAnnotations = new ArrayList<>();
+ private List<Marker> mMarkersNearLastTap = new ArrayList<>();
+ private Marker mSelectedMarker;
+ private InfoWindowAdapter mInfoWindowAdapter;
+ private SpriteFactory mSpriteFactory;
+ private ArrayList<Sprite> mSprites = new ArrayList<>();
+
+ // Used for the Mapbox Logo
+ private ImageView mLogoView;
+
+ // Used for attributions control
+ private ImageView mAttributionsView;
+
+ // Used to manage MapChange event listeners
+ private ArrayList<OnMapChangedListener> mOnMapChangedListener = new ArrayList<>();
+
+ // Used to manage map click event listeners
+ private OnMapClickListener mOnMapClickListener;
+ private OnMapLongClickListener mOnMapLongClickListener;
+
+ // Used to manage fling and scroll event listeners
+ private OnFlingListener mOnFlingListener;
+ private OnScrollListener mOnScrollListener;
+
+ // Used to manage marker click event listeners
+ private OnMarkerClickListener mOnMarkerClickListener;
+ private OnInfoWindowClickListener mOnInfoWindowClickListener;
+
+ // Used to manage FPS change event listeners
+ private OnFpsChangedListener mOnFpsChangedListener;
+
+ //
+ // Properties
+ //
+
+ // These are properties with setters/getters, saved in onSaveInstanceState and XML attributes
+ private boolean mZoomEnabled = true;
+ private boolean mScrollEnabled = true;
+ private boolean mRotateEnabled = true;
+ private String mStyleUrl;
+
+ //
+ // Inner classes
+ //
+
+ //
+ // Enums
+ //
+
+ /**
+ * Map change event types.
+ *
+ * @see MapView.OnMapChangedListener#onMapChanged(int)
+ */
+ @IntDef({REGION_WILL_CHANGE,
+ REGION_WILL_CHANGE_ANIMATED,
+ REGION_IS_CHANGING,
+ REGION_DID_CHANGE,
+ REGION_DID_CHANGE_ANIMATED,
+ WILL_START_LOADING_MAP,
+ DID_FINISH_LOADING_MAP,
+ DID_FAIL_LOADING_MAP,
+ WILL_START_RENDERING_FRAME,
+ DID_FINISH_RENDERING_FRAME,
+ DID_FINISH_RENDERING_FRAME_FULLY_RENDERED,
+ WILL_START_RENDERING_MAP,
+ DID_FINISH_RENDERING_MAP,
+ DID_FINISH_RENDERING_MAP_FULLY_RENDERED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MapChange {
+ }
+
+ /**
+ * This event is triggered whenever the currently displayed map region is about to changing
+ * without an animation.
+ * <p/>
+ * This event is followed by a series of {@link MapView#REGION_IS_CHANGING} and ends
+ * with {@link MapView#REGION_DID_CHANGE}.
+ */
+ public static final int REGION_WILL_CHANGE = 0;
+
+ /**
+ * This event is triggered whenever the currently displayed map region is about to changing
+ * with an animation.
+ * <p/>
+ * This event is followed by a series of {@link MapView#REGION_IS_CHANGING} and ends
+ * with {@link MapView#REGION_DID_CHANGE_ANIMATED}.
+ */
+ public static final int REGION_WILL_CHANGE_ANIMATED = 1;
+
+ /**
+ * This event is triggered whenever the currently displayed map region is changing.
+ */
+ public static final int REGION_IS_CHANGING = 2;
+
+ /**
+ * This event is triggered whenever the currently displayed map region finished changing
+ * without an animation.
+ */
+ public static final int REGION_DID_CHANGE = 3;
+
+ /**
+ * This event is triggered whenever the currently displayed map region finished changing
+ * with an animation.
+ */
+ public static final int REGION_DID_CHANGE_ANIMATED = 4;
+
+ /**
+ * This event is triggered when the map is about to start loading a new map style.
+ * <p/>
+ * This event is followed by {@link MapView#DID_FINISH_LOADING_MAP} or
+ * {@link MapView#DID_FAIL_LOADING_MAP}.
+ */
+ public static final int WILL_START_LOADING_MAP = 5;
+
+ /**
+ * This event is triggered when the map has successfully loaded a new map style.
+ */
+ public static final int DID_FINISH_LOADING_MAP = 6;
+
+ /**
+ * Currently not implemented.
+ * <p/>
+ * This event is triggered when the map has failed to load a new map style.
+ */
+ public static final int DID_FAIL_LOADING_MAP = 7;
+
+ /**
+ * Currently not implemented.
+ */
+ public static final int WILL_START_RENDERING_FRAME = 8;
+
+ /**
+ * Currently not implemented.
+ */
+ public static final int DID_FINISH_RENDERING_FRAME = 9;
+
+ /**
+ * Currently not implemented.
+ */
+ public static final int DID_FINISH_RENDERING_FRAME_FULLY_RENDERED = 10;
+
+ /**
+ * Currently not implemented.
+ */
+ public static final int WILL_START_RENDERING_MAP = 11;
+
+ /**
+ * Currently not implemented.
+ */
+ public static final int DID_FINISH_RENDERING_MAP = 12;
+
+ /**
+ * Currently not implemented.
+ */
+ public static final int DID_FINISH_RENDERING_MAP_FULLY_RENDERED = 13;
+
+ //
+ // Interfaces
+ //
+
+ /**
+ * Interface definition for a callback to be invoked when the map is flinged.
+ *
+ * @see MapView#setOnFlingListener(OnFlingListener)
+ */
+ public interface OnFlingListener {
+ /**
+ * Called when the map is flinged.
+ */
+ void onFling();
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the map is scrolled.
+ *
+ * @see MapView#setOnScrollListener(OnScrollListener)
+ */
+ public interface OnScrollListener {
+ /**
+ * Called when the map is scrolled.
+ */
+ void onScroll();
+ }
+
+ /**
+ * Interface definition for a callback to be invoked on every frame rendered to the map view.
+ *
+ * @see MapView#setOnFpsChangedListener(OnFpsChangedListener)
+ */
+ public interface OnFpsChangedListener {
+ /**
+ * Called for every frame rendered to the map view.
+ *
+ * @param fps The average number of frames rendered over the last second.
+ */
+ void onFpsChanged(double fps);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the user clicks on the map view.
+ *
+ * @see MapView#setOnMapClickListener(OnMapClickListener)
+ */
+ public interface OnMapClickListener {
+ /**
+ * Called when the user clicks on the map view.
+ *
+ * @param point The projected map coordinate the user clicked on.
+ */
+ void onMapClick(@NonNull LatLng point);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the user long clicks on the map view.
+ *
+ * @see MapView#setOnMapLongClickListener(OnMapLongClickListener)
+ */
+ public interface OnMapLongClickListener {
+ /**
+ * Called when the user long clicks on the map view.
+ *
+ * @param point The projected map coordinate the user long clicked on.
+ */
+ void onMapLongClick(@NonNull LatLng point);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the user clicks on a marker.
+ *
+ * @see MapView#setOnMarkerClickListener(OnMarkerClickListener)
+ */
+ public interface OnMarkerClickListener {
+ /**
+ * Called when the user clicks on a marker.
+ *
+ * @param marker The marker the user clicked on.
+ * @return If true the listener has consumed the event and the info window will not be shown.
+ */
+ boolean onMarkerClick(@NonNull Marker marker);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the user clicks on an info window.
+ *
+ * @see MapView#setOnInfoWindowClickListener(OnInfoWindowClickListener)
+ */
+ public interface OnInfoWindowClickListener {
+ /**
+ * Called when the user clicks on an info window.
+ *
+ * @param marker The marker of the info window the user clicked on.
+ * @return If true the listener has consumed the event and the info window will not be closed.
+ */
+ boolean onMarkerClick(@NonNull Marker marker);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the displayed map view changes.
+ *
+ * @see MapView#addOnMapChangedListener(OnMapChangedListener)
+ * @see MapView.MapChange
+ */
+ public interface OnMapChangedListener {
+ /**
+ * Called when the displayed map view changes.
+ *
+ * @param change The type of map change event.
+ */
+ void onMapChanged(@MapChange int change);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when an info window will be shown.
+ *
+ * @see MapView#setInfoWindowAdapter(InfoWindowAdapter)
+ */
+ public interface InfoWindowAdapter {
+ /**
+ * Called when an info window will be shown as a result of a marker click.
+ *
+ * @param marker The marker the user clicked on.
+ * @return View to be shown as a info window. If null is returned the default
+ * info window will be shown.
+ */
+ @Nullable
+ View getInfoWindow(@NonNull Marker marker);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the the My Location dot
+ * (which signifies the user's location) changes location.
+ *
+ * @see MapView#setOnMyLocationChangeListener(OnMyLocationChangeListener)
+ */
+ public interface OnMyLocationChangeListener {
+ /**
+ * Called when the location of the My Location dot has changed
+ * (be it latitude/longitude, bearing or accuracy).
+ *
+ * @param location The current location of the My Location dot The type of map change event.
+ */
+ void onMyLocationChange(@Nullable Location location);
+ }
+
+ //
+ // Constructors
+ //
+
+ /**
+ * Simple constructor to use when creating a {@link MapView} from code using the default map style.
+ *
+ * @param context The {@link Context} of the {@link android.app.Activity}
+ * or {@link android.app.Fragment} the {@link MapView} is running in.
+ * @param accessToken Your public Mapbox access token. Used to load map styles and tiles.
+ */
+ @UiThread
+ public MapView(@NonNull Context context, @NonNull String accessToken) {
+ super(context);
+ if (accessToken == null) {
+ throw new NullPointerException("accessToken is null");
+ }
+ initialize(context, null);
+ setAccessToken(accessToken);
+ setStyleUrl(null);
+ }
+
+ /**
+ * Simple constructor to use when creating a {@link MapView} from code using the provided map style URL.
+ *
+ * @param context The {@link Context} of the {@link android.app.Activity}
+ * or {@link android.app.Fragment} the {@link MapView} is running in.
+ * @param accessToken Your public Mapbox access token. Used to load map styles and tiles.
+ * @param styleUrl A URL to the map style initially displayed. See {@link MapView#setStyleUrl(String)} for possible values.
+ * @see MapView#setStyleUrl(String)
+ */
+ @UiThread
+ public MapView(@NonNull Context context, @NonNull String accessToken, @NonNull String styleUrl) {
+ super(context);
+ if (accessToken == null) {
+ throw new NullPointerException("accessToken is null");
+ }
+ if (styleUrl == null) {
+ throw new NullPointerException("styleUrl is null");
+ }
+ initialize(context, null);
+ setAccessToken(accessToken);
+ setStyleUrl(styleUrl);
+ }
+
+ // Constructor that is called when inflating a view from XML.
+
+ /**
+ * Do not call from code.
+ */
+ @UiThread
+ public MapView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initialize(context, attrs);
+ }
+
+ // Constructor that is called when inflating a view from XML.
+
+ /**
+ * Do not call from code.
+ */
+ @UiThread
+ public MapView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize(context, attrs);
+ }
+
+ //
+ // Initialization
+ //
+
+ // Common initialization code goes here
+ private void initialize(Context context, AttributeSet attrs) {
+ if (context == null) {
+ throw new NullPointerException("context is null");
+ }
+
+ // Save the context
+ mContext = context;
+
+ setWillNotDraw(false);
+
+ // Create the TextureView
+ mTextureView = new TextureView(mContext);
+ addView(mTextureView);
+ mTextureView.setSurfaceTextureListener(new SurfaceTextureListener());
+
+ // Check if we are in Android Studio UI editor to avoid error in layout preview
+ if (isInEditMode()) {
+ return;
+ }
+
+ // Get the screen's density
+ mScreenDensity = context.getResources().getDisplayMetrics().density;
+
+ // Get the cache path
+ String cachePath = context.getCacheDir().getAbsolutePath();
+ String dataPath = context.getFilesDir().getAbsolutePath();
+ String apkPath = context.getPackageCodePath();
+
+ // Create the NativeMapView
+ int availableProcessors = Runtime.getRuntime().availableProcessors();
+ ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
+ ActivityManager activityManager = (ActivityManager) context
+ .getSystemService(Context.ACTIVITY_SERVICE);
+ activityManager.getMemoryInfo(memoryInfo);
+ long maxMemory = memoryInfo.availMem;
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ maxMemory = memoryInfo.totalMem;
+ }
+ mNativeMapView = new
+ NativeMapView(this, cachePath, dataPath, apkPath, mScreenDensity, availableProcessors, maxMemory);
+
+ // Ensure this view is interactable
+ setClickable(true);
+ setLongClickable(true);
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ requestFocus();
+
+ // Touch gesture detectors
+ mGestureDetector = new GestureDetectorCompat(context, new GestureListener());
+ mGestureDetector.setIsLongpressEnabled(true);
+ mScaleGestureDetector = new ScaleGestureDetector(context, new ScaleGestureListener());
+ ScaleGestureDetectorCompat.setQuickScaleEnabled(mScaleGestureDetector, true);
+ mRotateGestureDetector = new RotateGestureDetector(context, new RotateGestureListener());
+
+ // Shows the zoom controls
+ // But not when in Eclipse UI editor
+ if (!isInEditMode()) {
+ if (!context.getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)) {
+ mZoomButtonsController = new ZoomButtonsController(this);
+ mZoomButtonsController.setZoomSpeed(ANIMATION_DURATION);
+ mZoomButtonsController.setOnZoomListener(new OnZoomListener());
+ }
+
+ // Check current connection status
+ ConnectivityManager connectivityManager = (ConnectivityManager) context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo();
+ boolean isConnected = (activeNetwork != null) && activeNetwork.isConnectedOrConnecting();
+ onConnectivityChanged(isConnected);
+ }
+
+ // Setup user location UI
+ mUserLocationView = new UserLocationView(this, getContext());
+ addView(mUserLocationView);
+
+ // Setup compass
+ mCompassView = new CompassView(mContext);
+ mCompassView.setOnClickListener(new CompassView.CompassClickListener(this));
+ addView(mCompassView);
+
+ // Setup Mapbox logo
+ mLogoView = new ImageView(mContext);
+ mLogoView.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.attribution_logo));
+ mLogoView.setContentDescription(getResources().getString(R.string.mapboxIconContentDescription));
+ ViewGroup.LayoutParams logoParams = new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ mLogoView.setLayoutParams(logoParams);
+ addView(mLogoView);
+
+ // Setup Attributions control
+ mAttributionsView = new ImageView(mContext);
+ mAttributionsView.setClickable(true);
+ mAttributionsView.setImageResource(R.drawable.ic_info_selector);
+ int attrPadding = (int) (DIMENSION_SEVEN_DP * mScreenDensity);
+ mAttributionsView.setPadding(attrPadding, attrPadding, attrPadding, attrPadding);
+ mAttributionsView.setAdjustViewBounds(true);
+ mAttributionsView.setContentDescription(getResources()
+ .getString(R.string.attributionsIconContentDescription));
+ LayoutParams attrParams = new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ mAttributionsView.setLayoutParams(attrParams);
+ addView(mAttributionsView);
+ mAttributionsView.setOnClickListener(new AttributionOnClickListener(this));
+
+ // Load the attributes
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MapView, 0, 0);
+ try {
+ double centerLatitude = typedArray.getFloat(R.styleable.MapView_center_latitude, 0.0f);
+ double centerLongitude = typedArray.getFloat(R.styleable.MapView_center_longitude, 0.0f);
+ LatLng centerCoordinate = new LatLng(centerLatitude, centerLongitude);
+ setCenterCoordinate(centerCoordinate);
+ // need to set zoom level first because of limitation on rotating when zoomed out
+ setZoomLevel(typedArray.getFloat(R.styleable.MapView_zoom_level, 0.0f));
+ setDirection(typedArray.getFloat(R.styleable.MapView_direction, 0.0f));
+ setZoomEnabled(typedArray.getBoolean(R.styleable.MapView_zoom_enabled, true));
+ setScrollEnabled(typedArray.getBoolean(R.styleable.MapView_scroll_enabled, true));
+ setRotateEnabled(typedArray.getBoolean(R.styleable.MapView_rotate_enabled, true));
+ setDebugActive(typedArray.getBoolean(R.styleable.MapView_debug_active, false));
+ if (typedArray.getString(R.styleable.MapView_style_url) != null) {
+ setStyleUrl(typedArray.getString(R.styleable.MapView_style_url));
+ }
+ if (typedArray.getString(R.styleable.MapView_access_token) != null) {
+ setAccessToken(typedArray.getString(R.styleable.MapView_access_token));
+ }
+ if (typedArray.getString(R.styleable.MapView_style_classes) != null) {
+ List<String> styleClasses = Arrays.asList(typedArray
+ .getString(R.styleable.MapView_style_classes).split("\\s*,\\s*"));
+ for (String styleClass : styleClasses) {
+ if (styleClass.length() == 0) {
+ styleClasses.remove(styleClass);
+ }
+ }
+ setStyleClasses(styleClasses);
+ }
+
+ // Compass
+ setCompassEnabled(typedArray.getBoolean(R.styleable.MapView_compass_enabled, true));
+ setCompassGravity(typedArray.getInt(R.styleable.MapView_compass_gravity, Gravity.TOP | Gravity.END));
+ setWidgetMargins(mCompassView, typedArray.getDimension(R.styleable.MapView_compass_margin_left, DIMENSION_TEN_DP)
+ , typedArray.getDimension(R.styleable.MapView_compass_margin_top, DIMENSION_TEN_DP)
+ , typedArray.getDimension(R.styleable.MapView_compass_margin_right, DIMENSION_TEN_DP)
+ , typedArray.getDimension(R.styleable.MapView_compass_margin_bottom, DIMENSION_TEN_DP));
+
+ // Logo
+ setLogoVisibility(typedArray.getInt(R.styleable.MapView_logo_visibility, View.VISIBLE));
+ setLogoGravity(typedArray.getInt(R.styleable.MapView_logo_gravity, Gravity.BOTTOM | Gravity.START));
+ setWidgetMargins(mLogoView, typedArray.getDimension(R.styleable.MapView_logo_margin_left, DIMENSION_SIXTEEN_DP)
+ , typedArray.getDimension(R.styleable.MapView_logo_margin_top, DIMENSION_SIXTEEN_DP)
+ , typedArray.getDimension(R.styleable.MapView_logo_margin_right, DIMENSION_SIXTEEN_DP)
+ , typedArray.getDimension(R.styleable.MapView_logo_margin_bottom, DIMENSION_SIXTEEN_DP));
+
+ // Attribution
+ setAttributionVisibility(typedArray.getInt(R.styleable.MapView_attribution_visibility, View.VISIBLE));
+ setAttributionGravity(typedArray.getInt(R.styleable.MapView_attribution_gravity, Gravity.BOTTOM));
+ setWidgetMargins(mAttributionsView, typedArray.getDimension(R.styleable.MapView_attribution_margin_left, DIMENSION_SEVENTYSIX_DP)
+ , typedArray.getDimension(R.styleable.MapView_attribution_margin_top, DIMENSION_SEVEN_DP)
+ , typedArray.getDimension(R.styleable.MapView_attribution_margin_right, DIMENSION_SEVEN_DP)
+ , typedArray.getDimension(R.styleable.MapView_attribution_margin_bottom, DIMENSION_SEVEN_DP));
+
+ // User location
+ setMyLocationEnabled(typedArray.getBoolean(R.styleable.MapView_my_location_enabled, false));
+ } finally {
+ typedArray.recycle();
+ }
+ }
+
+ //
+ // Lifecycle events
+ //
+
+ /**
+ * You must call this method from the parent's {@link android.app.Activity#onCreate(Bundle)} or
+ * {@link android.app.Fragment#onCreate(Bundle)}.
+ * </p>
+ * You must set a valid access token with {@link MapView#setAccessToken(String)} before you this method
+ * or an exception will be thrown.
+ *
+ * @param savedInstanceState Pass in the parent's savedInstanceState.
+ * @see MapView#setAccessToken(String)
+ */
+ @UiThread
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ setCenterCoordinate((LatLng) savedInstanceState.getParcelable(STATE_CENTER_COORDINATE));
+ // need to set zoom level first because of limitation on rotating when zoomed out
+ setZoomLevel(savedInstanceState.getDouble(STATE_ZOOM_LEVEL));
+ setDirection(savedInstanceState.getDouble(STATE_CENTER_DIRECTION));
+ setDirection(savedInstanceState.getDouble(STATE_DIRECTION));
+ setZoomEnabled(savedInstanceState.getBoolean(STATE_ZOOM_ENABLED));
+ setScrollEnabled(savedInstanceState.getBoolean(STATE_SCROLL_ENABLED));
+ setRotateEnabled(savedInstanceState.getBoolean(STATE_ROTATE_ENABLED));
+ setDebugActive(savedInstanceState.getBoolean(STATE_DEBUG_ACTIVE));
+ setStyleUrl(savedInstanceState.getString(STATE_STYLE_URL));
+ setAccessToken(savedInstanceState.getString(STATE_ACCESS_TOKEN));
+ List<String> appliedStyleClasses = savedInstanceState.getStringArrayList(STATE_STYLE_CLASSES);
+ if (!appliedStyleClasses.isEmpty()) {
+ setStyleClasses(appliedStyleClasses);
+ }
+ mNativeMapView.setDefaultTransitionDuration(
+ savedInstanceState.getLong(STATE_DEFAULT_TRANSITION_DURATION));
+ setMyLocationEnabled(savedInstanceState.getBoolean(STATE_MY_LOCATION_ENABLED));
+
+ // Compass
+ setCompassEnabled(savedInstanceState.getBoolean(STATE_COMPASS_ENABLED));
+ setCompassGravity(savedInstanceState.getInt(STATE_COMPASS_GRAVITY));
+ setCompassMargins(savedInstanceState.getInt(STATE_COMPASS_MARGIN_LEFT)
+ , savedInstanceState.getInt(STATE_COMPASS_MARGIN_TOP)
+ , savedInstanceState.getInt(STATE_COMPASS_MARGIN_RIGHT)
+ , savedInstanceState.getInt(STATE_COMPASS_MARGIN_BOTTOM));
+
+ // Logo
+ setLogoVisibility(savedInstanceState.getInt(STATE_LOGO_VISIBILITY));
+ setLogoGravity(savedInstanceState.getInt(STATE_LOGO_GRAVITY));
+ setLogoMargins(savedInstanceState.getInt(STATE_LOGO_MARGIN_LEFT)
+ , savedInstanceState.getInt(STATE_LOGO_MARGIN_TOP)
+ , savedInstanceState.getInt(STATE_LOGO_MARGIN_RIGHT)
+ , savedInstanceState.getInt(STATE_LOGO_MARGIN_BOTTOM));
+
+ // Attribution
+ setAttributionVisibility(savedInstanceState.getInt(STATE_ATTRIBUTION_VISIBILITY));
+ setAttributionGravity(savedInstanceState.getInt(STATE_ATTRIBUTION_GRAVITY));
+ setAttributionMargins(savedInstanceState.getInt(STATE_ATTRIBUTION_MARGIN_LEFT)
+ , savedInstanceState.getInt(STATE_ATTRIBUTION_MARGIN_TOP)
+ , savedInstanceState.getInt(STATE_ATTRIBUTION_MARGIN_RIGHT)
+ , savedInstanceState.getInt(STATE_ATTRIBUTION_MARGIN_BOTTOM));
+ }
+
+ // Force a check for an access token
+ validateAccessToken(getAccessToken());
+
+ // Initialize EGL
+ mNativeMapView.initializeDisplay();
+ mNativeMapView.initializeContext();
+
+ // Add annotation deselection listener
+ addOnMapChangedListener(new OnMapChangedListener() {
+ @Override
+ public void onMapChanged(@MapChange int change) {
+ if ((change == REGION_WILL_CHANGE) || (change == REGION_WILL_CHANGE_ANIMATED)) {
+ deselectMarker();
+ }
+
+ if (change == DID_FINISH_LOADING_MAP) {
+ reloadSprites();
+ reloadMarkers();
+ adjustTopOffsetPixels();
+ }
+ }
+ });
+ }
+
+ /**
+ * You must call this method from the parent's {@link android.app.Activity#onSaveInstanceState(Bundle)}
+ * or {@link android.app.Fragment#onSaveInstanceState(Bundle)}.
+ *
+ * @param outState Pass in the parent's outState.
+ */
+ @UiThread
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ if (outState == null) {
+ throw new NullPointerException("outState is null");
+ }
+
+ outState.putParcelable(STATE_CENTER_COORDINATE, getCenterCoordinate());
+ // need to set zoom level first because of limitation on rotating when zoomed out
+ outState.putDouble(STATE_ZOOM_LEVEL, getZoomLevel());
+ outState.putDouble(STATE_CENTER_DIRECTION, getDirection());
+ outState.putBoolean(STATE_ZOOM_ENABLED, mZoomEnabled);
+ outState.putBoolean(STATE_SCROLL_ENABLED, mScrollEnabled);
+ outState.putBoolean(STATE_ROTATE_ENABLED, mRotateEnabled);
+ outState.putBoolean(STATE_DEBUG_ACTIVE, isDebugActive());
+ outState.putString(STATE_STYLE_URL, mStyleUrl);
+ outState.putString(STATE_ACCESS_TOKEN, getAccessToken());
+ outState.putStringArrayList(STATE_STYLE_CLASSES, new ArrayList<>(getStyleClasses()));
+ outState.putLong(STATE_DEFAULT_TRANSITION_DURATION, mNativeMapView.getDefaultTransitionDuration());
+ outState.putBoolean(STATE_MY_LOCATION_ENABLED, isMyLocationEnabled());
+
+ // Compass
+ LayoutParams compassParams = (LayoutParams) mCompassView.getLayoutParams();
+ outState.putBoolean(STATE_COMPASS_ENABLED, isCompassEnabled());
+ outState.putInt(STATE_COMPASS_GRAVITY, compassParams.gravity);
+ outState.putInt(STATE_COMPASS_MARGIN_LEFT, compassParams.leftMargin);
+ outState.putInt(STATE_COMPASS_MARGIN_TOP, compassParams.topMargin);
+ outState.putInt(STATE_COMPASS_MARGIN_BOTTOM, compassParams.bottomMargin);
+ outState.putInt(STATE_COMPASS_MARGIN_RIGHT, compassParams.rightMargin);
+
+ // Logo
+ LayoutParams logoParams = (LayoutParams) mLogoView.getLayoutParams();
+ outState.putInt(STATE_LOGO_GRAVITY, logoParams.gravity);
+ outState.putInt(STATE_LOGO_MARGIN_LEFT, logoParams.leftMargin);
+ outState.putInt(STATE_LOGO_MARGIN_TOP, logoParams.topMargin);
+ outState.putInt(STATE_LOGO_MARGIN_RIGHT, logoParams.rightMargin);
+ outState.putInt(STATE_LOGO_MARGIN_BOTTOM, logoParams.bottomMargin);
+ outState.putInt(STATE_LOGO_VISIBILITY, mLogoView.getVisibility());
+
+ // Attribution
+ LayoutParams attrParams = (LayoutParams) mAttributionsView.getLayoutParams();
+ outState.putInt(STATE_ATTRIBUTION_GRAVITY, attrParams.gravity);
+ outState.putInt(STATE_ATTRIBUTION_MARGIN_LEFT, attrParams.leftMargin);
+ outState.putInt(STATE_ATTRIBUTION_MARGIN_TOP, attrParams.topMargin);
+ outState.putInt(STATE_ATTRIBUTION_MARGIN_RIGHT, attrParams.rightMargin);
+ outState.putInt(STATE_ATTRIBUTION_MARGIN_BOTTOM, attrParams.bottomMargin);
+ outState.putInt(STATE_ATTRIBUTION_VISIBILITY, mAttributionsView.getVisibility());
+ }
+
+ /**
+ * You must call this method from the parent's {@link Activity#onDestroy()} or {@link Fragment#onDestroy()}.
+ */
+ @UiThread
+ public void onDestroy() {
+ mNativeMapView.terminateContext();
+ mNativeMapView.terminateDisplay();
+ mNativeMapView.destroySurface();
+ mNativeMapView.destroy();
+ mNativeMapView = null;
+ }
+
+ /**
+ * You must call this method from the parent's {@link Activity#onStart()} or {@link Fragment#onStart()}.
+ */
+ @UiThread
+ public void onStart() {
+ }
+
+ /**
+ * You must call this method from the parent's {@link Activity#onStop()} or {@link Fragment#onStop()}
+ */
+ @UiThread
+ public void onStop() {
+ mUserLocationView.cancelAnimations();
+ }
+
+ /**
+ * You must call this method from the parent's {@link Activity#onPause()} or {@link Fragment#onPause()}.
+ */
+ @UiThread
+ public void onPause() {
+ // Register for connectivity changes
+ getContext().unregisterReceiver(mConnectivityReceiver);
+ mConnectivityReceiver = null;
+
+ mUserLocationView.pause();
+ mNativeMapView.pause();
+ }
+
+ /**
+ * You must call this method from the parent's {@link Activity#onResume()} or {@link Fragment#onResume()}.
+ */
+ @UiThread
+ public void onResume() {
+ // Register for connectivity changes
+ mConnectivityReceiver = new ConnectivityReceiver();
+ mContext.registerReceiver(mConnectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+
+ mUserLocationView.resume();
+ mNativeMapView.resume();
+ mNativeMapView.update();
+ }
+
+ /**
+ * You must call this method from the parent's {@link Activity#onLowMemory()} or {@link Fragment#onLowMemory()}.
+ */
+ @UiThread
+ public void onLowMemory() {
+ mNativeMapView.onLowMemory();
+ }
+
+ //
+ // Position
+ //
+
+ /**
+ * Returns the current coordinate at the center of the map view.
+ *
+ * @return The current coordinate.
+ */
+ @UiThread
+ @NonNull
+ public LatLng getCenterCoordinate() {
+ return mNativeMapView.getLatLng();
+ }
+
+ /**
+ * Centers the map on a new coordinate immediately without changing the zoom level.
+ * <p/>
+ * The initial coordinate is (0, 0).
+ * <p/>
+ * If you want to animate the change, use {@link MapView#setCenterCoordinate(LatLng, boolean)}.
+ *
+ * @param centerCoordinate The new coordinate.
+ * @see MapView#setCenterCoordinate(LatLng, boolean)
+ */
+ @UiThread
+ public void setCenterCoordinate(@NonNull LatLng centerCoordinate) {
+ setCenterCoordinate(centerCoordinate, false);
+ }
+
+ /**
+ * Centers the map on a new coordinate without changing the zoom level and optionally animates the change.
+ * <p/>
+ * The initial coordinate is (0, 0).
+ *
+ * @param centerCoordinate The new coordinate.
+ * @param animated If true, animates the change. If false, immediately changes the map.
+ */
+ @UiThread
+ public void setCenterCoordinate(@NonNull LatLng centerCoordinate, boolean animated) {
+ if (centerCoordinate == null) {
+ throw new NullPointerException("centerCoordinate is null");
+ }
+ long duration = animated ? ANIMATION_DURATION : 0;
+ mNativeMapView.cancelTransitions();
+ mNativeMapView.setLatLng(centerCoordinate, duration);
+ }
+
+
+ /**
+ * Centers the map on a new coordinate immediately while changing the current zoom level.
+ * <p/>
+ * The initial value is a center coordinate of (0, 0) and a zoom level of 0.
+ * <p/>
+ * If you want to animate the change, use {@link MapView#setCenterCoordinate(LatLngZoom, boolean)}.
+ *
+ * @param centerCoordinate The new coordinate and zoom level.
+ * @see MapView#setCenterCoordinate(LatLngZoom, boolean)
+ */
+ @UiThread
+ public void setCenterCoordinate(@NonNull LatLngZoom centerCoordinate) {
+ setCenterCoordinate(centerCoordinate, false);
+ }
+
+ /**
+ * Resets the map to the minimum zoom level, a center coordinate of (0, 0), a true north heading,
+ * and animates the change.
+ */
+ @UiThread
+ public void resetPosition() {
+ mNativeMapView.cancelTransitions();
+ mNativeMapView.resetPosition();
+ }
+
+ /**
+ * Centers the map on a new coordinate while changing the zoom level and optionally animates the change.
+ * <p/>
+ * The initial value is a center coordinate of (0, 0) and a zoom level of 0.
+ *
+ * @param centerCoordinate The new coordinate and zoom level.
+ * @param animated If true, animates the change. If false, immediately changes the map.
+ */
+ @UiThread
+ public void setCenterCoordinate(@NonNull LatLngZoom centerCoordinate,
+ boolean animated) {
+ if (centerCoordinate == null) {
+ throw new NullPointerException("centerCoordinate is null");
+ }
+ long duration = animated ? ANIMATION_DURATION : 0;
+ mNativeMapView.cancelTransitions();
+ mNativeMapView.setLatLngZoom(centerCoordinate, duration);
+ }
+
+ /**
+ * Returns whether the user may scroll around the map.
+ *
+ * @return If true, scrolling is enabled.
+ */
+ @UiThread
+ public boolean isScrollEnabled() {
+ return mScrollEnabled;
+ }
+
+ /**
+ * Changes whether the user may scroll around the map.
+ * <p/>
+ * This setting controls only user interactions with the map. If you set the value to false,
+ * you may still change the map location programmatically.
+ * <p/>
+ * The default value is true.
+ *
+ * @param scrollEnabled If true, scrolling is enabled.
+ */
+ @UiThread
+ public void setScrollEnabled(boolean scrollEnabled) {
+ this.mScrollEnabled = scrollEnabled;
+ }
+
+ //
+ // Rotation
+ //
+
+ /**
+ * Returns the current heading of the map relative to true north.
+ *
+ * @return The current heading measured in degrees.
+ */
+ @UiThread
+ @FloatRange(from = 0, to = 360)
+ public double getDirection() {
+ double direction = -mNativeMapView.getBearing();
+
+ while (direction > 360) {
+ direction -= 360;
+ }
+ while (direction < 0) {
+ direction += 360;
+ }
+
+ return direction;
+ }
+
+ /**
+ * Rotates the map to a new heading relative to true north immediately.
+ * <p/>
+ * The value 0 means that the top edge of the map view will correspond to true north.<br>
+ * The value 90 means the top of the map will point due east.<br>
+ * The value 180 means the top of the map will point due south.<br>
+ * The value 270 means the top of the map will point due west.
+ * <p/>
+ * The initial heading is 0.
+ * <p/>
+ * If you want to animate the change, use {@link MapView#setDirection(double, boolean)}.
+ *
+ * @param direction The new heading measured in degrees.
+ * @see MapView#setDirection(double, boolean)
+ */
+ @UiThread
+ public void setDirection(@FloatRange(from = 0, to = 360) double direction) {
+ setDirection(direction, false);
+ }
+
+ /**
+ * Rotates the map to a new heading relative to true north and optionally animates the change.
+ * <p/>
+ * The value 0 means that the top edge of the map view will correspond to true north.<br/>
+ * The value 90 means the top of the map will point due east.<br/>
+ * The value 180 means the top of the map will point due south.<br/>
+ * The value 270 means the top of the map will point due west
+ * <p/>
+ * The initial heading is 0.
+ *
+ * @param direction The new heading measured in degrees from true north.
+ * @param animated If true, animates the change. If false, immediately changes the map.
+ */
+ @UiThread
+ public void setDirection(@FloatRange(from = 0, to = 360) double direction, boolean animated) {
+ long duration = animated ? ANIMATION_DURATION : 0;
+ mNativeMapView.cancelTransitions();
+ // Out of range direactions are normallised in setBearing
+ mNativeMapView.setBearing(-direction, duration);
+ }
+
+ /**
+ * Resets the map heading to true north and animates the change.
+ */
+ @UiThread
+ public void resetNorth() {
+ mNativeMapView.cancelTransitions();
+ mNativeMapView.resetNorth();
+ }
+
+ /**
+ * Returns whether the user may rotate the map.
+ *
+ * @return If true, rotating is enabled.
+ */
+ @UiThread
+ public boolean isRotateEnabled() {
+ return mRotateEnabled;
+ }
+
+ /**
+ * Changes whether the user may rotate the map.
+ * <p/>
+ * This setting controls only user interactions with the map. If you set the value to false,
+ * you may still change the map location programmatically.
+ * <p/>
+ * The default value is true.
+ *
+ * @param rotateEnabled If true, rotating is enabled.
+ */
+ @UiThread
+ public void setRotateEnabled(boolean rotateEnabled) {
+ this.mRotateEnabled = rotateEnabled;
+ }
+
+ //
+ // Scale
+ //
+
+ /**
+ * Returns the current zoom level of the map view.
+ *
+ * @return The current zoom level.
+ */
+ @UiThread
+ @FloatRange(from = 0.0, to = MAXIMUM_ZOOM_LEVEL)
+ public double getZoomLevel() {
+ return mNativeMapView.getZoom();
+ }
+
+ /**
+ * Zooms the map to a new zoom level immediately without changing the center coordinate.
+ * <p/>
+ * At zoom level 0, tiles cover the entire world map;
+ * at zoom level 1, tiles cover &frac14 of the world;
+ * at zoom level 2, tiles cover 1/16 of the world, and so on.
+ * <p/>
+ * The initial zoom level is 0. The maximum zoom level is {@link MapView#MAXIMUM_ZOOM_LEVEL}.
+ * <p/>
+ * If you want to animate the change, use {@link MapView#setZoomLevel(double, boolean)}.
+ *
+ * @param zoomLevel The new coordinate.
+ * @see MapView#setZoomLevel(double, boolean)
+ * @see MapView#MAXIMUM_ZOOM_LEVEL
+ */
+ @UiThread
+ public void setZoomLevel(@FloatRange(from = 0.0, to = MAXIMUM_ZOOM_LEVEL) double zoomLevel) {
+ setZoomLevel(zoomLevel, false);
+ }
+
+ /**
+ * Zooms the map to a new zoom level and optionally animates the change without changing the center coordinate.
+ * <p/>
+ * At zoom level 0, tiles cover the entire world map;
+ * at zoom level 1, tiles cover &frac14 of the world;
+ * at zoom level 2, tiles cover 1/16 of the world, and so on.
+ * <p/>
+ * The initial zoom level is 0. The maximum zoom level is {@link MapView#MAXIMUM_ZOOM_LEVEL}.
+ *
+ * @param zoomLevel The new coordinate.
+ * @param animated If true, animates the change. If false, immediately changes the map.
+ * @see MapView#MAXIMUM_ZOOM_LEVEL
+ */
+ @UiThread
+ public void setZoomLevel(@FloatRange(from = 0.0, to = MAXIMUM_ZOOM_LEVEL) double zoomLevel, boolean animated) {
+ if ((zoomLevel < 0.0) || (zoomLevel > MAXIMUM_ZOOM_LEVEL)) {
+ throw new IllegalArgumentException("zoomLevel is < 0 or > MapView.MAXIMUM_ZOOM_LEVEL");
+ }
+ long duration = animated ? ANIMATION_DURATION : 0;
+ mNativeMapView.cancelTransitions();
+ mNativeMapView.setZoom(zoomLevel, duration);
+ }
+
+ /**
+ * Returns whether the user may zoom the map.
+ *
+ * @return If true, zooming is enabled.
+ */
+ @UiThread
+ public boolean isZoomEnabled() {
+ return mZoomEnabled;
+ }
+
+ /**
+ * Changes whether the user may zoom the map.
+ * <p/>
+ * This setting controls only user interactions with the map. If you set the value to false,
+ * you may still change the map location programmatically.
+ * <p/>
+ * The default value is true.
+ *
+ * @param zoomEnabled If true, zooming is enabled.
+ */
+ @UiThread
+ public void setZoomEnabled(boolean zoomEnabled) {
+ this.mZoomEnabled = zoomEnabled;
+
+ if ((mZoomButtonsController != null) && (getVisibility() == View.VISIBLE) && mZoomEnabled) {
+ mZoomButtonsController.setVisible(true);
+ }
+ }
+
+ // Zoom in or out
+ private void zoom(boolean zoomIn) {
+ zoom(zoomIn, -1.0f, -1.0f);
+ }
+
+ private void zoom(boolean zoomIn, float x, float y) {
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ if (zoomIn) {
+ mNativeMapView.scaleBy(2.0, x / mScreenDensity, y / mScreenDensity, ANIMATION_DURATION);
+ } else {
+ mNativeMapView.scaleBy(0.5, x / mScreenDensity, y / mScreenDensity, ANIMATION_DURATION);
+ }
+ }
+
+ //
+ // Debug
+ //
+
+ /**
+ * Returns whether the map debug information is currently shown.
+ *
+ * @return If true, map debug information is currently shown.
+ */
+ @UiThread
+ public boolean isDebugActive() {
+ return mNativeMapView.getDebug() || mNativeMapView.getCollisionDebug();
+ }
+
+ /**
+ * Changes whether the map debug information is shown.
+ * <p/>
+ * The default value is false.
+ *
+ * @param debugActive If true, map debug information is shown.
+ */
+ @UiThread
+ public void setDebugActive(boolean debugActive) {
+ mNativeMapView.setDebug(debugActive);
+ mNativeMapView.setCollisionDebug(debugActive);
+ }
+
+ /**
+ * Toggles whether the map debug information is shown.
+ * <p/>
+ * The value of {@link MapView#isDebugActive()} is toggled.
+ *
+ * @see MapView#isDebugActive()
+ */
+ @UiThread
+ public void toggleDebug() {
+ mNativeMapView.toggleDebug();
+ mNativeMapView.toggleCollisionDebug();
+ }
+
+ // True if map has finished loading the view
+ private boolean isFullyLoaded() {
+ return mNativeMapView.isFullyLoaded();
+ }
+
+ //
+ // Styling
+ //
+
+ /**
+ * Loads a new map style from the specified URL.
+ * <p/>
+ * {@code url} can take the following forms:
+ * <ul>
+ * <li>{@code Style.*}: load one of the bundled styles in {@link Style}.</li>
+ * <li>{@code mapbox://styles/<user>/<style>}:
+ * retrieves the style from a <a href="https://www.mapbox.com/account/">Mapbox account.</a>
+ * {@code user} is your username. {@code style} is the ID of your custom
+ * style created in <a href="https://www.mapbox.com/studio">Mapbox Studio</a>.</li>
+ * <li>{@code http://...} or {@code https://...}:
+ * retrieves the style over the Internet from any web server.</li>
+ * <li>{@code asset://...}:
+ * reads the style from the APK {@code asset/} directory.
+ * This is used to load a style bundled with your app.</li>
+ * <li>{@code null}: loads the default {@link Style#MAPBOX_STREETS} style.</li>
+ * </ul>
+ * <p/>
+ * This method is asynchronous and will return immediately before the style finishes loading.
+ * If you wish to wait for the map to finish loading listen for the {@link MapView#DID_FINISH_LOADING_MAP} event.
+ * <p/>
+ * If the style fails to load or an invalid style URL is set, the map view will become blank.
+ * An error message will be logged in the Android logcat and {@link MapView#DID_FAIL_LOADING_MAP} event will be sent.
+ *
+ * @param url The URL of the map style
+ * @see Style
+ */
+ @UiThread
+ public void setStyleUrl(@Nullable String url) {
+ if (url == null) {
+ url = Style.MAPBOX_STREETS;
+ }
+ mStyleUrl = url;
+ mNativeMapView.setStyleUrl(url);
+ }
+
+ /**
+ * Loads a new map style from the specified bundled style.
+ * <p/>
+ * This method is asynchronous and will return immediately before the style finishes loading.
+ * If you wish to wait for the map to finish loading listen for the {@link MapView#DID_FINISH_LOADING_MAP} event.
+ * <p/>
+ * If the style fails to load or an invalid style URL is set, the map view will become blank.
+ * An error message will be logged in the Android logcat and {@link MapView#DID_FAIL_LOADING_MAP} event will be sent.
+ *
+ * @param style The bundled style. Accepts one of the values from {@link Style}.
+ */
+ @UiThread
+ public void setStyle(@Style.StyleUrl String style) {
+ setStyleUrl(style);
+ }
+
+ /**
+ * Returns the map style currently displayed in the map view.
+ * <p/>
+ * If the default style is currently displayed, a URL will be returned instead of null.
+ *
+ * @return The URL of the map style.
+ */
+ @UiThread
+ @NonNull
+ public String getStyleUrl() {
+ return mStyleUrl;
+ }
+
+ /**
+ * Returns the set of currently active map style classes.
+ *
+ * @return A list of class identifiers.
+ */
+ @UiThread
+ @NonNull
+ public List<String> getStyleClasses() {
+ return Collections.unmodifiableList(mNativeMapView.getClasses());
+ }
+
+ /**
+ * Changes the set of currently active map style classes immediately.
+ * <p/>
+ * The list of valid class identifiers is defined by the currently loaded map style.
+ * <p/>
+ * If you want to animate the change, use {@link MapView#setStyleClasses(List, long)}.
+ *
+ * @param styleClasses A list of class identifiers.
+ * @see MapView#setStyleClasses(List, long)
+ * @see MapView#setStyleUrl(String)
+ */
+ @UiThread
+ public void setStyleClasses(@NonNull List<String> styleClasses) {
+ setStyleClasses(styleClasses, 0);
+ }
+
+ /**
+ * Changes the set of currently active map style classes with an animated transition.
+ * <p/>
+ * The list of valid class identifiers is defined by the currently loaded map style.
+ *
+ * @param styleClasses A list of class identifiers.
+ * @param transitionDuration The duration of the transition animation in milliseconds.
+ * @see MapView#setStyleClasses(List, long)
+ * @see MapView#setStyleUrl(String)
+ */
+ @UiThread
+ public void setStyleClasses(@NonNull List<String> styleClasses, @IntRange(from = 0) long transitionDuration) {
+ if (styleClasses == null) {
+ throw new NullPointerException("styleClasses is null");
+ }
+ if (transitionDuration < 0) {
+ throw new IllegalArgumentException("transitionDuration is < 0");
+ }
+ // TODO non negative check and annotation (go back and check other functions too)
+ mNativeMapView.setDefaultTransitionDuration(transitionDuration);
+ mNativeMapView.setClasses(styleClasses);
+ }
+
+ /**
+ * Activates the specified map style class.
+ * <p/>
+ * If you want to animate the change, use {@link MapView#setStyleClasses(List, long)}.
+ *
+ * @param styleClass The class identifier.
+ * @see MapView#setStyleClasses(List, long)
+ */
+ @UiThread
+ public void addStyleClass(@NonNull String styleClass) {
+ if (styleClass == null) {
+ throw new NullPointerException("styleClass is null");
+ }
+ mNativeMapView.addClass(styleClass);
+ }
+
+ /**
+ * Deactivates the specified map style class.
+ * <p/>
+ * If you want to animate the change, use {@link MapView#setStyleClasses(List, long)}.
+ *
+ * @param styleClass The class identifier.
+ * @see MapView#setStyleClasses(List, long)
+ */
+ @UiThread
+ public void removeStyleClass(@NonNull String styleClass) {
+ if (styleClass == null) {
+ throw new NullPointerException("styleClass is null");
+ }
+ mNativeMapView.removeClass(styleClass);
+ }
+
+ /**
+ * Returns whether the specified map style class is currently active.
+ *
+ * @param styleClass The class identifier.
+ * @return If true, the class is currently active.
+ */
+ @UiThread
+ public boolean hasStyleClass(@NonNull String styleClass) {
+ if (styleClass == null) {
+ throw new NullPointerException("styleClass is null");
+ }
+ return mNativeMapView.hasClass(styleClass);
+ }
+
+ /**
+ * Deactivates all the currently active map style classes immediately.
+ * <p/>
+ * If you want to animate the change, use {@link MapView#removeAllStyleClasses(long)}.
+ *
+ * @see MapView#removeAllStyleClasses(long)
+ */
+ @UiThread
+ public void removeAllStyleClasses() {
+ removeAllStyleClasses(0);
+ }
+
+ /**
+ * Deactivates all the currently active map style classes with an animated transition.
+ *
+ * @param transitionDuration The duration of the transition animation in milliseconds.
+ */
+ @UiThread
+ public void removeAllStyleClasses(@IntRange(from = 0) long transitionDuration) {
+ if (transitionDuration < 0) {
+ throw new IllegalArgumentException("transitionDuration is < 0");
+ }
+ mNativeMapView.setDefaultTransitionDuration(transitionDuration);
+ ArrayList<String> styleClasses = new ArrayList<>(0);
+ setStyleClasses(styleClasses);
+ }
+
+ //
+ // Access token
+ //
+
+ // Checks if the given token is valid
+ private void validateAccessToken(String accessToken) {
+ if (TextUtils.isEmpty(accessToken) || (!accessToken.startsWith("pk.") && !accessToken.startsWith("sk."))) {
+ throw new InvalidAccessTokenException();
+ }
+ }
+
+ /**
+ * Sets the current Mapbox access token used to load map styles and tiles.
+ * <p/>
+ * You must set a valid access token before you call {@link MapView#onCreate(Bundle)}
+ * or an exception will be thrown.
+ * <p/>
+ * You can use {@link ApiAccess#getToken(Context)} to load an access token from your
+ * application's manifest.
+ *
+ * @param accessToken Your public Mapbox access token.
+ * @see MapView#onCreate(Bundle)
+ * @see ApiAccess#getToken(Context)
+ */
+ @UiThread
+ public void setAccessToken(@NonNull String accessToken) {
+ // validateAccessToken does the null check
+ validateAccessToken(accessToken);
+ mNativeMapView.setAccessToken(accessToken);
+ }
+
+ /**
+ * Returns the current Mapbox access token used to load map styles and tiles.
+ *
+ * @return The current Mapbox access token.
+ */
+ @UiThread
+ @Nullable
+ public String getAccessToken() {
+ return mNativeMapView.getAccessToken();
+ }
+
+ //
+ // Projection
+ //
+
+ /**
+ * Converts a point in this view's coordinate system to a map coordinate.
+ *
+ * @param point A point in this view's coordinate system.
+ * @return The converted map coordinate.
+ */
+ @UiThread
+ @NonNull
+ public LatLng fromScreenLocation(@NonNull PointF point) {
+ if (point == null) {
+ throw new NullPointerException("point is null");
+ }
+
+ float x = point.x;
+ float y = point.y;
+
+ // flip y direction vertically to match core GL
+ y = getHeight() - y;
+
+ return mNativeMapView.latLngForPixel(new PointF(x / mScreenDensity, y / mScreenDensity));
+ }
+
+ /**
+ * Converts a map coordinate to a point in this view's coordinate system.
+ *
+ * @param location A map coordinate.
+ * @return The converted point in this view's coordinate system.
+ */
+ @UiThread
+ @NonNull
+ public PointF toScreenLocation(@NonNull LatLng location) {
+ if (location == null) {
+ throw new NullPointerException("location is null");
+ }
+
+ PointF point = mNativeMapView.pixelForLatLng(location);
+
+ float x = point.x * mScreenDensity;
+ float y = point.y * mScreenDensity;
+
+ // flip y direction vertically to match core GL
+ y = getHeight() - y;
+
+ return new PointF(x, y);
+ }
+
+ //
+ // Annotations
+ //
+
+ public SpriteFactory getSpriteFactory() {
+ if (mSpriteFactory == null) {
+ mSpriteFactory = new SpriteFactory(this);
+ }
+ return mSpriteFactory;
+ }
+
+ private void loadSprite(Sprite sprite) {
+ Bitmap bitmap = sprite.getBitmap();
+ String id = sprite.getId();
+ if (bitmap.getConfig() != Bitmap.Config.ARGB_8888) {
+ bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false);
+ }
+ ByteBuffer buffer = ByteBuffer.allocate(bitmap.getRowBytes() * bitmap.getHeight());
+ bitmap.copyPixelsToBuffer(buffer);
+
+ float density = bitmap.getDensity();
+ if (density == Bitmap.DENSITY_NONE) {
+ density = DisplayMetrics.DENSITY_DEFAULT;
+ }
+ float scale = density / DisplayMetrics.DENSITY_DEFAULT;
+
+ mNativeMapView.setSprite(
+ id,
+ (int) (bitmap.getWidth() / scale),
+ (int) (bitmap.getHeight() / scale),
+ scale, buffer.array());
+ }
+
+ private void reloadSprites() {
+ int count = mSprites.size();
+ for (int i = 0; i < count; i++) {
+ Sprite sprite = mSprites.get(i);
+ loadSprite(sprite);
+ }
+ }
+
+ private Marker prepareMarker(MarkerOptions markerOptions) {
+ Marker marker = markerOptions.getMarker();
+ Sprite icon = marker.getIcon();
+ if (icon == null) {
+ icon = getSpriteFactory().defaultMarker();
+ marker.setIcon(icon);
+ }
+ if (!mSprites.contains(icon)) {
+ mSprites.add(icon);
+ loadSprite(icon);
+ } else {
+ Sprite oldSprite = mSprites.get(mSprites.indexOf(icon));
+ if (!oldSprite.getBitmap().sameAs(icon.getBitmap())) {
+ throw new SpriteBitmapChangedException();
+ }
+ }
+ marker.setTopOffsetPixels(getTopOffsetPixelsForSprite(icon));
+ return marker;
+ }
+
+ /**
+ * Adds a marker to this map.
+ * <p/>
+ * The marker's icon is rendered on the map at the location {@code Marker.position}.
+ * If {@code Marker.title} is defined, the map shows an info box with the marker's title and snippet.
+ *
+ * @param markerOptions A marker options object that defines how to render the marker.
+ * @return The {@code Marker} that was added to the map.
+ */
+ @UiThread
+ @NonNull
+ public Marker addMarker(@NonNull MarkerOptions markerOptions) {
+ if (markerOptions == null) {
+ throw new NullPointerException("markerOptions is null");
+ }
+
+ Marker marker = prepareMarker(markerOptions);
+ long id = mNativeMapView.addMarker(marker);
+ marker.setId(id); // the annotation needs to know its id
+ marker.setMapView(this); // the annotation needs to know which map view it is in
+ mAnnotations.add(marker);
+ return marker;
+ }
+
+ /**
+ * Adds multiple markers to this map.
+ * <p/>
+ * The marker's icon is rendered on the map at the location {@code Marker.position}.
+ * If {@code Marker.title} is defined, the map shows an info box with the marker's title and snippet.
+ *
+ * @param markerOptionsList A list of marker options objects that defines how to render the markers.
+ * @return A list of the {@code Marker}s that were added to the map.
+ */
+ @UiThread
+ @NonNull
+ public List<Marker> addMarkers(@NonNull List<MarkerOptions> markerOptionsList) {
+ if (markerOptionsList == null) {
+ throw new NullPointerException("markerOptionsList is null");
+ }
+
+ int count = markerOptionsList.size();
+ List<Marker> markers = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ MarkerOptions markerOptions = markerOptionsList.get(i);
+ Marker marker = prepareMarker(markerOptions);
+ markers.add(marker);
+ }
+
+ long[] ids = mNativeMapView.addMarkers(markers);
+
+ Marker m;
+ for (int i = 0; i < count; i++) {
+ m = markers.get(i);
+ m.setId(ids[i]);
+ m.setMapView(this);
+ mAnnotations.add(m);
+ }
+
+ return new ArrayList<>(markers);
+ }
+
+ /**
+ * Adds a polyline to this map.
+ *
+ * @param polylineOptions A polyline options object that defines how to render the polyline.
+ * @return The {@code Polyine} that was added to the map.
+ */
+ @UiThread
+ @NonNull
+ public Polyline addPolyline(@NonNull PolylineOptions polylineOptions) {
+ if (polylineOptions == null) {
+ throw new NullPointerException("polylineOptions is null");
+ }
+
+ Polyline polyline = polylineOptions.getPolyline();
+ long id = mNativeMapView.addPolyline(polyline);
+ polyline.setId(id);
+ polyline.setMapView(this);
+ mAnnotations.add(polyline);
+ return polyline;
+ }
+
+ /**
+ * Adds multiple polylines to this map.
+ *
+ * @param polylineOptionsList A list of polyline options objects that defines how to render the polylines.
+ * @return A list of the {@code Polyline}s that were added to the map.
+ */
+ @UiThread
+ @NonNull
+ public List<Polyline> addPolylines(@NonNull List<PolylineOptions> polylineOptionsList) {
+ if (polylineOptionsList == null) {
+ throw new NullPointerException("polylineOptionsList is null");
+ }
+
+ // TODO make faster in JNI
+ int count = polylineOptionsList.size();
+ List<Polyline> polylines = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ PolylineOptions polylineOptions = polylineOptionsList.get(i);
+ polylines.add(addPolyline(polylineOptions));
+ }
+
+ return new ArrayList<>(polylines);
+ }
+
+ /**
+ * Adds a polygon to this map.
+ *
+ * @param polygonOptions A polygon options object that defines how to render the polygon.
+ * @return The {@code Polygon} that was added to the map.
+ */
+ @UiThread
+ @NonNull
+ public Polygon addPolygon(@NonNull PolygonOptions polygonOptions) {
+ if (polygonOptions == null) {
+ throw new NullPointerException("polygonOptions is null");
+ }
+
+ Polygon polygon = polygonOptions.getPolygon();
+ long id = mNativeMapView.addPolygon(polygon);
+ polygon.setId(id);
+ polygon.setMapView(this);
+ mAnnotations.add(polygon);
+ return polygon;
+ }
+
+
+ /**
+ * Adds multiple polygons to this map.
+ *
+ * @param polygonOptionsList A list of polygon options objects that defines how to render the polygons.
+ * @return A list of the {@code Polygon}s that were added to the map.
+ */
+ @UiThread
+ @NonNull
+ public List<Polygon> addPolygons(@NonNull List<PolygonOptions> polygonOptionsList) {
+ if (polygonOptionsList == null) {
+ throw new NullPointerException("polygonOptionsList is null");
+ }
+
+ int count = polygonOptionsList.size();
+ List<Polygon> polygons = new ArrayList<>(count);
+ for (PolygonOptions polygonOptions : polygonOptionsList) {
+ polygons.add(polygonOptions.getPolygon());
+ }
+
+ long[] ids = mNativeMapView.addPolygons(polygons);
+
+ Polygon p;
+ for (int i = 0; i < count; i++) {
+ p = polygons.get(i);
+ p.setId(ids[i]);
+ p.setMapView(this);
+ mAnnotations.add(p);
+ }
+
+ return new ArrayList<>(polygons);
+ }
+
+ /**
+ * Removes an annotation from the map.
+ *
+ * @param annotation The annotation object to remove.
+ */
+ @UiThread
+ public void removeAnnotation(@NonNull Annotation annotation) {
+ if (annotation == null) {
+ throw new NullPointerException("annotation is null");
+ }
+
+ if (annotation instanceof Marker) {
+ ((Marker) annotation).hideInfoWindow();
+ }
+ long id = annotation.getId();
+ mNativeMapView.removeAnnotation(id);
+ mAnnotations.remove(annotation);
+ }
+
+ /**
+ * Removes multiple annotations from the map.
+ *
+ * @param annotationList A list of annotation objects to remove.
+ */
+ @UiThread
+ public void removeAnnotations(@NonNull List<Annotation> annotationList) {
+ if (annotationList == null) {
+ throw new NullPointerException("annotationList is null");
+ }
+
+ // TODO make faster in JNI
+ int count = annotationList.size();
+ for (int i = 0; i < count; i++) {
+ Annotation annotation = annotationList.get(i);
+ removeAnnotation(annotation);
+ }
+ }
+
+ /**
+ * Removes all annotations from the map.
+ */
+ @UiThread
+ public void removeAllAnnotations() {
+ int count = mAnnotations.size();
+ long[] ids = new long[mAnnotations.size()];
+
+ for (int i = 0; i < count; i++) {
+ Annotation annotation = mAnnotations.get(i);
+ long id = annotation.getId();
+ ids[i] = id;
+ if (annotation instanceof Marker) {
+ ((Marker) annotation).hideInfoWindow();
+ }
+ }
+
+ mNativeMapView.removeAnnotations(ids);
+ mAnnotations.clear();
+ }
+
+ /**
+ * Returns a list of all the annotations on the map.
+ *
+ * @return A list of all the annotation objects. The returned object is a copy so modifying this
+ * list will not update the map.
+ */
+ @NonNull
+ public List<Annotation> getAllAnnotations() {
+ return new ArrayList<>(mAnnotations);
+ }
+
+ private List<Marker> getMarkersInBounds(@NonNull BoundingBox bbox) {
+ if (bbox == null) {
+ throw new NullPointerException("bbox is null");
+ }
+
+ // TODO: filter in JNI using C++ parameter to getAnnotationsInBounds
+ long[] ids = mNativeMapView.getAnnotationsInBounds(bbox);
+
+ List<Long> idsList = new ArrayList<>(ids.length);
+ for (int i = 0; i < ids.length; i++) {
+ idsList.add(ids[i]);
+ }
+
+ List<Marker> annotations = new ArrayList<>(ids.length);
+ int count = mAnnotations.size();
+ for (int i = 0; i < count; i++) {
+ Annotation annotation = mAnnotations.get(i);
+ if (annotation instanceof Marker && idsList.contains(annotation.getId())) {
+ annotations.add((Marker) annotation);
+ }
+ }
+
+ return new ArrayList<>(annotations);
+ }
+
+ private int getTopOffsetPixelsForSprite(Sprite sprite) {
+ // This method will dead lock if map paused. Causes a freeze if you add a marker in an
+ // activity's onCreate()
+ if (mNativeMapView.isPaused()) {
+ return 0;
+ }
+
+ return (int) (mNativeMapView.getTopOffsetPixelsForAnnotationSymbol(sprite.getId())
+ * mScreenDensity);
+ }
+
+ /**
+ * Returns the distance spanned by one pixel at the specified latitude and current zoom level.
+ * <p/>
+ * The distance between pixels decreases as the latitude approaches the poles.
+ * This relationship parallels the relationship between longitudinal coordinates at different latitudes.
+ *
+ * @param latitude The latitude for which to return the value.
+ * @return The distance measured in meters.
+ */
+ @UiThread
+ public double getMetersPerPixelAtLatitude(@FloatRange(from = -180, to = 180) double latitude) {
+ return mNativeMapView.getMetersPerPixelAtLatitude(latitude, getZoomLevel()) / mScreenDensity;
+ }
+
+ /**
+ * Selects a marker. The selected marker will have it's info window opened.
+ * Any other open info windows will be closed.
+ * <p/>
+ * Selecting an already selected marker will have no effect.
+ *
+ * @param marker The marker to select.
+ */
+ @UiThread
+ public void selectMarker(@NonNull Marker marker) {
+ if (marker == null) {
+ throw new NullPointerException("marker is null");
+ }
+
+ if (marker == mSelectedMarker) {
+ return;
+ }
+
+ // Need to deselect any currently selected annotation first
+ deselectMarker();
+
+ boolean handledDefaultClick = false;
+ if (mOnMarkerClickListener != null) {
+ // end developer has provided a custom click listener
+ handledDefaultClick = mOnMarkerClickListener.onMarkerClick(marker);
+ }
+
+ if (!handledDefaultClick) {
+ // default behaviour
+ // Can't do this as InfoWindow will get hidden
+ //setCenterCoordinate(marker.getPosition(), true);
+ marker.showInfoWindow();
+ }
+
+ mSelectedMarker = marker;
+ }
+
+ /**
+ * Deselects any currently selected marker. The selected marker will have it's info window closed.
+ */
+ @UiThread
+ public void deselectMarker() {
+ if (mSelectedMarker != null) {
+ if (mSelectedMarker.isInfoWindowShown()) {
+ mSelectedMarker.hideInfoWindow();
+ mSelectedMarker = null;
+ }
+ }
+ }
+
+ //
+ // Camera
+ //
+
+ /**
+ * Changes the map's viewport to fit the given coordinate bounds.
+ *
+ * @param bounds The bounds that the viewport will show in its entirety.
+ */
+ @UiThread
+ public void setVisibleCoordinateBounds(@NonNull CoordinateBounds bounds) {
+ setVisibleCoordinateBounds(bounds, false);
+ }
+
+ /**
+ * Changes the map's viewing area to fit the given coordinate bounds, optionally animating the change.
+ *
+ * @param bounds The bounds that the viewport will show in its entirety.
+ * @param animated If true, animates the change. If false, immediately changes the map.
+ */
+ @UiThread
+ public void setVisibleCoordinateBounds(@NonNull CoordinateBounds bounds, boolean animated) {
+ setVisibleCoordinateBounds(bounds, new RectF(), animated);
+ }
+
+ /**
+ * Changes the map’s viewport to fit the given coordinate bounds, optionally some additional padding on each side
+ * and animating the change.
+ *
+ * @param bounds The bounds that the viewport will show in its entirety.
+ * @param padding The minimum padding (in pixels) that will be visible around the given coordinate bounds.
+ * @param animated If true, animates the change. If false, immediately changes the map.
+ */
+ @UiThread
+ public void setVisibleCoordinateBounds(@NonNull CoordinateBounds bounds, @NonNull RectF padding, boolean animated) {
+ LatLng[] coordinates = {
+ new LatLng(bounds.getNorthEast().getLatitude(), bounds.getSouthWest().getLongitude()),
+ bounds.getSouthWest(),
+ new LatLng(bounds.getSouthWest().getLatitude(), bounds.getNorthEast().getLongitude()),
+ bounds.getNorthEast()
+
+ };
+ setVisibleCoordinateBounds(coordinates, padding, animated);
+ }
+
+ /**
+ * Changes the map’s viewport to fit the given coordinates, optionally some additional padding on each side
+ * and animating the change.
+ *
+ * @param coordinates The coordinates that the viewport will show.
+ * @param padding The minimum padding (in pixels) that will be visible around the given coordinate bounds.
+ * @param animated If true, animates the change. If false, immediately changes the map.
+ */
+ @UiThread
+ public void setVisibleCoordinateBounds(@NonNull LatLng[] coordinates, @NonNull RectF padding, boolean animated) {
+ setVisibleCoordinateBounds(coordinates, padding, getDirection(), animated);
+ }
+
+ private void setVisibleCoordinateBounds(LatLng[] coordinates, RectF padding, double direction, boolean animated) {
+ setVisibleCoordinateBounds(coordinates, padding, direction, animated ? ANIMATION_DURATION : 0l);
+ }
+
+ private void setVisibleCoordinateBounds(LatLng[] coordinates, RectF padding, double direction, long duration) {
+ mNativeMapView.setVisibleCoordinateBounds(coordinates, new RectF(padding.left / mScreenDensity,
+ padding.top / mScreenDensity, padding.right / mScreenDensity, padding.bottom / mScreenDensity),
+ direction, duration);
+ }
+
+ /**
+ * Gets the currently selected marker.
+ * @return The currently selected marker.
+ */
+ @UiThread
+ @Nullable
+ public Marker getSelectedMarker() {
+ return mSelectedMarker;
+ }
+
+ private void adjustTopOffsetPixels() {
+ int count = mAnnotations.size();
+ for (int i = 0; i < count; i++) {
+ Annotation annotation = mAnnotations.get(i);
+ if (annotation instanceof Marker) {
+ Marker marker = (Marker) annotation;
+ marker.setTopOffsetPixels(
+ getTopOffsetPixelsForSprite(marker.getIcon()));
+ }
+ }
+
+ if (mSelectedMarker != null) {
+ if (mSelectedMarker.isInfoWindowShown()) {
+ Marker temp = mSelectedMarker;
+ temp.hideInfoWindow();
+ temp.showInfoWindow();
+ mSelectedMarker = temp;
+ }
+ }
+ }
+
+ private void reloadMarkers() {
+ int count = mAnnotations.size();
+ for (int i = 0; i < count; i++) {
+ Annotation annotation = mAnnotations.get(i);
+ if (annotation instanceof Marker) {
+ Marker marker = (Marker) annotation;
+ mNativeMapView.removeAnnotation(annotation.getId());
+ long newId = mNativeMapView.addMarker(marker);
+ marker.setId(newId);
+ }
+ }
+ }
+
+ //
+ // Rendering
+ //
+
+ // Called when the map needs to be rerendered
+ // Called via JNI from NativeMapView
+ protected void onInvalidate() {
+ postInvalidate();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ if (!mNativeMapView.isPaused()) {
+ mNativeMapView.renderSync();
+ }
+ super.onDraw(canvas);
+ }
+
+ @Override
+ protected void onSizeChanged(int width, int height, int oldw, int oldh) {
+ if (!isInEditMode()) {
+ mNativeMapView.resizeView((int) (width / mScreenDensity), (int) (height / mScreenDensity));
+ }
+ }
+
+ // This class handles TextureView callbacks
+ private class SurfaceTextureListener implements TextureView.SurfaceTextureListener {
+
+ // Called when the native surface texture has been created
+ // Must do all EGL/GL ES initialization here
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ mNativeMapView.createSurface(new Surface(surface));
+ mNativeMapView.resizeFramebuffer(width, height);
+ }
+
+ // Called when the native surface texture has been destroyed
+ // Must do all EGL/GL ES destruction here
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ if (mNativeMapView != null) {
+ mNativeMapView.destroySurface();
+ }
+ return true;
+ }
+
+ // Called when the format or size of the native surface texture has been changed
+ // Must handle window resizing here.
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ mNativeMapView.resizeFramebuffer(width, height);
+ }
+
+ // Called when the SurfaceTexure frame is drawn to screen
+ // Must sync with UI here
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ mCompassView.update(getDirection());
+ mUserLocationView.update();
+ }
+ }
+
+ // Used by UserLocationView
+ void update() {
+ mNativeMapView.update();
+ }
+
+ //
+ // View events
+ //
+
+ // Called when view is no longer connected
+ @Override
+ @CallSuper
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ // Required by ZoomButtonController (from Android SDK documentation)
+ if (mZoomButtonsController != null) {
+ mZoomButtonsController.setVisible(false);
+ }
+ }
+
+ // Called when view is hidden and shown
+ @Override
+ protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
+ // Required by ZoomButtonController (from Android SDK documentation)
+ if ((mZoomButtonsController != null) && (visibility != View.VISIBLE)) {
+ mZoomButtonsController.setVisible(false);
+ }
+ if ((mZoomButtonsController != null) && (visibility == View.VISIBLE)
+ && mZoomEnabled) {
+ mZoomButtonsController.setVisible(true);
+ }
+ }
+
+ //
+ // Touch events
+ //
+
+ // Called when user touches the screen, all positions are absolute
+ @Override
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ // Check and ignore non touch or left clicks
+
+ if ((event.getButtonState() != 0) && (event.getButtonState() != MotionEvent.BUTTON_PRIMARY)) {
+ return false;
+ }
+
+ // Check two finger gestures first
+ mRotateGestureDetector.onTouchEvent(event);
+ mScaleGestureDetector.onTouchEvent(event);
+
+ // Handle two finger tap
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ // First pointer down
+ mNativeMapView.setGestureInProgress(true);
+ break;
+
+ case MotionEvent.ACTION_POINTER_DOWN:
+ // Second pointer down
+ mTwoTap = event.getPointerCount() == 2;
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ // Second pointer up
+ break;
+
+ case MotionEvent.ACTION_UP:
+ // First pointer up
+ long tapInterval = event.getEventTime() - event.getDownTime();
+ boolean isTap = tapInterval <= ViewConfiguration.getTapTimeout();
+ boolean inProgress = mRotateGestureDetector.isInProgress() || mScaleGestureDetector.isInProgress();
+
+ if (mTwoTap && isTap && !inProgress) {
+ PointF focalPoint = TwoFingerGestureDetector.determineFocalPoint(event);
+ zoom(false, focalPoint.x, focalPoint.y);
+ mTwoTap = false;
+ return true;
+ }
+
+ mTwoTap = false;
+ mNativeMapView.setGestureInProgress(false);
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ mTwoTap = false;
+ mNativeMapView.setGestureInProgress(false);
+ break;
+ }
+
+ boolean retVal = mGestureDetector.onTouchEvent(event);
+ return retVal || super.onTouchEvent(event);
+ }
+
+ // This class handles one finger gestures
+ private class GestureListener extends
+ GestureDetector.SimpleOnGestureListener {
+
+ // Must always return true otherwise all events are ignored
+ @Override
+ public boolean onDown(MotionEvent e) {
+ // Show the zoom controls
+ if ((mZoomButtonsController != null) && mZoomEnabled) {
+ mZoomButtonsController.setVisible(true);
+ }
+
+ return true;
+ }
+
+ // Called for double taps
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ if (!mZoomEnabled) {
+ return false;
+ }
+
+ // Single finger double tap
+ // Zoom in
+ zoom(true, e.getX(), e.getY());
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ // Open / Close InfoWindow
+ PointF tapPoint = new PointF(e.getX(), e.getY());
+
+ float toleranceWidth = 40 * mScreenDensity;
+ float toleranceHeight = 60 * mScreenDensity;
+
+ RectF tapRect = new RectF(tapPoint.x - toleranceWidth / 2, tapPoint.y - 2 * toleranceHeight / 3,
+ tapPoint.x + toleranceWidth / 2, tapPoint.y + 1 * toleranceHeight / 3);
+
+ List<LatLng> corners = Arrays.asList(
+ fromScreenLocation(new PointF(tapRect.left, tapRect.bottom)),
+ fromScreenLocation(new PointF(tapRect.left, tapRect.top)),
+ fromScreenLocation(new PointF(tapRect.right, tapRect.top)),
+ fromScreenLocation(new PointF(tapRect.right, tapRect.bottom))
+ );
+
+ BoundingBox tapBounds = BoundingBox.fromLatLngs(corners);
+
+ List<Marker> nearbyMarkers = getMarkersInBounds(tapBounds);
+
+ long newSelectedMarkerId;
+
+ if (nearbyMarkers.size() > 0) {
+
+ // there is at least one nearby marker; select one
+ //
+ // first, sort for comparison and iteration
+ Collections.sort(nearbyMarkers);
+
+ if (nearbyMarkers == mMarkersNearLastTap) {
+ // the selection candidates haven't changed; cycle through them
+ if (mSelectedMarker != null && (mSelectedMarker.getId() == mMarkersNearLastTap.get(mMarkersNearLastTap.size() - 1).getId())) {
+ // the selected marker is the last in the set; cycle back to the first
+ // note: this could be the selected marker if only one in set
+ newSelectedMarkerId = mMarkersNearLastTap.get(0).getId();
+ } else if (mSelectedMarker != null) {
+ // otherwise increment the selection through the candidates
+ long result = mMarkersNearLastTap.indexOf(mSelectedMarker);
+ newSelectedMarkerId = mMarkersNearLastTap.get((int) result + 1).getId();
+ } else {
+ // no current selection; select the first one
+ newSelectedMarkerId = mMarkersNearLastTap.get(0).getId();
+ }
+ } else {
+ // start tracking a new set of nearby markers
+ mMarkersNearLastTap = nearbyMarkers;
+
+ // select the first one
+ newSelectedMarkerId = mMarkersNearLastTap.get(0).getId();
+ }
+
+ } else {
+ // there are no nearby markers; deselect if necessary
+ newSelectedMarkerId = -1;
+ }
+
+ if (newSelectedMarkerId >= 0) {
+
+ int count = mAnnotations.size();
+ for (int i = 0; i < count; i++) {
+ Annotation annotation = mAnnotations.get(i);
+ if (annotation instanceof Marker) {
+ if (annotation.getId() == newSelectedMarkerId) {
+ if (mSelectedMarker == null || annotation.getId() != mSelectedMarker.getId()) {
+ selectMarker((Marker) annotation);
+ }
+ break;
+ }
+ }
+ }
+
+ } else {
+ // deselect any selected marker
+ deselectMarker();
+
+ // notify app of map click
+ if (mOnMapClickListener != null) {
+ LatLng point = fromScreenLocation(tapPoint);
+ mOnMapClickListener.onMapClick(point);
+ }
+ }
+
+ return true;
+ }
+
+ // Called for a long press
+ @Override
+ public void onLongPress(MotionEvent e) {
+ if (mOnMapLongClickListener != null) {
+ LatLng point = fromScreenLocation(new PointF(e.getX(), e.getY()));
+ mOnMapLongClickListener.onMapLongClick(point);
+ }
+ }
+
+ // Called for flings
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ if (!mScrollEnabled) {
+ return false;
+ }
+
+ // Fling the map
+ // TODO Google Maps also has a rotate and zoom fling
+
+ float ease = 0.25f;
+
+ velocityX = velocityX * ease;
+ velocityY = velocityY * ease;
+
+ double speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY);
+ double deceleration = 2500;
+ double duration = speed / (deceleration * ease);
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ mNativeMapView.moveBy(velocityX * duration / 2.0 / mScreenDensity, velocityY * duration / 2.0 / mScreenDensity, (long) (duration * 1000.0f));
+
+ if (mOnFlingListener != null) {
+ mOnFlingListener.onFling();
+ }
+
+ return true;
+ }
+
+ // Called for drags
+ // TODO use MoveGestureDetector
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ if (!mScrollEnabled) {
+ return false;
+ }
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions(); // TODO need to test canceling
+ // transitions with touch
+
+ // Scroll the map
+ mNativeMapView.moveBy(-distanceX / mScreenDensity, -distanceY / mScreenDensity);
+
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScroll();
+ }
+
+ return true;
+ }
+ }
+
+ // This class handles two finger gestures
+ private class ScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+
+ long mBeginTime = 0;
+ float mScaleFactor = 1.0f;
+ boolean mStarted = false;
+
+ // Called when two fingers first touch the screen
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ if (!mZoomEnabled) {
+ return false;
+ }
+
+ mBeginTime = detector.getEventTime();
+ return true;
+ }
+
+ // Called when fingers leave screen
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mBeginTime = 0;
+ mScaleFactor = 1.0f;
+ mStarted = false;
+ }
+
+ // Called each time one of the two fingers moves
+ // Called for pinch zooms
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ if (!mZoomEnabled) {
+ return false;
+ }
+
+ // If scale is large enough ignore a tap
+ // TODO: Google Maps seem to use a velocity rather than absolute
+ // value?
+ mScaleFactor *= detector.getScaleFactor();
+ if ((mScaleFactor > 1.05f) || (mScaleFactor < 0.95f)) {
+ mStarted = true;
+ }
+
+ // Ignore short touches in case it is a tap
+ // Also ignore small scales
+ long time = detector.getEventTime();
+ long interval = time - mBeginTime;
+ if (!mStarted && (interval <= ViewConfiguration.getTapTimeout())) {
+ return false;
+ }
+
+ // TODO complex decision between rotate or scale or both (see Google
+ // Maps app)
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Scale the map
+ mNativeMapView.scaleBy(detector.getScaleFactor(), detector.getFocusX() / mScreenDensity, detector.getFocusY() / mScreenDensity);
+
+ return true;
+ }
+ }
+
+ // This class handles two finger rotate gestures
+ // TODO need way to single finger rotate - need to research how google maps
+ // does this - for phones with single touch, or when using mouse etc
+ private class RotateGestureListener extends RotateGestureDetector.SimpleOnRotateGestureListener {
+
+ long mBeginTime = 0;
+ float mTotalAngle = 0.0f;
+ boolean mStarted = false;
+
+ // Called when two fingers first touch the screen
+ @Override
+ public boolean onRotateBegin(RotateGestureDetector detector) {
+ if (!mRotateEnabled) {
+ return false;
+ }
+
+ mBeginTime = detector.getEventTime();
+ return true;
+ }
+
+ // Called when the fingers leave the screen
+ @Override
+ public void onRotateEnd(RotateGestureDetector detector) {
+ mBeginTime = 0;
+ mTotalAngle = 0.0f;
+ mStarted = false;
+ }
+
+ // Called each time one of the two fingers moves
+ // Called for rotation
+ @Override
+ public boolean onRotate(RotateGestureDetector detector) {
+ if (!mRotateEnabled) {
+ return false;
+ }
+
+ // If rotate is large enough ignore a tap
+ // TODO: Google Maps seem to use a velocity rather than absolute
+ // value, up to a point then they always rotate
+ mTotalAngle += detector.getRotationDegreesDelta();
+ if ((mTotalAngle > 5.0f) || (mTotalAngle < -5.0f)) {
+ mStarted = true;
+ }
+
+ // Ignore short touches in case it is a tap
+ // Also ignore small rotate
+ long time = detector.getEventTime();
+ long interval = time - mBeginTime;
+ if (!mStarted && (interval <= ViewConfiguration.getTapTimeout())) {
+ return false;
+ }
+
+ // TODO complex decision between rotate or scale or both (see Google
+ // Maps app). It seems if you start one or the other it takes more
+ // to start the other too. Haven't figured out what it uses to
+ // decide when to transition to both at the same time.
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Rotate the map
+ double bearing = mNativeMapView.getBearing();
+ bearing += detector.getRotationDegreesDelta();
+ mNativeMapView.setBearing(bearing, detector.getFocusX() / mScreenDensity, detector.getFocusY() / mScreenDensity);
+
+ return true;
+ }
+ }
+
+ // This class handles input events from the zoom control buttons
+ // Zoom controls allow single touch only devices to zoom in and out
+ private class OnZoomListener implements ZoomButtonsController.OnZoomListener {
+
+ // Not used
+ @Override
+ public void onVisibilityChanged(boolean visible) {
+ // Ignore
+ }
+
+ // Called when user pushes a zoom button
+ @Override
+ public void onZoom(boolean zoomIn) {
+ if (!mZoomEnabled) {
+ return;
+ }
+
+ // Zoom in or out
+ zoom(zoomIn);
+ }
+ }
+
+ //
+ // Input events
+ //
+
+ // Called when the user presses a key, also called for repeating keys held
+ // down
+ @Override
+ public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
+ // If the user has held the scroll key down for a while then accelerate
+ // the scroll speed
+ double scrollDist = event.getRepeatCount() >= 5 ? 50.0 : 10.0;
+
+ // Check which key was pressed via hardware/real key code
+ switch (keyCode) {
+ // Tell the system to track these keys for long presses on
+ // onKeyLongPress is fired
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ event.startTracking();
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (!mScrollEnabled) {
+ return false;
+ }
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Move left
+ mNativeMapView.moveBy(scrollDist / mScreenDensity, 0.0 / mScreenDensity);
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (!mScrollEnabled) {
+ return false;
+ }
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Move right
+ mNativeMapView.moveBy(-scrollDist / mScreenDensity, 0.0 / mScreenDensity);
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (!mScrollEnabled) {
+ return false;
+ }
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Move up
+ mNativeMapView.moveBy(0.0 / mScreenDensity, scrollDist / mScreenDensity);
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (!mScrollEnabled) {
+ return false;
+ }
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Move down
+ mNativeMapView.moveBy(0.0 / mScreenDensity, -scrollDist / mScreenDensity);
+ return true;
+
+ default:
+ // We are not interested in this key
+ return super.onKeyUp(keyCode, event);
+ }
+ }
+
+ // Called when the user long presses a key that is being tracked
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ // Check which key was pressed via hardware/real key code
+ switch (keyCode) {
+ // Tell the system to track these keys for long presses on
+ // onKeyLongPress is fired
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (!mZoomEnabled) {
+ return false;
+ }
+
+ // Zoom out
+ zoom(false);
+ return true;
+
+ default:
+ // We are not interested in this key
+ return super.onKeyUp(keyCode, event);
+ }
+ }
+
+ // Called when the user releases a key
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ // Check if the key action was canceled (used for virtual keyboards)
+ if (event.isCanceled()) {
+ return super.onKeyUp(keyCode, event);
+ }
+
+ // Check which key was pressed via hardware/real key code
+ // Note if keyboard does not have physical key (ie primary non-shifted
+ // key) then it will not appear here
+ // Must use the key character map as physical to character is not
+ // fixed/guaranteed
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (!mZoomEnabled) {
+ return false;
+ }
+
+ // Zoom in
+ zoom(true);
+ return true;
+ }
+
+ // We are not interested in this key
+ return super.onKeyUp(keyCode, event);
+ }
+
+ // Called for trackball events, all motions are relative in device specific
+ // units
+ @Override
+ public boolean onTrackballEvent(MotionEvent event) {
+ // Choose the action
+ switch (event.getActionMasked()) {
+ // The trackball was rotated
+ case MotionEvent.ACTION_MOVE:
+ if (!mScrollEnabled) {
+ return false;
+ }
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Scroll the map
+ mNativeMapView.moveBy(-10.0 * event.getX() / mScreenDensity, -10.0 * event.getY() / mScreenDensity);
+ return true;
+
+ // Trackball was pushed in so start tracking and tell system we are
+ // interested
+ // We will then get the up action
+ case MotionEvent.ACTION_DOWN:
+ // Set up a delayed callback to check if trackball is still
+ // After waiting the system long press time out
+ if (mCurrentTrackballLongPressTimeOut != null) {
+ mCurrentTrackballLongPressTimeOut.cancel();
+ mCurrentTrackballLongPressTimeOut = null;
+ }
+ mCurrentTrackballLongPressTimeOut = new TrackballLongPressTimeOut();
+ postDelayed(mCurrentTrackballLongPressTimeOut,
+ ViewConfiguration.getLongPressTimeout());
+ return true;
+
+ // Trackball was released
+ case MotionEvent.ACTION_UP:
+ if (!mZoomEnabled) {
+ return false;
+ }
+
+ // Only handle if we have not already long pressed
+ if (mCurrentTrackballLongPressTimeOut != null) {
+ // Zoom in
+ zoom(true);
+ }
+ return true;
+
+ // Trackball was cancelled
+ case MotionEvent.ACTION_CANCEL:
+ if (mCurrentTrackballLongPressTimeOut != null) {
+ mCurrentTrackballLongPressTimeOut.cancel();
+ mCurrentTrackballLongPressTimeOut = null;
+ }
+ return true;
+
+ default:
+ // We are not interested in this event
+ return super.onTrackballEvent(event);
+ }
+ }
+
+ // This class implements the trackball long press time out callback
+ private class TrackballLongPressTimeOut implements Runnable {
+
+ // Track if we have been cancelled
+ private boolean cancelled;
+
+ public TrackballLongPressTimeOut() {
+ cancelled = false;
+ }
+
+ // Cancel the timeout
+ public void cancel() {
+ cancelled = true;
+ }
+
+ // Called when long press time out expires
+ @Override
+ public void run() {
+ // Check if the trackball is still pressed
+ if (!cancelled) {
+ // Zoom out
+ zoom(false);
+
+ // Ensure the up action is not run
+ mCurrentTrackballLongPressTimeOut = null;
+ }
+ }
+ }
+
+ // Called for events that don't fit the other handlers
+ // such as mouse scroll events, mouse moves, joystick, trackpad
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ // Mouse events
+ //if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) { // this is not available before API 18
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) == InputDevice.SOURCE_CLASS_POINTER) {
+ // Choose the action
+ switch (event.getActionMasked()) {
+ // Mouse scrolls
+ case MotionEvent.ACTION_SCROLL:
+ if (!mZoomEnabled) {
+ return false;
+ }
+
+ // Cancel any animation
+ mNativeMapView.cancelTransitions();
+
+ // Get the vertical scroll amount, one click = 1
+ float scrollDist = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+
+ // Scale the map by the appropriate power of two factor
+ mNativeMapView.scaleBy(Math.pow(2.0, scrollDist), event.getX() / mScreenDensity, event.getY() / mScreenDensity);
+
+ return true;
+
+ default:
+ // We are not interested in this event
+ return super.onGenericMotionEvent(event);
+ }
+ }
+
+ // We are not interested in this event
+ return super.onGenericMotionEvent(event);
+ }
+
+ // Called when the mouse pointer enters or exits the view
+ // or when it fades in or out due to movement
+ @Override
+ public boolean onHoverEvent(@NonNull MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ case MotionEvent.ACTION_HOVER_MOVE:
+ // Show the zoom controls
+ if ((mZoomButtonsController != null) && mZoomEnabled) {
+ mZoomButtonsController.setVisible(true);
+ }
+ return true;
+
+ case MotionEvent.ACTION_HOVER_EXIT:
+ // Hide the zoom controls
+ if (mZoomButtonsController != null) {
+ mZoomButtonsController.setVisible(false);
+ }
+
+ default:
+ // We are not interested in this event
+ return super.onHoverEvent(event);
+ }
+ }
+
+ //
+ // Connectivity events
+ //
+
+ // This class handles connectivity changes
+ private class ConnectivityReceiver extends BroadcastReceiver {
+
+ // Called when an action we are listening to in the manifest has been sent
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+ boolean noConnectivity = intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
+ onConnectivityChanged(!noConnectivity);
+ }
+ }
+ }
+
+ // Called when our Internet connectivity has changed
+ private void onConnectivityChanged(boolean isConnected) {
+ mNativeMapView.setReachability(isConnected);
+ }
+
+ //
+ // Map events
+ //
+
+ /**
+ * Add a callback that's invoked when the displayed map view changes.
+ * <p/>
+ * To remove the callback, use {@link MapView#removeOnMapChangedListener(OnMapChangedListener)}.
+ *
+ * @param listener The callback that's invoked on every frame rendered to the map view.
+ * @see MapView#removeOnMapChangedListener(OnMapChangedListener)
+ */
+ @UiThread
+ public void addOnMapChangedListener(@Nullable OnMapChangedListener listener) {
+ if (listener != null) {
+ mOnMapChangedListener.add(listener);
+ }
+ }
+
+ /**
+ * Remove a callback added with {@link MapView#addOnMapChangedListener(OnMapChangedListener)}
+ *
+ * @param listener The previously added callback to remove.
+ * @see MapView#addOnMapChangedListener(OnMapChangedListener)
+ */
+ @UiThread
+ public void removeOnMapChangedListener(@Nullable OnMapChangedListener listener) {
+ if (listener != null) {
+ mOnMapChangedListener.remove(listener);
+ }
+ }
+
+ // Called when the map view transformation has changed
+ // Called via JNI from NativeMapView
+ // Forward to any listeners
+ protected void onMapChanged(int mapChange) {
+ if (mOnMapChangedListener != null) {
+ int count = mOnMapChangedListener.size();
+ for (int i = 0; i < count; i++) {
+ mOnMapChangedListener.get(i).onMapChanged(mapChange);
+ }
+ }
+ }
+
+ /**
+ * Sets a custom renderer for the contents of info window.
+ * <p/>
+ * When set your callback is invoked when an info window is about to be shown. By returning
+ * a custom {@link View}, the default info window will be replaced.
+ *
+ * @param infoWindowAdapter The callback to be invoked when an info window will be shown.
+ * To unset the callback, use null.
+ */
+ @UiThread
+ public void setInfoWindowAdapter(@Nullable InfoWindowAdapter infoWindowAdapter) {
+ mInfoWindowAdapter = infoWindowAdapter;
+ }
+
+ /**
+ * Gets the callback to be invoked when an info window will be shown.
+ *
+ * @return The callback to be invoked when an info window will be shown.
+ */
+ @UiThread
+ @Nullable
+ public InfoWindowAdapter getInfoWindowAdapter() {
+ return mInfoWindowAdapter;
+ }
+
+
+
+ /**
+ * Sets a callback that's invoked on every frame rendered to the map view.
+ *
+ * @param listener The callback that's invoked on every frame rendered to the map view.
+ * To unset the callback, use null.
+ */
+ @UiThread
+ public void setOnFpsChangedListener(@Nullable OnFpsChangedListener listener) {
+ mOnFpsChangedListener = listener;
+ }
+
+ // Called when debug mode is enabled to update a FPS counter
+ // Called via JNI from NativeMapView
+ // Forward to any listener
+ protected void onFpsChanged(final double fps) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ if (mOnFpsChangedListener != null) {
+ mOnFpsChangedListener.onFpsChanged(fps);
+ }
+ }
+ });
+ }
+
+ /**
+ * Sets a callback that's invoked when the map is scrolled.
+ *
+ * @param listener The callback that's invoked when the map is scrolled.
+ * To unset the callback, use null.
+ */
+ @UiThread
+ public void setOnScrollListener(@Nullable OnScrollListener listener) {
+ mOnScrollListener = listener;
+ }
+
+ /**
+ * Sets a callback that's invoked when the map is flinged.
+ *
+ * @param listener The callback that's invoked when the map is flinged.
+ * To unset the callback, use null.
+ */
+ @UiThread
+ public void setOnFlingListener(@Nullable OnFlingListener listener) {
+ mOnFlingListener = listener;
+ }
+
+ /**
+ * Sets a callback that's invoked when the user clicks on the map view.
+ *
+ * @param listener The callback that's invoked when the user clicks on the map view.
+ * To unset the callback, use null.
+ */
+ @UiThread
+ public void setOnMapClickListener(@Nullable OnMapClickListener listener) {
+ mOnMapClickListener = listener;
+ }
+
+ /**
+ * Sets a callback that's invoked when the user long clicks on the map view.
+ *
+ * @param listener The callback that's invoked when the user long clicks on the map view.
+ * To unset the callback, use null.
+ */
+ @UiThread
+ public void setOnMapLongClickListener(@Nullable OnMapLongClickListener listener) {
+ mOnMapLongClickListener = listener;
+ }
+
+ /**
+ * Sets a callback that's invoked when the user clicks on a marker.
+ *
+ * @param listener The callback that's invoked when the user clicks on a marker.
+ * To unset the callback, use null.
+ */
+ @UiThread
+ public void setOnMarkerClickListener(@Nullable OnMarkerClickListener listener) {
+ mOnMarkerClickListener = listener;
+ }
+
+ /**
+ * Sets a callback that's invoked when the user clicks on an info window.
+ *
+ * @return The callback that's invoked when the user clicks on an info window.
+ */
+ @UiThread
+ @Nullable
+ public OnInfoWindowClickListener getOnInfoWindowClickListener() {
+ return mOnInfoWindowClickListener;
+ }
+
+ /**
+ * Sets a callback that's invoked when the user clicks on an info window.
+ *
+ * @param listener The callback that's invoked when the user clicks on an info window.
+ * To unset the callback, use null.
+ */
+ @UiThread
+ public void setOnInfoWindowClickListener(@Nullable OnInfoWindowClickListener listener) {
+ mOnInfoWindowClickListener = listener;
+ }
+
+ //
+ // User location
+ //
+
+ /**
+ * Returns the status of the my-location layer.
+ *
+ * @return True if the my-location layer is enabled, false otherwise.
+ */
+ @UiThread
+ public boolean isMyLocationEnabled() {
+ return mUserLocationView.isEnabled();
+ }
+
+ /**
+ * Enables or disables the my-location layer.
+ * While enabled, the my-location layer continuously draws an indication of a user's current
+ * location and bearing.
+ * <p/>
+ * In order to use the my-location layer feature you need to request permission for either
+ * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}
+ * or @link android.Manifest.permission#ACCESS_FINE_LOCATION.
+ *
+ * @param enabled True to enable; false to disable.
+ */
+ @UiThread
+ public void setMyLocationEnabled(boolean enabled) {
+ mUserLocationView.setEnabled(enabled);
+ }
+
+ /**
+ * Returns the currently displayed user location, or null if there is no location data available.
+ *
+ * @return The currently displayed user location.
+ */
+ @UiThread
+ @Nullable
+ public Location getMyLocation() {
+ return mUserLocationView.getLocation();
+ }
+
+ /**
+ * Sets a callback that's invoked when the the My Location dot
+ * (which signifies the user's location) changes location.
+ *
+ * @param listener The callback that's invoked when the user clicks on a marker.
+ * To unset the callback, use null.
+ */
+ @UiThread
+ public void setOnMyLocationChangeListener(@Nullable OnMyLocationChangeListener listener) {
+ mUserLocationView.setOnMyLocationChangeListener(listener);
+ }
+
+ //
+ // Compass
+ //
+
+ /**
+ * Returns whether the compass is enabled.
+ *
+ * @return True if the compass is enabled; false if the compass is disabled.
+ */
+ @UiThread
+ public boolean isCompassEnabled() {
+ return mCompassView.isEnabled();
+ }
+
+ /**
+ * Enables or disables the compass. The compass is an icon on the map that indicates the
+ * direction of north on the map. When a user clicks
+ * the compass, the camera orients itself to its default orientation and fades away shortly
+ * after. If disabled, the compass will never be displayed.
+ * <p/>
+ * By default, the compass is enabled.
+ *
+ * @param compassEnabled True to enable the compass; false to disable the compass.
+ */
+ @UiThread
+ public void setCompassEnabled(boolean compassEnabled) {
+ mCompassView.setEnabled(compassEnabled);
+ }
+
+ /**
+ * Sets the gravity of the compass view. Use this to change the corner of the map view that the
+ * compass is displayed in.
+ * <p/>
+ * By default, the compass is in the top right corner.
+ *
+ * @param gravity One of the values from {@link Gravity}.
+ * @see Gravity
+ */
+ @UiThread
+ public void setCompassGravity(int gravity) {
+ setWidgetGravity(mCompassView, gravity);
+ }
+
+ /**
+ * Sets the margins of the compass view. Use this to change the distance of the compass from the
+ * map view edge.
+ *
+ * @param left The left margin in pixels.
+ * @param top The top margin in pixels.
+ * @param right The right margin in pixels.
+ * @param bottom The bottom margin in pixels.
+ */
+ @UiThread
+ public void setCompassMargins(int left, int top, int right, int bottom) {
+ setWidgetMargins(mCompassView, left, top, right, bottom);
+ }
+
+ //
+ // Logo
+ //
+
+ /**
+ * Sets the gravity of the logo view. Use this to change the corner of the map view that the
+ * Mapbox logo is displayed in.
+ * <p/>
+ * By default, the logo is in the bottom left corner.
+ *
+ * @param gravity One of the values from {@link Gravity}.
+ * @see Gravity
+ */
+ @UiThread
+ public void setLogoGravity(int gravity) {
+ setWidgetGravity(mLogoView, gravity);
+ }
+
+ /**
+ * Sets the margins of the logo view. Use this to change the distance of the Mapbox logo from the
+ * map view edge.
+ *
+ * @param left The left margin in pixels.
+ * @param top The top margin in pixels.
+ * @param right The right margin in pixels.
+ * @param bottom The bottom margin in pixels.
+ */
+ @UiThread
+ public void setLogoMargins(int left, int top, int right, int bottom) {
+ setWidgetMargins(mLogoView, left, top, right, bottom);
+ }
+
+ /**
+ * Enables or disables the Mapbox logo.
+ * <p/>
+ * By default, the compass is enabled.
+ *
+ * @param visibility True to enable the logo; false to disable the logo.
+ */
+ @UiThread
+ public void setLogoVisibility(int visibility) {
+ mLogoView.setVisibility(visibility);
+ }
+
+ //
+ // Attribution
+ //
+
+ /**
+ * Sets the gravity of the attribution button view. Use this to change the corner of the map
+ * view that the attribution button is displayed in.
+ * <p/>
+ * By default, the attribution button is in the bottom left corner.
+ *
+ * @param gravity One of the values from {@link Gravity}.
+ * @see Gravity
+ */
+ @UiThread
+ public void setAttributionGravity(int gravity) {
+ setWidgetGravity(mAttributionsView, gravity);
+ }
+
+ /**
+ * Sets the margins of the attribution button view. Use this to change the distance of the
+ * attribution button from the map view edge.
+ *
+ * @param left The left margin in pixels.
+ * @param top The top margin in pixels.
+ * @param right The right margin in pixels.
+ * @param bottom The bottom margin in pixels.
+ */
+ @UiThread
+ public void setAttributionMargins(int left, int top, int right, int bottom) {
+ setWidgetMargins(mAttributionsView, left, top, right, bottom);
+ }
+
+ /**
+ * Enables or disables the attribution button. The attribution is a button with an "i" than when
+ * clicked shows a menu with copyright and legal notices. The menu also inlcudes the "Improve
+ * this map" link which user can report map errors with.
+ * <p/>
+ * By default, the attribution button is enabled.
+ *
+ * @param visibility True to enable the attribution button; false to disable the attribution button.
+ */
+ @UiThread
+ public void setAttributionVisibility(int visibility) {
+ mAttributionsView.setVisibility(visibility);
+ }
+
+ private void setWidgetGravity(@NonNull final View view, int gravity) {
+ LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
+ layoutParams.gravity = gravity;
+ view.setLayoutParams(layoutParams);
+ }
+
+ private void setWidgetMargins(@NonNull final View view, int left, int top, int right, int bottom) {
+ LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
+ layoutParams.setMargins(left, top, right, bottom);
+ view.setLayoutParams(layoutParams);
+ }
+
+ private void setWidgetMargins(@NonNull final View view, float leftDp, float topDp, float rightDp, float bottomDp) {
+ LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
+ layoutParams.setMargins((int) (leftDp * mScreenDensity), (int) (topDp * mScreenDensity), (int) (rightDp * mScreenDensity), (int) (bottomDp * mScreenDensity));
+ view.setLayoutParams(layoutParams);
+ }
+
+ private static class AttributionOnClickListener implements View.OnClickListener, DialogInterface.OnClickListener {
+
+ private MapView mMapView;
+
+ public AttributionOnClickListener(MapView mapView) {
+ mMapView = mapView;
+ }
+
+ // Called when someone presses the attribution icon
+ @Override
+ public void onClick(View v) {
+ Context context = v.getContext();
+ String[] items = context.getResources().getStringArray(R.array.attribution_names);
+ AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.AttributionAlertDialogStyle);
+ builder.setTitle(R.string.attributionsDialogTitle);
+ builder.setAdapter(new ArrayAdapter<>(context, R.layout.attribution_list_item, items), this);
+ builder.show();
+ }
+
+ // Called when someone selects an attribution, 'Improve this map' adds location data to the url
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Context context = ((Dialog) dialog).getContext();
+ String url = context.getResources().getStringArray(R.array.attribution_links)[which];
+ if (which == ATTRIBUTION_INDEX_IMPROVE_THIS_MAP) {
+ LatLng latLng = mMapView.getCenterCoordinate();
+ url = String.format(url, latLng.getLongitude(), latLng.getLatitude(), (int) mMapView.getZoomLevel());
+ }
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(url));
+ context.startActivity(intent);
+ }
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/NativeMapView.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/NativeMapView.java
new file mode 100644
index 0000000000..da13efcf87
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/NativeMapView.java
@@ -0,0 +1,632 @@
+package com.mapbox.mapboxsdk.views;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.view.Surface;
+
+import com.mapbox.mapboxsdk.annotations.Marker;
+import com.mapbox.mapboxsdk.annotations.Polygon;
+import com.mapbox.mapboxsdk.annotations.Polyline;
+import com.mapbox.mapboxsdk.geometry.BoundingBox;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngZoom;
+import com.mapbox.mapboxsdk.geometry.ProjectedMeters;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+// Class that wraps the native methods for convenience
+final class NativeMapView {
+
+ //
+ // Static members
+ //
+
+ //
+ // Instance members
+ //
+
+ boolean mDestroyed = false;
+
+ // Holds the pointer to JNI NativeMapView
+ private long mNativeMapViewPtr = 0;
+
+ // Used for callbacks
+ private WeakReference<MapView> mMapView;
+
+ //
+ // Static methods
+ //
+
+ static {
+ System.loadLibrary("mapbox-gl");
+ }
+
+ //
+ // Constructors
+ //
+
+ public NativeMapView(MapView mapView, String cachePath, String dataPath, String apkPath, float pixelRatio, int availableProcessors, long totalMemory) {
+ if (availableProcessors < 0) {
+ throw new IllegalArgumentException("availableProcessors cannot be negative.");
+ }
+
+ if (totalMemory < 0) {
+ throw new IllegalArgumentException("totalMemory cannot be negative.");
+ }
+
+ mMapView = new WeakReference<>(mapView);
+
+ // Create the NativeMapView
+ mNativeMapViewPtr = nativeCreate(cachePath, dataPath, apkPath, pixelRatio, availableProcessors, totalMemory);
+ }
+
+ //
+ // Methods
+ //
+
+ public void destroy() {
+ nativeDestroy(mNativeMapViewPtr);
+ mNativeMapViewPtr = 0;
+ mMapView = null;
+ mDestroyed = true;
+ }
+
+ public boolean wasDestroyed() {
+ return mDestroyed;
+ }
+
+ public void initializeDisplay() {
+ nativeInitializeDisplay(mNativeMapViewPtr);
+ }
+
+ public void terminateDisplay() {
+ nativeTerminateDisplay(mNativeMapViewPtr);
+ }
+
+ public void initializeContext() {
+ nativeInitializeContext(mNativeMapViewPtr);
+ }
+
+ public void terminateContext() {
+ nativeTerminateContext(mNativeMapViewPtr);
+ }
+
+ public void createSurface(Surface surface) {
+ nativeCreateSurface(mNativeMapViewPtr, surface);
+ }
+
+ public void destroySurface() {
+ nativeDestroySurface(mNativeMapViewPtr);
+ }
+
+ public void pause() {
+ nativePause(mNativeMapViewPtr);
+ }
+
+ public boolean isPaused() {
+ return nativeIsPaused(mNativeMapViewPtr);
+ }
+
+ public void resume() {
+ nativeResume(mNativeMapViewPtr);
+ }
+
+ public void update() {
+ nativeUpdate(mNativeMapViewPtr);
+ }
+
+ public void renderSync() {
+ nativeRenderSync(mNativeMapViewPtr);
+ }
+
+ public void resizeView(int width, int height) {
+ if (width < 0) {
+ throw new IllegalArgumentException("width cannot be negative.");
+ }
+
+ if (height < 0) {
+ throw new IllegalArgumentException("height cannot be negative.");
+ }
+
+ if (width > 65535) {
+ throw new IllegalArgumentException(
+ "width cannot be greater than 65535.");
+ }
+
+ if (height > 65535) {
+ throw new IllegalArgumentException(
+ "height cannot be greater than 65535.");
+ }
+ nativeViewResize(mNativeMapViewPtr, width, height);
+ }
+
+ public void resizeFramebuffer(int fbWidth, int fbHeight) {
+ if (fbWidth < 0) {
+ throw new IllegalArgumentException("fbWidth cannot be negative.");
+ }
+
+ if (fbHeight < 0) {
+ throw new IllegalArgumentException("fbHeight cannot be negative.");
+ }
+
+ if (fbWidth > 65535) {
+ throw new IllegalArgumentException(
+ "fbWidth cannot be greater than 65535.");
+ }
+
+ if (fbHeight > 65535) {
+ throw new IllegalArgumentException(
+ "fbHeight cannot be greater than 65535.");
+ }
+ nativeFramebufferResize(mNativeMapViewPtr, fbWidth, fbHeight);
+ }
+
+ public void addClass(String clazz) {
+ nativeAddClass(mNativeMapViewPtr, clazz);
+ }
+
+ public void removeClass(String clazz) {
+ nativeRemoveClass(mNativeMapViewPtr, clazz);
+ }
+
+ public boolean hasClass(String clazz) {
+ return nativeHasClass(mNativeMapViewPtr, clazz);
+ }
+
+ public void setClasses(List<String> classes) {
+ nativeSetClasses(mNativeMapViewPtr, classes);
+ }
+
+ public List<String> getClasses() {
+ return nativeGetClasses(mNativeMapViewPtr);
+ }
+
+ public void setDefaultTransitionDuration() {
+ setDefaultTransitionDuration(0);
+ }
+
+ public long getDefaultTransitionDuration() {
+ return nativeGetDefaultTransitionDuration(mNativeMapViewPtr);
+ }
+
+ public void setDefaultTransitionDuration(long milliseconds) {
+ if (milliseconds < 0) {
+ throw new IllegalArgumentException(
+ "milliseconds cannot be negative.");
+ }
+
+ nativeSetDefaultTransitionDuration(mNativeMapViewPtr,
+ milliseconds);
+ }
+
+ public void setStyleUrl(String url) {
+ nativeSetStyleUrl(mNativeMapViewPtr, url);
+ }
+
+ public void setStyleJson(String newStyleJson) {
+ setStyleJson(newStyleJson, "");
+ }
+
+ public void setStyleJson(String newStyleJson, String base) {
+ nativeSetStyleJson(mNativeMapViewPtr, newStyleJson, base);
+ }
+
+ public String getStyleJson() {
+ return nativeGetStyleJson(mNativeMapViewPtr);
+ }
+
+ public void setAccessToken(String accessToken) {
+ nativeSetAccessToken(mNativeMapViewPtr, accessToken);
+ }
+
+ public String getAccessToken() {
+ return nativeGetAccessToken(mNativeMapViewPtr);
+ }
+
+ public void cancelTransitions() {
+ nativeCancelTransitions(mNativeMapViewPtr);
+ }
+
+ public void setGestureInProgress(boolean inProgress) {
+ nativeSetGestureInProgress(mNativeMapViewPtr, inProgress);
+ }
+
+ public void moveBy(double dx, double dy) {
+ moveBy(dx, dy, 0);
+ }
+
+ public void moveBy(double dx, double dy, long duration) {
+ nativeMoveBy(mNativeMapViewPtr, dx, dy, duration);
+ }
+
+ public void setLatLng(LatLng latLng) {
+ setLatLng(latLng, 0);
+ }
+
+ public void setLatLng(LatLng latLng, long duration) {
+ nativeSetLatLng(mNativeMapViewPtr, latLng, duration);
+ }
+
+ public LatLng getLatLng() {
+ return nativeGetLatLng(mNativeMapViewPtr);
+ }
+
+ public void resetPosition() {
+ nativeResetPosition(mNativeMapViewPtr);
+ }
+
+ public void scaleBy(double ds) {
+ scaleBy(ds, -1.0, -1.0);
+ }
+
+ public void scaleBy(double ds, double cx, double cy) {
+ scaleBy(ds, cx, cy, 0);
+ }
+
+ public void scaleBy(double ds, double cx, double cy, long duration) {
+ nativeScaleBy(mNativeMapViewPtr, ds, cx, cy, duration);
+ }
+
+ public void setScale(double scale) {
+ setScale(scale, -1.0, -1.0);
+ }
+
+ public void setScale(double scale, double cx, double cy) {
+ setScale(scale, cx, cy, 0);
+ }
+
+ public void setScale(double scale, double cx, double cy, long duration) {
+ nativeSetScale(mNativeMapViewPtr, scale, cx, cy, duration);
+ }
+
+ public double getScale() {
+ return nativeGetScale(mNativeMapViewPtr);
+ }
+
+ public void setZoom(double zoom) {
+ setZoom(zoom, 0);
+ }
+
+ public void setZoom(double zoom, long duration) {
+ nativeSetZoom(mNativeMapViewPtr, zoom, duration);
+ }
+
+ public double getZoom() {
+ return nativeGetZoom(mNativeMapViewPtr);
+ }
+
+ public void setLatLngZoom(LatLngZoom latLngZoom) {
+ setLatLngZoom(latLngZoom, 0);
+ }
+
+ public void setLatLngZoom(LatLngZoom latLngZoom, long duration) {
+ nativeSetLatLngZoom(mNativeMapViewPtr, latLngZoom, duration);
+ }
+
+ public LatLngZoom getLatLngZoom() {
+ return nativeGetLatLngZoom(mNativeMapViewPtr);
+ }
+
+ public void resetZoom() {
+ nativeResetZoom(mNativeMapViewPtr);
+ }
+
+ public double getMinZoom() {
+ return nativeGetMinZoom(mNativeMapViewPtr);
+ }
+
+ public double getMaxZoom() {
+ return nativeGetMaxZoom(mNativeMapViewPtr);
+ }
+
+ public void rotateBy(double sx, double sy, double ex, double ey) {
+ rotateBy(sx, sy, ex, ey, 0);
+ }
+
+ public void rotateBy(double sx, double sy, double ex, double ey,
+ long duration) {
+ nativeRotateBy(mNativeMapViewPtr, sx, sy, ex, ey, duration);
+ }
+
+ public void setBearing(double degrees) {
+ setBearing(degrees, 0);
+ }
+
+ public void setBearing(double degrees, long duration) {
+ nativeSetBearing(mNativeMapViewPtr, degrees, duration);
+ }
+
+ public void setBearing(double degrees, double cx, double cy) {
+ nativeSetBearing(mNativeMapViewPtr, degrees, cx, cy);
+ }
+
+ public double getBearing() {
+ return nativeGetBearing(mNativeMapViewPtr);
+ }
+
+ public void resetNorth() {
+ nativeResetNorth(mNativeMapViewPtr);
+ }
+
+ public long addMarker(Marker marker) {
+ return nativeAddMarker(mNativeMapViewPtr, marker);
+ }
+
+ public long[] addMarkers(List<Marker> markers) {
+ return nativeAddMarkers(mNativeMapViewPtr, markers);
+ }
+
+ public long addPolyline(Polyline polyline) {
+ return nativeAddPolyline(mNativeMapViewPtr, polyline);
+ }
+
+ public long addPolygon(Polygon polygon) {
+ return nativeAddPolygon(mNativeMapViewPtr, polygon);
+ }
+
+ public long[] addPolygons(List<Polygon> polygon) {
+ return nativeAddPolygons(mNativeMapViewPtr, polygon);
+ }
+
+ public void removeAnnotation(long id) {
+ nativeRemoveAnnotation(mNativeMapViewPtr, id);
+ }
+
+ public void removeAnnotations(long[] ids) {
+ nativeRemoveAnnotations(mNativeMapViewPtr, ids);
+ }
+
+ public long[] getAnnotationsInBounds(BoundingBox bbox) {
+ return nativeGetAnnotationsInBounds(mNativeMapViewPtr, bbox);
+ }
+
+ public void setSprite(String symbol, int width, int height, float scale, byte[] pixels) {
+ nativeSetSprite(mNativeMapViewPtr, symbol, width, height, scale, pixels);
+ }
+
+ public void setVisibleCoordinateBounds(LatLng[] coordinates, RectF padding, double direction, long duration) {
+ nativeSetVisibleCoordinateBounds(mNativeMapViewPtr, coordinates, padding, direction, duration);
+ }
+
+ public void onLowMemory() {
+ nativeOnLowMemory(mNativeMapViewPtr);
+ }
+
+ public void setDebug(boolean debug) {
+ nativeSetDebug(mNativeMapViewPtr, debug);
+ }
+
+ public void toggleDebug() {
+ nativeToggleDebug(mNativeMapViewPtr);
+ }
+
+ public boolean getDebug() {
+ return nativeGetDebug(mNativeMapViewPtr);
+ }
+
+ public void setCollisionDebug(boolean debug) {
+ nativeSetCollisionDebug(mNativeMapViewPtr, debug);
+ }
+
+ public void toggleCollisionDebug() {
+ nativeToggleCollisionDebug(mNativeMapViewPtr);
+ }
+
+ public boolean getCollisionDebug() {
+ return nativeGetCollisionDebug(mNativeMapViewPtr);
+ }
+
+ public boolean isFullyLoaded() {
+ return nativeIsFullyLoaded(mNativeMapViewPtr);
+ }
+
+ public void setReachability(boolean status) {
+ nativeSetReachability(mNativeMapViewPtr, status);
+ }
+
+ public double getMetersPerPixelAtLatitude(double lat, double zoom) {
+ return nativeGetMetersPerPixelAtLatitude(mNativeMapViewPtr, lat, zoom);
+ }
+
+ public ProjectedMeters projectedMetersForLatLng(LatLng latLng) {
+ return nativeProjectedMetersForLatLng(mNativeMapViewPtr, latLng);
+ }
+
+ public LatLng latLngForProjectedMeters(ProjectedMeters projectedMeters) {
+ return nativeLatLngForProjectedMeters(mNativeMapViewPtr, projectedMeters);
+ }
+
+ public PointF pixelForLatLng(LatLng latLng) {
+ return nativePixelForLatLng(mNativeMapViewPtr, latLng);
+ }
+
+ public LatLng latLngForPixel(PointF pixel) {
+ return nativeLatLngForPixel(mNativeMapViewPtr, pixel);
+ }
+
+ public double getTopOffsetPixelsForAnnotationSymbol(String symbolName) {
+ return nativeGetTopOffsetPixelsForAnnotationSymbol(mNativeMapViewPtr, symbolName);
+ }
+
+ //
+ // Callbacks
+ //
+
+ protected void onInvalidate() {
+ mMapView.get().onInvalidate();
+ }
+
+ protected void onMapChanged(int rawChange) {
+ mMapView.get().onMapChanged(rawChange);
+ }
+
+ protected void onFpsChanged(double fps) {
+ mMapView.get().onFpsChanged(fps);
+ }
+
+ //
+ // JNI methods
+ //
+
+ private native long nativeCreate(String cachePath, String dataPath, String apkPath, float pixelRatio, int availableProcessors, long totalMemory);
+
+ private native void nativeDestroy(long nativeMapViewPtr);
+
+ private native void nativeInitializeDisplay(long nativeMapViewPtr);
+
+ private native void nativeTerminateDisplay(long nativeMapViewPtr);
+
+ private native void nativeInitializeContext(long nativeMapViewPtr);
+
+ private native void nativeTerminateContext(long nativeMapViewPtr);
+
+ private native void nativeCreateSurface(long nativeMapViewPtr,
+ Surface surface);
+
+ private native void nativeDestroySurface(long nativeMapViewPtr);
+
+ private native void nativePause(long nativeMapViewPtr);
+
+ private native boolean nativeIsPaused(long nativeMapViewPtr);
+
+ private native void nativeResume(long nativeMapViewPtr);
+
+ private native void nativeUpdate(long nativeMapViewPtr);
+
+ private native void nativeRenderSync(long nativeMapViewPtr);
+
+ private native void nativeViewResize(long nativeMapViewPtr, int width, int height);
+
+ private native void nativeFramebufferResize(long nativeMapViewPtr, int fbWidth, int fbHeight);
+
+ private native void nativeAddClass(long nativeMapViewPtr, String clazz);
+
+ private native void nativeRemoveClass(long nativeMapViewPtr, String clazz);
+
+ private native boolean nativeHasClass(long nativeMapViewPtr, String clazz);
+
+ private native void nativeSetClasses(long nativeMapViewPtr,
+ List<String> classes);
+
+ private native List<String> nativeGetClasses(long nativeMapViewPtr);
+
+ private native void nativeSetDefaultTransitionDuration(
+ long nativeMapViewPtr, long duration);
+
+ private native long nativeGetDefaultTransitionDuration(long nativeMapViewPtr);
+
+ private native void nativeSetStyleUrl(long nativeMapViewPtr, String url);
+
+ private native void nativeSetStyleJson(long nativeMapViewPtr,
+ String newStyleJson, String base);
+
+ private native String nativeGetStyleJson(long nativeMapViewPtr);
+
+ private native void nativeSetAccessToken(long nativeMapViewPtr, String accessToken);
+
+ private native String nativeGetAccessToken(long nativeMapViewPtr);
+
+ private native void nativeCancelTransitions(long nativeMapViewPtr);
+
+ private native void nativeSetGestureInProgress(long nativeMapViewPtr, boolean inProgress);
+
+ private native void nativeMoveBy(long nativeMapViewPtr, double dx,
+ double dy, long duration);
+
+ private native void nativeSetLatLng(long nativeMapViewPtr, LatLng latLng,
+ long duration);
+
+ private native LatLng nativeGetLatLng(long nativeMapViewPtr);
+
+ private native void nativeResetPosition(long nativeMapViewPtr);
+
+ private native void nativeScaleBy(long nativeMapViewPtr, double ds,
+ double cx, double cy, long duration);
+
+ private native void nativeSetScale(long nativeMapViewPtr, double scale,
+ double cx, double cy, long duration);
+
+ private native double nativeGetScale(long nativeMapViewPtr);
+
+ private native void nativeSetZoom(long nativeMapViewPtr, double zoom,
+ long duration);
+
+ private native double nativeGetZoom(long nativeMapViewPtr);
+
+ private native void nativeSetLatLngZoom(long nativeMapViewPtr,
+ LatLngZoom lonLatZoom, long duration);
+
+ private native LatLngZoom nativeGetLatLngZoom(long nativeMapViewPtr);
+
+ private native void nativeResetZoom(long nativeMapViewPtr);
+
+ private native double nativeGetMinZoom(long nativeMapViewPtr);
+
+ private native double nativeGetMaxZoom(long nativeMapViewPtr);
+
+ private native void nativeRotateBy(long nativeMapViewPtr, double sx,
+ double sy, double ex, double ey, long duration);
+
+ private native void nativeSetBearing(long nativeMapViewPtr, double degrees,
+ long duration);
+
+ private native void nativeSetBearing(long nativeMapViewPtr, double degrees,
+ double cx, double cy);
+
+ private native double nativeGetBearing(long nativeMapViewPtr);
+
+ private native void nativeResetNorth(long nativeMapViewPtr);
+
+ private native long nativeAddMarker(long nativeMapViewPtr, Marker marker);
+
+ private native long[] nativeAddMarkers(long nativeMapViewPtr, List<Marker> markers);
+
+ private native long nativeAddPolyline(long nativeMapViewPtr, Polyline polyline);
+
+ private native long nativeAddPolygon(long mNativeMapViewPtr, Polygon polygon);
+
+ private native long[] nativeAddPolygons(long mNativeMapViewPtr, List<Polygon> polygon);
+
+ private native void nativeRemoveAnnotation(long nativeMapViewPtr, long id);
+
+ private native void nativeRemoveAnnotations(long nativeMapViewPtr, long[] id);
+
+ private native long[] nativeGetAnnotationsInBounds(long mNativeMapViewPtr, BoundingBox bbox);
+
+ private native void nativeSetSprite(long nativeMapViewPtr, String symbol,
+ int width, int height, float scale, byte[] pixels);
+
+ private native void nativeSetVisibleCoordinateBounds(long mNativeMapViewPtr, LatLng[] coordinates,
+ RectF padding, double direction, long duration);
+
+ private native void nativeOnLowMemory(long nativeMapViewPtr);
+
+ private native void nativeSetDebug(long nativeMapViewPtr, boolean debug);
+
+ private native void nativeToggleDebug(long nativeMapViewPtr);
+
+ private native boolean nativeGetDebug(long nativeMapViewPtr);
+
+ private native void nativeSetCollisionDebug(long nativeMapViewPtr, boolean debug);
+
+ private native void nativeToggleCollisionDebug(long nativeMapViewPtr);
+
+ private native boolean nativeGetCollisionDebug(long nativeMapViewPtr);
+
+ private native boolean nativeIsFullyLoaded(long nativeMapViewPtr);
+
+ private native void nativeSetReachability(long nativeMapViewPtr, boolean status);
+
+ private native double nativeGetMetersPerPixelAtLatitude(long nativeMapViewPtr, double lat, double zoom);
+
+ private native ProjectedMeters nativeProjectedMetersForLatLng(long nativeMapViewPtr, LatLng latLng);
+
+ private native LatLng nativeLatLngForProjectedMeters(long nativeMapViewPtr, ProjectedMeters projectedMeters);
+
+ private native PointF nativePixelForLatLng(long nativeMapViewPtr, LatLng latLng);
+
+ private native LatLng nativeLatLngForPixel(long nativeMapViewPtr, PointF pixel);
+
+ private native double nativeGetTopOffsetPixelsForAnnotationSymbol(long nativeMapViewPtr, String symbolName);
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/UserLocationView.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/UserLocationView.java
new file mode 100644
index 0000000000..7453f4e28c
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/UserLocationView.java
@@ -0,0 +1,473 @@
+package com.mapbox.mapboxsdk.views;
+
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.mapbox.mapboxsdk.R;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapzen.android.lost.api.LocationListener;
+import com.mapzen.android.lost.api.LocationRequest;
+import com.mapzen.android.lost.api.LocationServices;
+import com.mapzen.android.lost.api.LostApiClient;
+
+import java.lang.ref.WeakReference;
+
+final class UserLocationView extends View {
+
+ private WeakReference<MapView> mMapView;
+
+ private static final int BLUE_COLOR = 0x39ACCBFF;
+
+ private float mDensity;
+
+ private boolean mShowMarker;
+ private boolean mShowDirection;
+ private boolean mShowAccuracy;
+
+ private PointF mMarkerScreenPoint;
+ private Matrix mMarkerScreenMatrix;
+
+ private Paint mAccuracyPaintFill;
+ private Paint mAccuracyPaintStroke;
+ private Path mAccuracyPath;
+ private RectF mAccuracyBounds;
+
+ private Drawable mUserLocationDrawable;
+ private RectF mUserLocationDrawableBoundsF;
+ private Rect mUserLocationDrawableBounds;
+
+ private Drawable mUserLocationBearingDrawable;
+ private RectF mUserLocationBearingDrawableBoundsF;
+ private Rect mUserLocationBearingDrawableBounds;
+
+ private Drawable mUserLocationStaleDrawable;
+ private RectF mUserLocationStaleDrawableBoundsF;
+ private Rect mUserLocationStaleDrawableBounds;
+
+ private Rect mDirtyRect;
+ private RectF mDirtyRectF;
+
+ private LatLng mMarkerCoordinate;
+ private ValueAnimator mMarkerCoordinateAnimator;
+ private float mMarkerDirection;
+ private ObjectAnimator mMarkerDirectionAnimator;
+ private float mMarkerAccuracy;
+ private ObjectAnimator mMarkerAccuracyAnimator;
+
+ private boolean mPaused = false;
+ private LostApiClient mLocationClient;
+ private LocationRequest mLocationRequest;
+ private Location mUserLocation;
+ private MyLocationListener mLocationListener;
+
+ MapView.OnMyLocationChangeListener mOnMyLocationChangeListener;
+
+ public UserLocationView(MapView mapView, Context context) {
+ super(context);
+ initialize(mapView, context);
+ }
+
+ public UserLocationView(MapView mapView, Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialize(mapView, context);
+ }
+
+ public UserLocationView(MapView mapView, Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize(mapView, context);
+ }
+
+ private void initialize(MapView mapView, Context context) {
+ mMapView = new WeakReference<MapView>(mapView);
+
+ // View configuration
+ setEnabled(false);
+ setWillNotDraw(false);
+
+ // Layout params
+ ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ setLayoutParams(lp);
+
+ // Setup location services
+ mLocationClient = new LostApiClient.Builder(getContext()).build();
+ mLocationRequest = LocationRequest.create()
+ .setFastestInterval(1000)
+ .setSmallestDisplacement(3.0f)
+ .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
+
+ // Setup the custom paint
+ Resources resources = context.getResources();
+ mDensity = resources.getDisplayMetrics().density;
+ mMarkerCoordinate = new LatLng(0.0, 0.0);
+ mMarkerScreenPoint = new PointF();
+ mMarkerScreenMatrix = new Matrix();
+
+ mAccuracyPaintFill = new Paint();
+ mAccuracyPaintFill.setAntiAlias(true);
+ mAccuracyPaintFill.setStyle(Paint.Style.FILL);
+ mAccuracyPaintFill.setColor(BLUE_COLOR);
+ mAccuracyPaintFill.setAlpha((int) (255 * 0.25f));
+
+ mAccuracyPaintStroke = new Paint();
+ mAccuracyPaintStroke.setAntiAlias(true);
+ mAccuracyPaintStroke.setStyle(Paint.Style.STROKE);
+ mAccuracyPaintStroke.setStrokeWidth(0.5f * mDensity);
+ mAccuracyPaintStroke.setColor(BLUE_COLOR);
+ mAccuracyPaintStroke.setAlpha((int) (255 * 0.5f));
+
+ mAccuracyPath = new Path();
+ mAccuracyBounds = new RectF();
+
+ mUserLocationDrawable = ContextCompat.getDrawable(getContext(), R.drawable.user_location);
+ mUserLocationDrawableBounds = new Rect(
+ -mUserLocationDrawable.getIntrinsicWidth() / 2,
+ -mUserLocationDrawable.getIntrinsicHeight() / 2,
+ mUserLocationDrawable.getIntrinsicWidth() / 2,
+ mUserLocationDrawable.getIntrinsicHeight() / 2);
+ mUserLocationDrawableBoundsF = new RectF(
+ -mUserLocationDrawable.getIntrinsicWidth() / 2,
+ -mUserLocationDrawable.getIntrinsicHeight() / 2,
+ mUserLocationDrawable.getIntrinsicWidth() / 2,
+ mUserLocationDrawable.getIntrinsicHeight() / 2);
+ mUserLocationDrawable.setBounds(mUserLocationDrawableBounds);
+
+ mUserLocationBearingDrawable = ContextCompat.getDrawable(getContext(), R.drawable.user_location_bearing);
+ mUserLocationBearingDrawableBounds = new Rect(
+ -mUserLocationBearingDrawable.getIntrinsicWidth() / 2,
+ -mUserLocationBearingDrawable.getIntrinsicHeight() / 2,
+ mUserLocationBearingDrawable.getIntrinsicWidth() / 2,
+ mUserLocationBearingDrawable.getIntrinsicHeight() / 2);
+ mUserLocationBearingDrawableBoundsF = new RectF(
+ -mUserLocationBearingDrawable.getIntrinsicWidth() / 2,
+ -mUserLocationBearingDrawable.getIntrinsicHeight() / 2,
+ mUserLocationBearingDrawable.getIntrinsicWidth() / 2,
+ mUserLocationBearingDrawable.getIntrinsicHeight() / 2);
+ mUserLocationBearingDrawable.setBounds(mUserLocationBearingDrawableBounds);
+
+ mUserLocationStaleDrawable = ContextCompat.getDrawable(getContext(), R.drawable.user_location_stale);
+ mUserLocationStaleDrawableBounds = new Rect(
+ -mUserLocationStaleDrawable.getIntrinsicWidth() / 2,
+ -mUserLocationStaleDrawable.getIntrinsicHeight() / 2,
+ mUserLocationStaleDrawable.getIntrinsicWidth() / 2,
+ mUserLocationStaleDrawable.getIntrinsicHeight() / 2);
+ mUserLocationStaleDrawableBoundsF = new RectF(
+ -mUserLocationStaleDrawable.getIntrinsicWidth() / 2,
+ -mUserLocationStaleDrawable.getIntrinsicHeight() / 2,
+ mUserLocationStaleDrawable.getIntrinsicWidth() / 2,
+ mUserLocationStaleDrawable.getIntrinsicHeight() / 2);
+ mUserLocationStaleDrawable.setBounds(mUserLocationStaleDrawableBounds);
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (!mShowMarker) {
+ return;
+ }
+
+ canvas.concat(mMarkerScreenMatrix);
+
+ Drawable dotDrawable = mShowDirection ? mUserLocationBearingDrawable : mUserLocationDrawable;
+ // IMPORTANT also update in uodate()
+ RectF dotBounds = mShowDirection ? mUserLocationBearingDrawableBoundsF : mUserLocationDrawableBoundsF;
+
+ boolean willDraw;
+ willDraw = mShowAccuracy && !canvas.quickReject(mAccuracyPath, Canvas.EdgeType.AA);
+ willDraw |= !canvas.quickReject(dotBounds, Canvas.EdgeType.AA);
+
+ dotBounds.offset(
+ (int) -mMarkerScreenPoint.x,
+ (int) -mMarkerScreenPoint.y);
+
+ if (willDraw) {
+ if (mShowAccuracy) {
+ canvas.drawPath(mAccuracyPath, mAccuracyPaintFill);
+ canvas.drawPath(mAccuracyPath, mAccuracyPaintStroke);
+ }
+ dotDrawable.draw(canvas);
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
+ toggleGps(enabled);
+ }
+
+ public void update() {
+ if (isEnabled() && mShowMarker) {
+ setVisibility(View.VISIBLE);
+
+ // compute new marker position
+ // TODO add JNI method that takes existing pointf
+ mMarkerScreenPoint = mMapView.get().toScreenLocation(mMarkerCoordinate);
+ mMarkerScreenMatrix.reset();
+ mMarkerScreenMatrix.setTranslate(
+ mMarkerScreenPoint.x,
+ mMarkerScreenPoint.y);
+
+ // rotate so arrow in points to bearing
+ if (mShowDirection) {
+ mMarkerScreenMatrix.preRotate(mMarkerDirection +
+ (float) mMapView.get().getDirection());
+ }
+
+ // adjust accuracy circle
+ if (mShowAccuracy) {
+ mAccuracyPath.reset();
+ mAccuracyPath.addCircle(0.0f, 0.0f,
+ (float) (mMarkerAccuracy / mMapView.get().getMetersPerPixelAtLatitude(
+ mMarkerCoordinate.getLatitude())),
+ Path.Direction.CW);
+
+ mAccuracyPath.computeBounds(mAccuracyBounds, false);
+ mAccuracyBounds.inset(-1.0f, -1.0f);
+ }
+
+ // invalidate changed pixels
+ if (mDirtyRect == null) {
+ mDirtyRect = new Rect();
+ mDirtyRectF = new RectF();
+ } else {
+ // the old marker location
+ invalidate(mDirtyRect);
+ }
+
+ RectF dotBounds = mShowDirection ? mUserLocationBearingDrawableBoundsF : mUserLocationDrawableBoundsF;
+ RectF largerBounds = mAccuracyBounds.contains(dotBounds) ? mAccuracyBounds : dotBounds;
+ mMarkerScreenMatrix.mapRect(mDirtyRectF, largerBounds);
+ mDirtyRectF.roundOut(mDirtyRect);
+ invalidate(mDirtyRect); // the new marker location
+ } else {
+ setVisibility(View.INVISIBLE);
+ }
+ }
+
+ public Location getLocation() {
+ return mUserLocation;
+ }
+
+ /**
+ * Enabled / Disable GPS location updates along with updating the UI
+ *
+ * @param enableGps true if GPS is to be enabled, false if GPS is to be disabled
+ */
+ private void toggleGps(boolean enableGps) {
+ if (mLocationClient == null) {
+ return;
+ }
+
+ if (enableGps) {
+ if (!mLocationClient.isConnected()) {
+ mUserLocation = null;
+ mLocationClient.connect();
+ setLocation(LocationServices.FusedLocationApi.getLastLocation());
+ mLocationListener = new MyLocationListener();
+ LocationServices.FusedLocationApi.requestLocationUpdates(mLocationRequest, mLocationListener);
+ }
+ } else {
+ if (mLocationClient.isConnected()) {
+ LocationServices.FusedLocationApi.removeLocationUpdates(mLocationListener);
+ mLocationListener = null;
+ mLocationClient.disconnect();
+ mUserLocation = null;
+ }
+ }
+ }
+
+ private class MyLocationListener implements LocationListener {
+ @Override
+ public void onLocationChanged(Location location) {
+ if (mPaused) {
+ return;
+ }
+ setLocation(location);
+ }
+ }
+
+ // Handles location updates from GPS
+ private void setLocation(Location location) {
+ // if null we should hide the marker
+ if (location == null) {
+ mShowMarker = false;
+ mShowDirection = false;
+ mShowAccuracy = false;
+
+ cancelAnimations();
+
+ mUserLocation = null;
+ return;
+ }
+
+ if (mMarkerCoordinateAnimator != null) {
+ mMarkerCoordinateAnimator.end();
+ mMarkerCoordinateAnimator = null;
+ }
+
+ if (mMarkerDirectionAnimator != null) {
+ mMarkerDirectionAnimator.end();
+ mMarkerDirectionAnimator = null;
+ }
+
+ if (mMarkerAccuracyAnimator != null) {
+ mMarkerAccuracyAnimator.end();
+ mMarkerAccuracyAnimator = null;
+ }
+
+ mShowMarker = true;
+
+ LatLng previousCoordinate;
+ if (mUserLocation == null) {
+ previousCoordinate = new LatLng(location);
+ } else {
+ previousCoordinate = new LatLng(mUserLocation);
+ }
+
+ mMarkerCoordinateAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+ mMarkerCoordinateAnimator.setDuration(1000);
+ mMarkerCoordinateAnimator.addUpdateListener(new MarkerCoordinateAnimatorListener(
+ previousCoordinate, new LatLng(location)
+ ));
+ mMarkerCoordinateAnimator.start();
+
+ mShowDirection = location.hasBearing();
+ if (mShowDirection) {
+ if (mUserLocation != null && mUserLocation.hasBearing()) {
+ mMarkerDirection = mUserLocation.getBearing();
+ }
+ float oldDir = mMarkerDirection;
+ float newDir = location.getBearing();
+ float diff = oldDir - newDir;
+ if (diff > 180.0f) {
+ newDir += 360.0f;
+ } else if (diff < -180.0f) {
+ newDir -= 360.f;
+ }
+ mMarkerDirectionAnimator = ObjectAnimator.ofFloat(this, "direction", oldDir, newDir);
+ mMarkerDirectionAnimator.setDuration(1000);
+ mMarkerDirectionAnimator.start();
+ }
+
+ mShowAccuracy = location.hasAccuracy();
+ if (mShowAccuracy) {
+ if (mUserLocation != null && mUserLocation.hasAccuracy()) {
+ mMarkerAccuracy = mUserLocation.getAccuracy();
+ }
+ mMarkerAccuracyAnimator = ObjectAnimator.ofFloat(this, "accuracy", location.getAccuracy());
+ mMarkerAccuracyAnimator.setDuration(1000);
+ mMarkerAccuracyAnimator.start();
+ }
+
+ mUserLocation = location;
+ updateOnNextFrame();
+
+ if (mOnMyLocationChangeListener != null) {
+ mOnMyLocationChangeListener.onMyLocationChange(location);
+ }
+ }
+
+ void updateOnNextFrame() {
+ mMapView.get().update();
+ }
+
+ public void pause() {
+ mPaused = true;
+ toggleGps(false);
+ }
+
+ public void resume() {
+ mPaused = false;
+ if (isEnabled()) {
+ toggleGps(true);
+ }
+ }
+
+ public void setOnMyLocationChangeListener(@Nullable MapView.OnMyLocationChangeListener listener) {
+ mOnMyLocationChangeListener = listener;
+ }
+
+ // public for animator only
+ public float getDirection() {
+ return mMarkerDirection;
+ }
+
+ // public for animator only
+ public void setDirection(float direction) {
+ mMarkerDirection = direction % 360.0f;
+ updateOnNextFrame();
+ }
+
+ // public for animator only
+ public float getAccuracy() {
+ return mMarkerAccuracy;
+ }
+
+ // public for animator only
+ public void setAccuracy(float accuracy) {
+ mMarkerAccuracy = accuracy;
+ updateOnNextFrame();
+ }
+
+ private class MarkerCoordinateAnimatorListener implements ValueAnimator.AnimatorUpdateListener {
+
+ private double mFromLat;
+ private double mFromLng;
+ private double mToLat;
+ private double mToLng;
+
+ private MarkerCoordinateAnimatorListener(LatLng from, LatLng to) {
+ mFromLat = from.getLatitude();
+ mFromLng = from.getLongitude();
+ mToLat = to.getLatitude();
+ mToLng = to.getLongitude();
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ float frac = animation.getAnimatedFraction();
+ double latitude = mFromLat + (mToLat - mFromLat) * frac;
+ double longitude = mFromLng + (mToLng - mFromLng) * frac;
+ mMarkerCoordinate.setLatitude(latitude);
+ mMarkerCoordinate.setLongitude(longitude);
+ updateOnNextFrame();
+ }
+ }
+
+ public void cancelAnimations() {
+ if (mMarkerCoordinateAnimator != null) {
+ mMarkerCoordinateAnimator.cancel();
+ mMarkerCoordinateAnimator = null;
+ }
+
+ if (mMarkerDirectionAnimator != null) {
+ mMarkerDirectionAnimator.cancel();
+ mMarkerDirectionAnimator = null;
+ }
+
+ if (mMarkerAccuracyAnimator != null) {
+ mMarkerAccuracyAnimator.cancel();
+ mMarkerAccuracyAnimator = null;
+ }
+ }
+}
diff --git a/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/package-info.java b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/package-info.java
new file mode 100644
index 0000000000..a17f268c01
--- /dev/null
+++ b/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/views/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains the {@link com.mapbox.mapboxsdk.views.MapView} and related classes.
+ * {@code MapView} is the core component of this SDK and adds a map to your app.
+ */
+package com.mapbox.mapboxsdk.views;