From 1dffe2871ba76c054b80bbb84bedec3ffcabe6ff Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Thu, 17 Mar 2016 11:40:47 -0700
Subject: [PATCH 01/37] Initial steps to cleanup plugin and encapsulate map
 instances.

---
 plugin.xml                   |   1 +
 src/android/MapInstance.java | 169 +++++++++
 src/android/Mapbox.java      | 679 ++++++++++++++++-------------------
 src/android/mapbox.gradle    |   4 +-
 4 files changed, 474 insertions(+), 379 deletions(-)
 create mode 100644 src/android/MapInstance.java

diff --git a/plugin.xml b/plugin.xml
index f2d6d31..f5c6ef0 100755
--- a/plugin.xml
+++ b/plugin.xml
@@ -48,6 +48,7 @@
 
     <framework src="src/android/mapbox.gradle" custom="true" type="gradleReference"/>
     <source-file src="src/android/Mapbox.java" target-dir="src/com/telerik/plugins/mapbox"/>
+    <source-file src="src/android/MapInstance.java" target-dir="src/com/telerik/plugins/mapbox"/>
 
     <!-- This leads to trouble in AppBuilder when compiling for Cordova-Android 4 -->
     <!--source-file src="src/android/res/values/mapboxstrings.xml" target-dir="res/values" />
diff --git a/src/android/MapInstance.java b/src/android/MapInstance.java
new file mode 100644
index 0000000..f388fcd
--- /dev/null
+++ b/src/android/MapInstance.java
@@ -0,0 +1,169 @@
+package com.telerik.plugins.mapbox;
+
+import android.content.Context;
+import android.webkit.WebView;
+import android.widget.FrameLayout;
+
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.maps.UiSettings;
+
+import org.apache.cordova.CordovaWebView;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+
+public class MapInstance {
+
+    public interface MapCreatedCallback {
+        void onMapReady(MapInstance map);
+    }
+
+    public static MapInstance createMap(Context context, String accessToken, MapCreatedCallback callback) {
+        MapView mapView = new MapView(context);
+        mapView.setAccessToken(accessToken);
+        MapInstance map = new MapInstance(mapView, callback);
+        maps.put(map.getId(), map);
+        return map;
+    }
+
+    public static MapInstance getMap(int id) {
+        return maps.get(id);
+    }
+
+    private static HashMap<Integer, MapInstance> maps = new HashMap<Integer, MapInstance>();
+
+    private static int ids = 0;
+
+    private int id;
+
+    private MapView mapView;
+
+    private MapboxMap mapboxMap;
+
+    private MapCreatedCallback constructorCallback;
+
+    private MapInstance(MapView mapView, MapCreatedCallback callback) {
+        this.id = this.ids++;
+        this.constructorCallback = callback;
+        this.mapView = mapView;
+
+        mapView.getMapAsync(new OnMapReadyCallback() {
+            @Override
+            public void onMapReady(MapboxMap mMap) {
+                mapboxMap = mMap;
+                constructorCallback.onMapReady(MapInstance.this);
+            }
+        });
+    }
+
+    public int getId() {
+        return this.id;
+    }
+
+    public MapView getMapView() {
+        return this.mapView;
+    }
+
+    public MapboxMap getMapboxMap() {
+        return this.mapboxMap;
+    }
+
+    public void configure(JSONObject options) throws JSONException {
+        UiSettings uiSettings = mapboxMap.getUiSettings();
+        uiSettings.setCompassEnabled(options.isNull("hideCompass") || !options.getBoolean("hideCompass"));
+        uiSettings.setRotateGesturesEnabled(options.isNull("disableRotation") || !options.getBoolean("disableRotation"));
+        uiSettings.setScrollGesturesEnabled(options.isNull("disableScroll") || !options.getBoolean("disableScroll"));
+        uiSettings.setZoomGesturesEnabled(options.isNull("disableZoom") || !options.getBoolean("disableZoom"));
+        uiSettings.setTiltGesturesEnabled(options.isNull("disableTilt") || !options.getBoolean("disableTilt"));
+    }
+
+    public void show(CordovaWebView webView, float retinaFactor, JSONObject options) throws JSONException {
+        final String style = getStyle(options.optString("style"));
+        final JSONObject center = options.isNull("center") ? null : options.getJSONObject("center");
+
+        final JSONObject margins = options.isNull("margins") ? null : options.getJSONObject("margins");
+        final int left = (int) (retinaFactor * (margins == null || margins.isNull("left") ? 0 : margins.getInt("left")));
+        final int right = (int) (retinaFactor * (margins == null || margins.isNull("right") ? 0 : margins.getInt("right")));
+        final int top = (int) (retinaFactor * (margins == null || margins.isNull("top") ? 0 : margins.getInt("top")));
+        final int bottom = (int) (retinaFactor * (margins == null || margins.isNull("bottom") ? 0 : margins.getInt("bottom")));
+
+        // need to do this to register a receiver which onPause later needs
+        mapView.onResume();
+        mapView.onCreate(null);
+
+        // position the mapView overlay
+        int webViewWidth = webView.getView().getWidth();
+        int webViewHeight = webView.getView().getHeight();
+        final FrameLayout layout = (FrameLayout) webView.getView().getParent();
+        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(webViewWidth - left - right, webViewHeight - top - bottom);
+        params.setMargins(left, top, right, bottom);
+        mapView.setLayoutParams(params);
+
+        layout.addView(mapView);
+    }
+
+    private static String getStyle(final String requested) {
+        if ("light".equalsIgnoreCase(requested)) {
+            return Style.LIGHT;
+        } else if ("dark".equalsIgnoreCase(requested)) {
+            return Style.DARK;
+        } else if ("emerald".equalsIgnoreCase(requested)) {
+            return Style.EMERALD;
+        } else if ("satellite".equalsIgnoreCase(requested)) {
+            return Style.SATELLITE;
+        // TODO not currently supported on Android
+        //} else if ("hybrid".equalsIgnoreCase(requested)) {
+        //    return Style.HYBRID;
+        } else if ("streets".equalsIgnoreCase(requested)) {
+            return Style.MAPBOX_STREETS;
+        } else {
+            return requested;
+        }
+    }
+
+//    public void show(JSONObject options, CallbackContext callbackContext) {
+//
+//
+//        try {
+//
+//
+//            // placing these offscreen in case the user wants to hide them
+//            if (!options.isNull("hideAttribution") && options.getBoolean("hideAttribution")) {
+//                mapView.setAttributionMargins(-300, 0, 0, 0);
+//            }
+//            if (!options.isNull("hideLogo") && options.getBoolean("hideLogo")) {
+//                mapView.setLogoMargins(-300, 0, 0, 0);
+//            }
+//
+//            if (showUserLocation) {
+//                showUserLocation();
+//            }
+//
+//            Double zoom = options.isNull("zoomLevel") ? 10 : options.getDouble("zoomLevel");
+//            float zoomLevel = zoom.floatValue();
+//            if (center != null) {
+//                final double lat = center.getDouble("lat");
+//                final double lng = center.getDouble("lng");
+//                mapView.setLatLng(new LatLngZoom(lat, lng, zoomLevel));
+//            } else {
+//                if (zoomLevel > 18.0) {
+//                    zoomLevel = 18.0f;
+//                }
+//                mapView.setZoom(zoomLevel);
+//            }
+//
+//            if (options.has("markers")) {
+//                addMarkers(options.getJSONArray("markers"));
+//            }
+//        } catch (JSONException e) {
+//            callbackContext.error(e.getMessage());
+//            return;
+//        }
+//
+//        mapView.setStyleUrl(style);
+//    }
+}
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 029fe92..150a24f 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -1,24 +1,14 @@
 package com.telerik.plugins.mapbox;
 
 import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.os.Build;
-import android.support.annotation.NonNull;
+
 import android.support.v4.app.ActivityCompat;
 import android.util.DisplayMetrics;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-
-import com.mapbox.mapboxsdk.camera.CameraPosition;
-import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
-import com.mapbox.mapboxsdk.constants.Style;
-import com.mapbox.mapboxsdk.annotations.Marker;
-import com.mapbox.mapboxsdk.annotations.MarkerOptions;
-import com.mapbox.mapboxsdk.annotations.PolygonOptions;
-import com.mapbox.mapboxsdk.geometry.LatLng;
-import com.mapbox.mapboxsdk.geometry.LatLngZoom;
-import com.mapbox.mapboxsdk.views.MapView;
 
 import org.apache.cordova.CallbackContext;
 import org.apache.cordova.CordovaArgs;
@@ -26,14 +16,10 @@
 import org.apache.cordova.CordovaPlugin;
 import org.apache.cordova.CordovaWebView;
 import org.apache.cordova.PluginResult;
-import org.json.JSONArray;
+
 import org.json.JSONException;
 import org.json.JSONObject;
 
-import java.util.HashMap;
-import java.util.Map;
-
-
 // TODO for screen rotation, see https://www.mapbox.com/mapbox-android-sdk/#screen-rotation
 // TODO fox Xwalk compat, see nativepagetransitions plugin
 // TODO look at demo app: https://github.com/mapbox/mapbox-gl-native/blob/master/android/java/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxgl/testapp/MainActivity.java
@@ -61,387 +47,303 @@ public class Mapbox extends CordovaPlugin {
   private static final String ACTION_SET_TILT = "setTilt";
   private static final String ACTION_ANIMATE_CAMERA = "animateCamera";
 
-  public static MapView mapView;
   private static float retinaFactor;
   private String accessToken;
   private CallbackContext callback;
-  private CallbackContext markerCallbackContext;
 
-  private boolean showUserLocation;
+//  private boolean showUserLocation;
 
   @Override
   public void initialize(CordovaInterface cordova, CordovaWebView webView) {
     super.initialize(cordova, webView);
 
-    DisplayMetrics metrics = new DisplayMetrics();
-    cordova.getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
-    retinaFactor = metrics.density;
-
-    try {
-      int mapboxAccesstokenResourceId = cordova.getActivity().getResources().getIdentifier(MAPBOX_ACCESSTOKEN_RESOURCE_KEY, "string", cordova.getActivity().getPackageName());
-      accessToken = cordova.getActivity().getString(mapboxAccesstokenResourceId);
-    } catch (Resources.NotFoundException e) {
-      // we'll deal with this when the accessToken property is read, but for now let's dump the error:
-      e.printStackTrace();
-    }
+    this.retinaFactor = this.getRetinaFactor();
+    this.accessToken = this.getAccessToken();
   }
 
+
   @Override
   public boolean execute(final String action, final CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
-
-    this.callback = callbackContext;
-
-    try {
-      if (ACTION_SHOW.equals(action)) {
-        final JSONObject options = args.getJSONObject(0);
-        final String style = getStyle(options.optString("style"));
-
-        final JSONObject margins = options.isNull("margins") ? null : options.getJSONObject("margins");
-        final int left = applyRetinaFactor(margins == null || margins.isNull("left") ? 0 : margins.getInt("left"));
-        final int right = applyRetinaFactor(margins == null || margins.isNull("right") ? 0 : margins.getInt("right"));
-        final int top = applyRetinaFactor(margins == null || margins.isNull("top") ? 0 : margins.getInt("top"));
-        final int bottom = applyRetinaFactor(margins == null || margins.isNull("bottom") ? 0 : margins.getInt("bottom"));
-
-        final JSONObject center = options.isNull("center") ? null : options.getJSONObject("center");
-
-        this.showUserLocation = !options.isNull("showUserLocation") && options.getBoolean("showUserLocation");
-
-        cordova.getActivity().runOnUiThread(new Runnable() {
-          @Override
-          public void run() {
-            if (accessToken == null) {
-              callbackContext.error(MAPBOX_ACCESSTOKEN_RESOURCE_KEY + " not set in strings.xml");
-              return;
-            }
-            mapView = new MapView(webView.getContext(), accessToken);
-
-            // need to do this to register a receiver which onPause later needs
-            mapView.onResume();
-            mapView.onCreate(null);
-
-            try {
-              mapView.setCompassEnabled(options.isNull("hideCompass") || !options.getBoolean("hideCompass"));
-              mapView.setRotateEnabled(options.isNull("disableRotation") || !options.getBoolean("disableRotation"));
-              mapView.setScrollEnabled(options.isNull("disableScroll") || !options.getBoolean("disableScroll"));
-              mapView.setZoomEnabled(options.isNull("disableZoom") || !options.getBoolean("disableZoom"));
-              mapView.setTiltEnabled(options.isNull("disableTilt") || !options.getBoolean("disableTilt"));
-
-              // placing these offscreen in case the user wants to hide them
-              if (!options.isNull("hideAttribution") && options.getBoolean("hideAttribution")) {
-                mapView.setAttributionMargins(-300, 0, 0, 0);
-              }
-              if (!options.isNull("hideLogo") && options.getBoolean("hideLogo")) {
-                mapView.setLogoMargins(-300, 0, 0, 0);
-              }
-
-              if (showUserLocation) {
-                showUserLocation();
-              }
-
-              Double zoom = options.isNull("zoomLevel") ? 10 : options.getDouble("zoomLevel");
-              float zoomLevel = zoom.floatValue();
-              if (center != null) {
-                final double lat = center.getDouble("lat");
-                final double lng = center.getDouble("lng");
-                mapView.setLatLng(new LatLngZoom(lat, lng, zoomLevel));
-              } else {
-                if (zoomLevel > 18.0) {
-                  zoomLevel = 18.0f;
-                }
-                mapView.setZoom(zoomLevel);
-              }
-
-              if (options.has("markers")) {
-                addMarkers(options.getJSONArray("markers"));
-              }
-            } catch (JSONException e) {
-              callbackContext.error(e.getMessage());
-              return;
-            }
-
-            mapView.setStyleUrl(style);
-
-            // position the mapView overlay
-            int webViewWidth = webView.getView().getWidth();
-            int webViewHeight = webView.getView().getHeight();
-            final FrameLayout layout = (FrameLayout) webView.getView().getParent();
-            FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(webViewWidth - left - right, webViewHeight - top - bottom);
-            params.setMargins(left, top, right, bottom);
-            mapView.setLayoutParams(params);
-
-            layout.addView(mapView);
-            callbackContext.success();
-          }
-        });
-
-      } else if (ACTION_HIDE.equals(action)) {
-        if (mapView != null) {
-          cordova.getActivity().runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-              ViewGroup vg = (ViewGroup) mapView.getParent();
-              if (vg != null) {
-                vg.removeView(mapView);
-              }
-              callbackContext.success();
-            }
-          });
-        }
-
-      } else if (ACTION_GET_ZOOMLEVEL.equals(action)) {
-        if (mapView != null) {
-          cordova.getActivity().runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-              final double zoomLevel = mapView.getZoom();
-              callbackContext.success("" + zoomLevel);
-            }
-          });
-        }
-
-      } else if (ACTION_SET_ZOOMLEVEL.equals(action)) {
-        if (mapView != null) {
-          cordova.getActivity().runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-              try {
-                final JSONObject options = args.getJSONObject(0);
-                final double zoom = options.getDouble("level");
-                if (zoom >= 0 && zoom <= 20) {
-                  final boolean animated = !options.isNull("animated") && options.getBoolean("animated");
-                  mapView.setZoom(zoom, animated);
-                  callbackContext.success();
-                } else {
-                  callbackContext.error("invalid zoomlevel, use any double value from 0 to 20 (like 8.3)");
-                }
-              } catch (JSONException e) {
-                callbackContext.error(e.getMessage());
-              }
-            }
-          });
-        }
-
-      } else if (ACTION_GET_CENTER.equals(action)) {
-        if (mapView != null) {
-          cordova.getActivity().runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-              final LatLng center = mapView.getLatLng();
-              Map<String, Double> result = new HashMap<String, Double>();
-              result.put("lat", center.getLatitude());
-              result.put("lng", center.getLongitude());
-              callbackContext.success(new JSONObject(result));
-            }
-          });
-        }
-
-      } else if (ACTION_SET_CENTER.equals(action)) {
-        if (mapView != null) {
-          cordova.getActivity().runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-              try {
-                final JSONObject options = args.getJSONObject(0);
-                final boolean animated = !options.isNull("animated") && options.getBoolean("animated");
-                final double lat = options.getDouble("lat");
-                final double lng = options.getDouble("lng");
-                mapView.setLatLng(new LatLng(lat, lng), animated);
-                callbackContext.success();
-              } catch (JSONException e) {
-                callbackContext.error(e.getMessage());
-              }
-            }
-          });
-        }
-
-      } else if (ACTION_GET_TILT.equals(action)) {
-        if (mapView != null) {
-          cordova.getActivity().runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-              final double tilt = mapView.getTilt();
-              callbackContext.success("" + tilt);
-            }
-          });
+    callback = callbackContext;
+
+    if (ACTION_SHOW.equals(action)) {
+      final JSONObject options = args.getJSONObject(0);
+      this.show(options, callbackContext);
+    } else if (ACTION_HIDE.equals(action)) {
+//        if (mapView != null) {
+//          cordova.getActivity().runOnUiThread(new Runnable() {
+//            @Override
+//            public void run() {
+//              ViewGroup vg = (ViewGroup) mapView.getParent();
+//              if (vg != null) {
+//                vg.removeView(mapView);
+//              }
+//              callbackContext.success();
+//            }
+//          });
+//        }
+
+    } else if (ACTION_GET_ZOOMLEVEL.equals(action)) {
+//        if (mapView != null) {
+//          cordova.getActivity().runOnUiThread(new Runnable() {
+//            @Override
+//            public void run() {
+//              final double zoomLevel = mapView.getZoom();
+//              callbackContext.success("" + zoomLevel);
+//            }
+//          });
+//        }
+
+    } else if (ACTION_SET_ZOOMLEVEL.equals(action)) {
+//        if (mapView != null) {
+//          cordova.getActivity().runOnUiThread(new Runnable() {
+//            @Override
+//            public void run() {
+//              try {
+//                final JSONObject options = args.getJSONObject(0);
+//                final double zoom = options.getDouble("level");
+//                if (zoom >= 0 && zoom <= 20) {
+//                  final boolean animated = !options.isNull("animated") && options.getBoolean("animated");
+//                  mapView.setZoom(zoom, animated);
+//                  callbackContext.success();
+//                } else {
+//                  callbackContext.error("invalid zoomlevel, use any double value from 0 to 20 (like 8.3)");
+//                }
+//              } catch (JSONException e) {
+//                callbackContext.error(e.getMessage());
+//              }
+//            }
+//          });
+//        }
+
+    } else if (ACTION_GET_CENTER.equals(action)) {
+//        if (mapView != null) {
+//          cordova.getActivity().runOnUiThread(new Runnable() {
+//            @Override
+//            public void run() {
+//              final LatLng center = mapView.getLatLng();
+//              Map<String, Double> result = new HashMap<String, Double>();
+//              result.put("lat", center.getLatitude());
+//              result.put("lng", center.getLongitude());
+//              callbackContext.success(new JSONObject(result));
+//            }
+//          });
+//        }
+
+    } else if (ACTION_SET_CENTER.equals(action)) {
+//        if (mapView != null) {
+//          cordova.getActivity().runOnUiThread(new Runnable() {
+//            @Override
+//            public void run() {
+//              try {
+//                final JSONObject options = args.getJSONObject(0);
+//                final boolean animated = !options.isNull("animated") && options.getBoolean("animated");
+//                final double lat = options.getDouble("lat");
+//                final double lng = options.getDouble("lng");
+//                mapView.setLatLng(new LatLng(lat, lng), animated);
+//                callbackContext.success();
+//              } catch (JSONException e) {
+//                callbackContext.error(e.getMessage());
+//              }
+//            }
+//          });
+//        }
+
+    } else if (ACTION_GET_TILT.equals(action)) {
+//        if (mapView != null) {
+//          cordova.getActivity().runOnUiThread(new Runnable() {
+//            @Override
+//            public void run() {
+//              final double tilt = mapView.getTilt();
+//              callbackContext.success("" + tilt);
+//            }
+//          });
+//        }
+
+    } else if (ACTION_SET_TILT.equals(action)) {
+//        if (mapView != null) {
+//          cordova.getActivity().runOnUiThread(new Runnable() {
+//            @Override
+//            public void run() {
+//              try {
+//                final JSONObject options = args.getJSONObject(0);
+//                mapView.setTilt(
+//                    options.optDouble("pitch", 20),      // default 20
+//                    options.optLong("duration", 5000)); // default 5s
+//                callbackContext.success();
+//              } catch (JSONException e) {
+//                callbackContext.error(e.getMessage());
+//              }
+//            }
+//          });
+//        }
+
+    } else if (ACTION_ANIMATE_CAMERA.equals(action)) {
+//        if (mapView != null) {
+//          cordova.getActivity().runOnUiThread(new Runnable() {
+//            @Override
+//            public void run() {
+//              try {
+//                // TODO check mandatory elements
+//                final JSONObject options = args.getJSONObject(0);
+//
+//                final JSONObject target = options.getJSONObject("target");
+//                final double lat = target.getDouble("lat");
+//                final double lng = target.getDouble("lng");
+//
+//                final CameraPosition.Builder builder =
+//                    new CameraPosition.Builder()
+//                        .target(new LatLng(lat, lng));
+//
+//                if (options.has("bearing")) {
+//                  builder.bearing(((Double)options.getDouble("bearing")).floatValue());
+//                }
+//                if (options.has("tilt")) {
+//                  builder.tilt(((Double)options.getDouble("tilt")).floatValue());
+//                }
+//                if (options.has("zoomLevel")) {
+//                  builder.zoom(((Double)options.getDouble("zoomLevel")).floatValue());
+//                }
+//
+//                mapView.animateCamera(
+//                    CameraUpdateFactory.newCameraPosition(builder.build()),
+//                    (options.optInt("duration", 15)) * 1000, // default 15 seconds
+//                    null);
+//
+//                callbackContext.success();
+//              } catch (JSONException e) {
+//                callbackContext.error(e.getMessage());
+//              }
+//            }
+//          });
+//        }
+
+    } else if (ACTION_ADD_POLYGON.equals(action)) {
+//        cordova.getActivity().runOnUiThread(new Runnable() {
+//          @Override
+//          public void run() {
+//            try {
+//              final PolygonOptions polygon = new PolygonOptions();
+//              final JSONObject options = args.getJSONObject(0);
+//              final JSONArray points = options.getJSONArray("points");
+//              for (int i = 0; i < points.length(); i++) {
+//                final JSONObject marker = points.getJSONObject(i);
+//                final double lat = marker.getDouble("lat");
+//                final double lng = marker.getDouble("lng");
+//                polygon.add(new LatLng(lat, lng));
+//              }
+//              mapView.addPolygon(polygon);
+//
+//              callbackContext.success();
+//            } catch (JSONException e) {
+//              callbackContext.error(e.getMessage());
+//            }
+//          }
+//        });
+
+    } else if (ACTION_ADD_GEOJSON.equals(action)) {
+      cordova.getActivity().runOnUiThread(new Runnable() {
+        @Override
+        public void run() {
+          // TODO implement
+          callbackContext.success();
         }
+      });
+
+    } else if (ACTION_ADD_MARKERS.equals(action)) {
+//        cordova.getActivity().runOnUiThread(new Runnable() {
+//          @Override
+//          public void run() {
+//            try {
+//              addMarkers(args.getJSONArray(0));
+//              callbackContext.success();
+//            } catch (JSONException e) {
+//              callbackContext.error(e.getMessage());
+//            }
+//          }
+//        });
+
+    } else if (ACTION_ADD_MARKER_CALLBACK.equals(action)) {
+//        this.markerCallbackContext = callbackContext;
+//        mapView.setOnInfoWindowClickListener(new MarkerClickListener());
 
-      } else if (ACTION_SET_TILT.equals(action)) {
-        if (mapView != null) {
-          cordova.getActivity().runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-              try {
-                final JSONObject options = args.getJSONObject(0);
-                mapView.setTilt(
-                    options.optDouble("pitch", 20),      // default 20
-                    options.optLong("duration", 5000)); // default 5s
-                callbackContext.success();
-              } catch (JSONException e) {
-                callbackContext.error(e.getMessage());
-              }
-            }
-          });
-        }
-
-      } else if (ACTION_ANIMATE_CAMERA.equals(action)) {
-        if (mapView != null) {
-          cordova.getActivity().runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-              try {
-                // TODO check mandatory elements
-                final JSONObject options = args.getJSONObject(0);
-
-                final JSONObject target = options.getJSONObject("target");
-                final double lat = target.getDouble("lat");
-                final double lng = target.getDouble("lng");
-
-                final CameraPosition.Builder builder =
-                    new CameraPosition.Builder()
-                        .target(new LatLng(lat, lng));
-
-                if (options.has("bearing")) {
-                  builder.bearing(((Double)options.getDouble("bearing")).floatValue());
-                }
-                if (options.has("tilt")) {
-                  builder.tilt(((Double)options.getDouble("tilt")).floatValue());
-                }
-                if (options.has("zoomLevel")) {
-                  builder.zoom(((Double)options.getDouble("zoomLevel")).floatValue());
-                }
-
-                mapView.animateCamera(
-                    CameraUpdateFactory.newCameraPosition(builder.build()),
-                    (options.optInt("duration", 15)) * 1000, // default 15 seconds
-                    null);
-
-                callbackContext.success();
-              } catch (JSONException e) {
-                callbackContext.error(e.getMessage());
-              }
-            }
-          });
-        }
+    } else {
+      return false;
+    }
+    return true;
+  }
 
-      } else if (ACTION_ADD_POLYGON.equals(action)) {
-        cordova.getActivity().runOnUiThread(new Runnable() {
-          @Override
-          public void run() {
-            try {
-              final PolygonOptions polygon = new PolygonOptions();
-              final JSONObject options = args.getJSONObject(0);
-              final JSONArray points = options.getJSONArray("points");
-              for (int i = 0; i < points.length(); i++) {
-                final JSONObject marker = points.getJSONObject(i);
-                final double lat = marker.getDouble("lat");
-                final double lng = marker.getDouble("lng");
-                polygon.add(new LatLng(lat, lng));
-              }
-              mapView.addPolygon(polygon);
-
-              callbackContext.success();
-            } catch (JSONException e) {
-              callbackContext.error(e.getMessage());
-            }
-          }
-        });
+  private void show(final JSONObject options, final CallbackContext callback) {
+//    this.showUserLocation = !options.isNull("showUserLocation") && options.getBoolean("showUserLocation");
 
-      } else if (ACTION_ADD_GEOJSON.equals(action)) {
-        cordova.getActivity().runOnUiThread(new Runnable() {
-          @Override
-          public void run() {
-            // TODO implement
-            callbackContext.success();
-          }
-        });
+    if (accessToken == null) {
+      callback.error(MAPBOX_ACCESSTOKEN_RESOURCE_KEY + " not set in strings.xml");
+      return;
+    }
 
-      } else if (ACTION_ADD_MARKERS.equals(action)) {
+    MapInstance.createMap(this.webView.getContext(), accessToken, new MapInstance.MapCreatedCallback() {
+      @Override
+      public void onMapReady(final MapInstance map) {
         cordova.getActivity().runOnUiThread(new Runnable() {
           @Override
           public void run() {
+            JSONObject resp = new JSONObject();
             try {
-              addMarkers(args.getJSONArray(0));
-              callbackContext.success();
+              map.configure(options);
+              map.show(webView, retinaFactor, options);
+              resp.put("id", map.getId());
+              callback.success(resp);
+              return;
             } catch (JSONException e) {
-              callbackContext.error(e.getMessage());
+              e.printStackTrace();
+              callback.error("Failed to create map.");
+              return;
             }
           }
         });
-
-      } else if (ACTION_ADD_MARKER_CALLBACK.equals(action)) {
-        this.markerCallbackContext = callbackContext;
-        mapView.setOnInfoWindowClickListener(new MarkerClickListener());
-
-      } else {
-        return false;
       }
-    } catch (Throwable t) {
-      t.printStackTrace();
-      callbackContext.error(t.getMessage());
-    }
-    return true;
+    });
   }
 
-  private void addMarkers(JSONArray markers) throws JSONException {
-    for (int i=0; i<markers.length(); i++) {
-      final JSONObject marker = markers.getJSONObject(i);
-      final MarkerOptions mo = new MarkerOptions();
-      mo.title(marker.isNull("title") ? null : marker.getString("title"));
-      mo.snippet(marker.isNull("subtitle") ? null : marker.getString("subtitle"));
-      mo.position(new LatLng(marker.getDouble("lat"), marker.getDouble("lng")));
-      mapView.addMarker(mo);
-    }
-  }
 
-  private class MarkerClickListener implements MapView.OnInfoWindowClickListener {
-
-    @Override
-    public boolean onMarkerClick(@NonNull Marker marker) {
-      // callback
-      if (markerCallbackContext != null) {
-        final JSONObject json = new JSONObject();
-        try {
-          json.put("title", marker.getTitle());
-          json.put("subtitle", marker.getSnippet());
-          json.put("lat", marker.getPosition().getLatitude());
-          json.put("lng", marker.getPosition().getLongitude());
-        } catch (JSONException e) {
-          PluginResult pluginResult = new PluginResult(PluginResult.Status.ERROR,
-              "Error in callback of " + ACTION_ADD_MARKER_CALLBACK + ": " + e.getMessage());
-          pluginResult.setKeepCallback(true);
-          markerCallbackContext.sendPluginResult(pluginResult);
-        }
-        PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, json);
-        pluginResult.setKeepCallback(true);
-        markerCallbackContext.sendPluginResult(pluginResult);
-        return true;
-      }
-      return false;
-    }
-  }
 
-  private static int applyRetinaFactor(int i) {
-    return (int) (i * retinaFactor);
-  }
-
-  private static String getStyle(final String requested) {
-    if ("light".equalsIgnoreCase(requested)) {
-      return Style.LIGHT;
-    } else if ("dark".equalsIgnoreCase(requested)) {
-      return Style.DARK;
-    } else if ("emerald".equalsIgnoreCase(requested)) {
-      return Style.EMERALD;
-    } else if ("satellite".equalsIgnoreCase(requested)) {
-      return Style.SATELLITE;
-      // TODO not currently supported on Android
-//    } else if ("hybrid".equalsIgnoreCase(requested)) {
-//      return Style.HYBRID;
-    } else if ("streets".equalsIgnoreCase(requested)) {
-      return Style.MAPBOX_STREETS;
-    } else {
-      return requested;
-    }
-  }
+//  private void addMarkers(JSONArray markers) throws JSONException {
+//    for (int i=0; i<markers.length(); i++) {
+//      final JSONObject marker = markers.getJSONObject(i);
+//      final MarkerOptions mo = new MarkerOptions();
+//      mo.title(marker.isNull("title") ? null : marker.getString("title"));
+//      mo.snippet(marker.isNull("subtitle") ? null : marker.getString("subtitle"));
+//      mo.position(new LatLng(marker.getDouble("lat"), marker.getDouble("lng")));
+//      mapView.addMarker(mo);
+//    }
+//  }
+//
+//  private class MarkerClickListener implements MapView.OnInfoWindowClickListener {
+//
+//    @Override
+//    public boolean onMarkerClick(@NonNull Marker marker) {
+//      // callback
+//      if (markerCallbackContext != null) {
+//        final JSONObject json = new JSONObject();
+//        try {
+//          json.put("title", marker.getTitle());
+//          json.put("subtitle", marker.getSnippet());
+//          json.put("lat", marker.getPosition().getLatitude());
+//          json.put("lng", marker.getPosition().getLongitude());
+//        } catch (JSONException e) {
+//          PluginResult pluginResult = new PluginResult(PluginResult.Status.ERROR,
+//              "Error in callback of " + ACTION_ADD_MARKER_CALLBACK + ": " + e.getMessage());
+//          pluginResult.setKeepCallback(true);
+//          markerCallbackContext.sendPluginResult(pluginResult);
+//        }
+//        PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, json);
+//        pluginResult.setKeepCallback(true);
+//        markerCallbackContext.sendPluginResult(pluginResult);
+//        return true;
+//      }
+//      return false;
+//    }
+//  }
 
   private boolean permissionGranted(String... types) {
     if (Build.VERSION.SDK_INT < 23) {
@@ -458,7 +360,7 @@ private boolean permissionGranted(String... types) {
   protected void showUserLocation() {
     if (permissionGranted(COARSE_LOCATION, FINE_LOCATION)) {
       //noinspection MissingPermission
-      mapView.setMyLocationEnabled(showUserLocation);
+//      mapView.setMyLocationEnabled(showUserLocation);
     } else {
       requestPermission(COARSE_LOCATION, FINE_LOCATION);
     }
@@ -487,15 +389,40 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, int
     }
   }
 
-  public void onPause(boolean multitasking) {
-    mapView.onPause();
+//  public void onPause(boolean multitasking) {
+//    mapView.onPause();
+//  }
+//
+//  public void onResume(boolean multitasking) {
+//    mapView.onResume();
+//  }
+//
+//  public void onDestroy() {
+//    mapView.onDestroy();
+//  }
+
+  private float getRetinaFactor() {
+    DisplayMetrics metrics = new DisplayMetrics();
+    this.cordova.getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
+    return metrics.density;
   }
 
-  public void onResume(boolean multitasking) {
-    mapView.onResume();
-  }
+  private String getAccessToken() {
+    Activity activity = cordova.getActivity();
+    Resources res = activity.getResources();
+    String packageName = activity.getPackageName();
+    int resourceId;
+    String accessToken;
+
+    try {
+      resourceId = res.getIdentifier(MAPBOX_ACCESSTOKEN_RESOURCE_KEY, "string", packageName);
+      accessToken = activity.getString(resourceId);
+    } catch (Resources.NotFoundException e) {
+      // we'll deal with this when the accessToken property is read, but for now let's dump the error:
+      e.printStackTrace();
+      throw e;
+    }
 
-  public void onDestroy() {
-    mapView.onDestroy();
+    return accessToken;
   }
 }
diff --git a/src/android/mapbox.gradle b/src/android/mapbox.gradle
index af6df92..4bbe68b 100644
--- a/src/android/mapbox.gradle
+++ b/src/android/mapbox.gradle
@@ -2,13 +2,11 @@ ext.cdvMinSdkVersion = 15
 
 repositories {
     mavenCentral()
-    maven { url "http://oss.sonatype.org/content/repositories/snapshots/" }
 }
 
-
 dependencies {
     compile 'com.android.support:appcompat-v7:23.0.1'
-    compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:3.2.0@aar'){
+    compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:4.0.0-beta.1@aar'){
         transitive=true
     }
 }

From 34a5a548760a08df7a193ae17ec4370c9a47e242 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Thu, 17 Mar 2016 16:34:15 -0700
Subject: [PATCH 02/37] Added js bindings for new map instance. Rearranged
 MapView & MapboxMap creation sequence.

---
 plugin.xml                   |  4 +++-
 src/android/MapInstance.java | 28 +++--------------------
 src/android/Mapbox.java      | 44 ++++++++++++++++++++++++++++++++----
 www/Mapbox.js                | 17 +++++++++-----
 www/map-instance.js          |  5 ++++
 5 files changed, 61 insertions(+), 37 deletions(-)
 create mode 100644 www/map-instance.js

diff --git a/plugin.xml b/plugin.xml
index f5c6ef0..499a824 100755
--- a/plugin.xml
+++ b/plugin.xml
@@ -24,8 +24,10 @@
     <engine name="cordova-plugman" version=">=4.2.0"/><!-- needed for gradleReference support -->
   </engines>
 
+  <js-module src="www/map-instance.js" name="MapInstance" />
+
   <js-module src="www/Mapbox.js" name="Mapbox">
-    <clobbers target="Mapbox" />
+    <clobbers target="window.Mapbox" />
   </js-module>
 
   <!-- android -->
diff --git a/src/android/MapInstance.java b/src/android/MapInstance.java
index f388fcd..b4ca5d3 100644
--- a/src/android/MapInstance.java
+++ b/src/android/MapInstance.java
@@ -22,9 +22,7 @@ public interface MapCreatedCallback {
         void onMapReady(MapInstance map);
     }
 
-    public static MapInstance createMap(Context context, String accessToken, MapCreatedCallback callback) {
-        MapView mapView = new MapView(context);
-        mapView.setAccessToken(accessToken);
+    public static MapInstance createMap(MapView mapView, MapCreatedCallback callback) {
         MapInstance map = new MapInstance(mapView, callback);
         maps.put(map.getId(), map);
         return map;
@@ -55,6 +53,7 @@ private MapInstance(MapView mapView, MapCreatedCallback callback) {
             @Override
             public void onMapReady(MapboxMap mMap) {
                 mapboxMap = mMap;
+                mapboxMap.setMyLocationEnabled(false);
                 constructorCallback.onMapReady(MapInstance.this);
             }
         });
@@ -82,28 +81,7 @@ public void configure(JSONObject options) throws JSONException {
     }
 
     public void show(CordovaWebView webView, float retinaFactor, JSONObject options) throws JSONException {
-        final String style = getStyle(options.optString("style"));
-        final JSONObject center = options.isNull("center") ? null : options.getJSONObject("center");
-
-        final JSONObject margins = options.isNull("margins") ? null : options.getJSONObject("margins");
-        final int left = (int) (retinaFactor * (margins == null || margins.isNull("left") ? 0 : margins.getInt("left")));
-        final int right = (int) (retinaFactor * (margins == null || margins.isNull("right") ? 0 : margins.getInt("right")));
-        final int top = (int) (retinaFactor * (margins == null || margins.isNull("top") ? 0 : margins.getInt("top")));
-        final int bottom = (int) (retinaFactor * (margins == null || margins.isNull("bottom") ? 0 : margins.getInt("bottom")));
-
-        // need to do this to register a receiver which onPause later needs
-        mapView.onResume();
-        mapView.onCreate(null);
-
-        // position the mapView overlay
-        int webViewWidth = webView.getView().getWidth();
-        int webViewHeight = webView.getView().getHeight();
-        final FrameLayout layout = (FrameLayout) webView.getView().getParent();
-        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(webViewWidth - left - right, webViewHeight - top - bottom);
-        params.setMargins(left, top, right, bottom);
-        mapView.setLayoutParams(params);
-
-        layout.addView(mapView);
+
     }
 
     private static String getStyle(final String requested) {
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 150a24f..8b78095 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -2,13 +2,14 @@
 
 import android.Manifest;
 import android.app.Activity;
-import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.os.Build;
-
 import android.support.v4.app.ActivityCompat;
 import android.util.DisplayMetrics;
+import android.widget.FrameLayout;
+
+import com.mapbox.mapboxsdk.maps.MapView;
 
 import org.apache.cordova.CallbackContext;
 import org.apache.cordova.CordovaArgs;
@@ -274,14 +275,18 @@ public void run() {
   }
 
   private void show(final JSONObject options, final CallbackContext callback) {
-//    this.showUserLocation = !options.isNull("showUserLocation") && options.getBoolean("showUserLocation");
+    if (!this.permissionGranted(COARSE_LOCATION, FINE_LOCATION)) {
+      this.requestPermission(COARSE_LOCATION, FINE_LOCATION);
+      return;
+    }
 
     if (accessToken == null) {
       callback.error(MAPBOX_ACCESSTOKEN_RESOURCE_KEY + " not set in strings.xml");
       return;
     }
 
-    MapInstance.createMap(this.webView.getContext(), accessToken, new MapInstance.MapCreatedCallback() {
+    MapView mapView = this.createMapView(accessToken, options);
+    MapInstance.createMap(mapView, new MapInstance.MapCreatedCallback() {
       @Override
       public void onMapReady(final MapInstance map) {
         cordova.getActivity().runOnUiThread(new Runnable() {
@@ -305,7 +310,37 @@ public void run() {
     });
   }
 
+  private MapView createMapView(String accessToken, JSONObject options) {
+    MapView mapView = new MapView(this.webView.getContext());
+    mapView.setAccessToken(accessToken);
 
+    try {
+  //    final String style = getStyle(options.optString("style"));
+  //    final JSONObject center = options.isNull("center") ? null : options.getJSONObject("center");
+      final JSONObject margins = options.isNull("margins") ? null : options.getJSONObject("margins");
+      final int left = (int) (retinaFactor * (margins == null || margins.isNull("left") ? 0 : margins.getInt("left")));
+      final int right = (int) (retinaFactor * (margins == null || margins.isNull("right") ? 0 : margins.getInt("right")));
+      final int top = (int) (retinaFactor * (margins == null || margins.isNull("top") ? 0 : margins.getInt("top")));
+      final int bottom = (int) (retinaFactor * (margins == null || margins.isNull("bottom") ? 0 : margins.getInt("bottom")));
+
+      // need to do this to register a receiver which onPause later needs
+      mapView.onResume();
+      mapView.onCreate(null);
+
+      // position the mapView overlay
+      int webViewWidth = webView.getView().getWidth();
+      int webViewHeight = webView.getView().getHeight();
+      final FrameLayout layout = (FrameLayout) webView.getView().getParent();
+      FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(webViewWidth - left - right, webViewHeight - top - bottom);
+      params.setMargins(left, top, right, bottom);
+      mapView.setLayoutParams(params);
+
+      layout.addView(mapView);
+    } catch (JSONException e) {
+      e.printStackTrace();
+    }
+    return mapView;
+  }
 
 //  private void addMarkers(JSONArray markers) throws JSONException {
 //    for (int i=0; i<markers.length(); i++) {
@@ -384,7 +419,6 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, int
     }
     switch (requestCode) {
       case LOCATION_REQ_CODE:
-        showUserLocation();
         break;
     }
   }
diff --git a/www/Mapbox.js b/www/Mapbox.js
index 2360cd2..ea8b459 100644
--- a/www/Mapbox.js
+++ b/www/Mapbox.js
@@ -1,9 +1,15 @@
-var exec = require("cordova/exec");
+var exec = require("cordova/exec"),
+    MapInstance = require("./MapInstance");
 
 module.exports = {
-  show: function (options, successCallback, errorCallback) {
-    cordova.exec(successCallback, errorCallback, "Mapbox", "show", [options]);
-  },
+    show: function (options, successCallback, errorCallback) {
+        console.log('Mapbox.js show()');
+        cordova.exec(function(resp) {
+            console.log('Mapbox.js show()', resp);
+            var map = new MapInstance(resp.id);
+            successCallback(map);
+        }, errorCallback, "Mapbox", "show", [options]);
+    },
 
   hide: function (options, successCallback, errorCallback) {
     cordova.exec(successCallback, errorCallback, "Mapbox", "hide", [options]);
@@ -60,5 +66,4 @@ module.exports = {
   convertPoint: function(options, successCallback, errorCallback){
     cordova.exec(successCallback, errorCallback, "Mapbox", "convertPoint", [options]);
   }
-
-};
\ No newline at end of file
+};
diff --git a/www/map-instance.js b/www/map-instance.js
new file mode 100644
index 0000000..34ea06f
--- /dev/null
+++ b/www/map-instance.js
@@ -0,0 +1,5 @@
+function MapInstance(id) {
+    this._id = id;
+}
+
+module.exports = MapInstance;

From 20cf01b494dbcc40c45e5f713ae5f86d3d04e5d5 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Thu, 17 Mar 2016 17:12:02 -0700
Subject: [PATCH 03/37] Added service MapboxSDK depends on. Added a
 Mapbox.create API

---
 plugin.xml    | 4 ++++
 www/Mapbox.js | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/plugin.xml b/plugin.xml
index 499a824..37cc5ee 100755
--- a/plugin.xml
+++ b/plugin.xml
@@ -48,6 +48,10 @@
       <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
     </config-file>
 
+    <config-file target="AndroidManifest.xml" parent="/manifest/application">
+      <service android:name="com.mapbox.mapboxsdk.telemetry.TelemetryService" />
+    </config-file>
+
     <framework src="src/android/mapbox.gradle" custom="true" type="gradleReference"/>
     <source-file src="src/android/Mapbox.java" target-dir="src/com/telerik/plugins/mapbox"/>
     <source-file src="src/android/MapInstance.java" target-dir="src/com/telerik/plugins/mapbox"/>
diff --git a/www/Mapbox.js b/www/Mapbox.js
index ea8b459..358e715 100644
--- a/www/Mapbox.js
+++ b/www/Mapbox.js
@@ -2,7 +2,7 @@ var exec = require("cordova/exec"),
     MapInstance = require("./MapInstance");
 
 module.exports = {
-    show: function (options, successCallback, errorCallback) {
+    create: function (options, successCallback, errorCallback) {
         console.log('Mapbox.js show()');
         cordova.exec(function(resp) {
             console.log('Mapbox.js show()', resp);

From 881cb4ce1ff2ab3dcdbb4afb9e31b6c4dbeb58be Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Thu, 17 Mar 2016 17:20:04 -0700
Subject: [PATCH 04/37] fixing issue with accessing view from non-ui thread.

---
 src/android/Mapbox.java | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 8b78095..b95bd9a 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -284,14 +284,13 @@ private void show(final JSONObject options, final CallbackContext callback) {
       callback.error(MAPBOX_ACCESSTOKEN_RESOURCE_KEY + " not set in strings.xml");
       return;
     }
-
-    MapView mapView = this.createMapView(accessToken, options);
-    MapInstance.createMap(mapView, new MapInstance.MapCreatedCallback() {
+    cordova.getActivity().runOnUiThread(new Runnable() {
       @Override
-      public void onMapReady(final MapInstance map) {
-        cordova.getActivity().runOnUiThread(new Runnable() {
+      public void run() {
+        MapView mapView = createMapView(accessToken, options);
+        MapInstance.createMap(mapView, new MapInstance.MapCreatedCallback() {
           @Override
-          public void run() {
+          public void onMapReady(final MapInstance map) {
             JSONObject resp = new JSONObject();
             try {
               map.configure(options);

From d1a25e6c834f1b8e869682fc577217beb2c75ee0 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Fri, 18 Mar 2016 13:55:17 -0700
Subject: [PATCH 05/37] Fleshing out new instance API.

---
 src/android/MapInstance.java |  62 +++++++++--
 src/android/Mapbox.java      | 201 ++++++++++++++---------------------
 www/Mapbox.js                |  37 +++----
 www/map-instance.js          |  39 +++++++
 4 files changed, 191 insertions(+), 148 deletions(-)

diff --git a/src/android/MapInstance.java b/src/android/MapInstance.java
index b4ca5d3..e140ad3 100644
--- a/src/android/MapInstance.java
+++ b/src/android/MapInstance.java
@@ -1,16 +1,20 @@
 package com.telerik.plugins.mapbox;
 
-import android.content.Context;
-import android.webkit.WebView;
-import android.widget.FrameLayout;
-
+import com.mapbox.mapboxsdk.annotations.Marker;
+import com.mapbox.mapboxsdk.annotations.MarkerOptions;
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
 import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.geometry.LatLng;
 import com.mapbox.mapboxsdk.maps.MapView;
 import com.mapbox.mapboxsdk.maps.MapboxMap;
 import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
 import com.mapbox.mapboxsdk.maps.UiSettings;
 
+import org.apache.cordova.CallbackContext;
 import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.PluginResult;
+import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
@@ -80,8 +84,54 @@ public void configure(JSONObject options) throws JSONException {
         uiSettings.setTiltGesturesEnabled(options.isNull("disableTilt") || !options.getBoolean("disableTilt"));
     }
 
-    public void show(CordovaWebView webView, float retinaFactor, JSONObject options) throws JSONException {
+    public JSONArray getCenter() throws JSONException {
+        CameraPosition cameraPosition = mapboxMap.getCameraPosition();
+        double lat = cameraPosition.target.getLatitude();
+        double lng = cameraPosition.target.getLongitude();
+        double alt = cameraPosition.target.getAltitude();
+        return new JSONArray().put(lat).put(lng).put(alt);
+    }
+
+    public void setCenter(JSONArray coords) throws JSONException {
+        double lat = coords.getDouble(0);
+        double lng = coords.getDouble(1);
+        double alt = coords.getDouble(2);
+
+        mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(
+                new CameraPosition.Builder()
+                        .target(new LatLng(lat, lng, alt))
+                        .build()
+        ));
+    }
+
+    public double getZoom() {
+        CameraPosition cameraPosition = mapboxMap.getCameraPosition();
+        return cameraPosition.zoom;
+    }
+
+    public void setZoom(double zoom) {
+        mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(
+                new CameraPosition.Builder()
+                        .zoom(zoom)
+                        .build()
+        ));
+    }
+
+    public void addMarkers(JSONArray markers) throws JSONException {
+        for (int i = 0; i < markers.length(); i++) {
+            final JSONObject marker = markers.getJSONObject(i);
+            final MarkerOptions mo = new MarkerOptions();
+
+            mo.title(marker.isNull("title") ? null : marker.getString("title"));
+            mo.snippet(marker.isNull("subtitle") ? null : marker.getString("subtitle"));
+            mo.position(new LatLng(marker.getDouble("lat"), marker.getDouble("lng")));
+
+            mapboxMap.addMarker(mo);
+        }
+    }
 
+    public void addMarkerListener(MapboxMap.OnInfoWindowClickListener listener) {
+        mapboxMap.setOnInfoWindowClickListener(listener);
     }
 
     private static String getStyle(final String requested) {
@@ -144,4 +194,4 @@ private static String getStyle(final String requested) {
 //
 //        mapView.setStyleUrl(style);
 //    }
-}
+}
\ No newline at end of file
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index b95bd9a..d319f57 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -9,7 +9,9 @@
 import android.util.DisplayMetrics;
 import android.widget.FrameLayout;
 
+import com.mapbox.mapboxsdk.annotations.Marker;
 import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
 
 import org.apache.cordova.CallbackContext;
 import org.apache.cordova.CordovaArgs;
@@ -18,6 +20,7 @@
 import org.apache.cordova.CordovaWebView;
 import org.apache.cordova.PluginResult;
 
+import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
@@ -34,7 +37,7 @@ public class Mapbox extends CordovaPlugin {
 
   private static final String MAPBOX_ACCESSTOKEN_RESOURCE_KEY = "mapbox_accesstoken";
 
-  private static final String ACTION_SHOW = "show";
+  private static final String ACTION_CREATE = "create";
   private static final String ACTION_HIDE = "hide";
   private static final String ACTION_ADD_MARKERS = "addMarkers";
   private static final String ACTION_ADD_MARKER_CALLBACK = "addMarkerCallback";
@@ -67,90 +70,84 @@ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
   public boolean execute(final String action, final CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
     callback = callbackContext;
 
-    if (ACTION_SHOW.equals(action)) {
+    if (ACTION_CREATE.equals(action)) {
       final JSONObject options = args.getJSONObject(0);
-      this.show(options, callbackContext);
-    } else if (ACTION_HIDE.equals(action)) {
-//        if (mapView != null) {
-//          cordova.getActivity().runOnUiThread(new Runnable() {
-//            @Override
-//            public void run() {
-//              ViewGroup vg = (ViewGroup) mapView.getParent();
-//              if (vg != null) {
-//                vg.removeView(mapView);
-//              }
-//              callbackContext.success();
-//            }
-//          });
-//        }
+      this.create(options, callbackContext);
+    }
 
-    } else if (ACTION_GET_ZOOMLEVEL.equals(action)) {
-//        if (mapView != null) {
-//          cordova.getActivity().runOnUiThread(new Runnable() {
-//            @Override
-//            public void run() {
-//              final double zoomLevel = mapView.getZoom();
-//              callbackContext.success("" + zoomLevel);
-//            }
-//          });
-//        }
+    else if (ACTION_GET_CENTER.equals(action)) {
+      final int mapId = args.getInt(0);
+      final MapInstance map = MapInstance.getMap(mapId);
+      try {
+        callbackContext.success(map.getCenter());
+      } catch (JSONException e) {
+        callbackContext.error(e.getMessage());
+      }
+    }
 
-    } else if (ACTION_SET_ZOOMLEVEL.equals(action)) {
-//        if (mapView != null) {
-//          cordova.getActivity().runOnUiThread(new Runnable() {
-//            @Override
-//            public void run() {
-//              try {
-//                final JSONObject options = args.getJSONObject(0);
-//                final double zoom = options.getDouble("level");
-//                if (zoom >= 0 && zoom <= 20) {
-//                  final boolean animated = !options.isNull("animated") && options.getBoolean("animated");
-//                  mapView.setZoom(zoom, animated);
-//                  callbackContext.success();
-//                } else {
-//                  callbackContext.error("invalid zoomlevel, use any double value from 0 to 20 (like 8.3)");
-//                }
-//              } catch (JSONException e) {
-//                callbackContext.error(e.getMessage());
-//              }
-//            }
-//          });
-//        }
+    else if (ACTION_SET_CENTER.equals(action)) {
+      final int mapId = args.getInt(0);
+      final MapInstance map = MapInstance.getMap(mapId);
+      final JSONArray center = args.getJSONArray(1);
+      try {
+        map.setCenter(center);
+        callbackContext.success();
+      } catch (JSONException e) {
+        callbackContext.error(e.getMessage());
+      }
+    }
 
-    } else if (ACTION_GET_CENTER.equals(action)) {
-//        if (mapView != null) {
-//          cordova.getActivity().runOnUiThread(new Runnable() {
-//            @Override
-//            public void run() {
-//              final LatLng center = mapView.getLatLng();
-//              Map<String, Double> result = new HashMap<String, Double>();
-//              result.put("lat", center.getLatitude());
-//              result.put("lng", center.getLongitude());
-//              callbackContext.success(new JSONObject(result));
-//            }
-//          });
-//        }
+    else if (ACTION_GET_ZOOMLEVEL.equals(action)) {
+      final int mapId = args.getInt(0);
+      final MapInstance map = MapInstance.getMap(mapId);
+      callbackContext.success("" + map.getZoom());
+    }
 
-    } else if (ACTION_SET_CENTER.equals(action)) {
-//        if (mapView != null) {
-//          cordova.getActivity().runOnUiThread(new Runnable() {
-//            @Override
-//            public void run() {
-//              try {
-//                final JSONObject options = args.getJSONObject(0);
-//                final boolean animated = !options.isNull("animated") && options.getBoolean("animated");
-//                final double lat = options.getDouble("lat");
-//                final double lng = options.getDouble("lng");
-//                mapView.setLatLng(new LatLng(lat, lng), animated);
-//                callbackContext.success();
-//              } catch (JSONException e) {
-//                callbackContext.error(e.getMessage());
-//              }
-//            }
-//          });
-//        }
+    else if (ACTION_SET_ZOOMLEVEL.equals(action)) {
+      final int mapId = args.getInt(0);
+      final MapInstance map = MapInstance.getMap(mapId);
+      final double zoom = args.getDouble(1);
+      map.setZoom(zoom);
+      callbackContext.success();
+    }
+
+    else if (ACTION_ADD_MARKERS.equals(action)) {
+      final int mapId = args.getInt(0);
+      final MapInstance map = MapInstance.getMap(mapId);
+      try {
+        map.addMarkers(args.getJSONArray(1));
+        callbackContext.success();
+      } catch (JSONException e) {
+        callbackContext.error(e.getMessage());
+      }
+    }
 
-    } else if (ACTION_GET_TILT.equals(action)) {
+    else if (ACTION_ADD_MARKER_CALLBACK.equals(action)) {
+      final int mapId = args.getInt(0);
+      final MapInstance map = MapInstance.getMap(mapId);
+      map.addMarkerListener(
+        new MapboxMap.OnInfoWindowClickListener() {
+          @Override
+          public boolean onInfoWindowClick(Marker marker) {
+            try {
+              callbackContext.success(
+                new JSONObject()
+                        .put("title", marker.getTitle())
+                        .put("subtitle", marker.getSnippet())
+                        .put("lat", marker.getPosition().getLatitude())
+                        .put("lng", marker.getPosition().getLongitude())
+              );
+              return true;
+            } catch (JSONException e) {
+              String message = "Error in callback of " + ACTION_ADD_MARKER_CALLBACK + ": " + e.getMessage();
+              callbackContext.error(message);
+              return false;
+            }
+          }
+        }
+      );
+    }
+    else if (ACTION_GET_TILT.equals(action)) {
 //        if (mapView != null) {
 //          cordova.getActivity().runOnUiThread(new Runnable() {
 //            @Override
@@ -251,30 +248,13 @@ public void run() {
         }
       });
 
-    } else if (ACTION_ADD_MARKERS.equals(action)) {
-//        cordova.getActivity().runOnUiThread(new Runnable() {
-//          @Override
-//          public void run() {
-//            try {
-//              addMarkers(args.getJSONArray(0));
-//              callbackContext.success();
-//            } catch (JSONException e) {
-//              callbackContext.error(e.getMessage());
-//            }
-//          }
-//        });
-
-    } else if (ACTION_ADD_MARKER_CALLBACK.equals(action)) {
-//        this.markerCallbackContext = callbackContext;
-//        mapView.setOnInfoWindowClickListener(new MarkerClickListener());
-
     } else {
       return false;
     }
     return true;
   }
 
-  private void show(final JSONObject options, final CallbackContext callback) {
+  private void create(final JSONObject options, final CallbackContext callback) {
     if (!this.permissionGranted(COARSE_LOCATION, FINE_LOCATION)) {
       this.requestPermission(COARSE_LOCATION, FINE_LOCATION);
       return;
@@ -294,7 +274,6 @@ public void onMapReady(final MapInstance map) {
             JSONObject resp = new JSONObject();
             try {
               map.configure(options);
-              map.show(webView, retinaFactor, options);
               resp.put("id", map.getId());
               callback.success(resp);
               return;
@@ -352,32 +331,6 @@ private MapView createMapView(String accessToken, JSONObject options) {
 //    }
 //  }
 //
-//  private class MarkerClickListener implements MapView.OnInfoWindowClickListener {
-//
-//    @Override
-//    public boolean onMarkerClick(@NonNull Marker marker) {
-//      // callback
-//      if (markerCallbackContext != null) {
-//        final JSONObject json = new JSONObject();
-//        try {
-//          json.put("title", marker.getTitle());
-//          json.put("subtitle", marker.getSnippet());
-//          json.put("lat", marker.getPosition().getLatitude());
-//          json.put("lng", marker.getPosition().getLongitude());
-//        } catch (JSONException e) {
-//          PluginResult pluginResult = new PluginResult(PluginResult.Status.ERROR,
-//              "Error in callback of " + ACTION_ADD_MARKER_CALLBACK + ": " + e.getMessage());
-//          pluginResult.setKeepCallback(true);
-//          markerCallbackContext.sendPluginResult(pluginResult);
-//        }
-//        PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, json);
-//        pluginResult.setKeepCallback(true);
-//        markerCallbackContext.sendPluginResult(pluginResult);
-//        return true;
-//      }
-//      return false;
-//    }
-//  }
 
   private boolean permissionGranted(String... types) {
     if (Build.VERSION.SDK_INT < 23) {
diff --git a/www/Mapbox.js b/www/Mapbox.js
index 358e715..8804509 100644
--- a/www/Mapbox.js
+++ b/www/Mapbox.js
@@ -3,34 +3,35 @@ var exec = require("cordova/exec"),
 
 module.exports = {
     create: function (options, successCallback, errorCallback) {
-        console.log('Mapbox.js show()');
+        console.log('Mapbox.js create()');
         cordova.exec(function(resp) {
-            console.log('Mapbox.js show()', resp);
+            console.log('Mapbox.js create()', resp);
             var map = new MapInstance(resp.id);
             successCallback(map);
-        }, errorCallback, "Mapbox", "show", [options]);
+        }, errorCallback, "Mapbox", "create", [options]);
     },
 
-  hide: function (options, successCallback, errorCallback) {
-    cordova.exec(successCallback, errorCallback, "Mapbox", "hide", [options]);
-  },
+  // hide: function (options, successCallback, errorCallback) {
+  //   cordova.exec(successCallback, errorCallback, "Mapbox", "hide", [options]);
+  // },
 
-  addMarkers: function (options, successCallback, errorCallback) {
-    cordova.exec(successCallback, errorCallback, "Mapbox", "addMarkers", [options]);
-  },
+  // animateCamera: function (options, successCallback, errorCallback) {
+  //   cordova.exec(successCallback, errorCallback, "Mapbox", "animateCamera", [options]);
+  // },
 
-  addMarkerCallback: function (callback) {
-    cordova.exec(callback, null, "Mapbox", "addMarkerCallback", []);
-  },
+  // addGeoJSON: function (options, successCallback, errorCallback) {
+  //   cordova.exec(successCallback, errorCallback, "Mapbox", "addGeoJSON", [options]);
+  // },
 
-  animateCamera: function (options, successCallback, errorCallback) {
-    cordova.exec(successCallback, errorCallback, "Mapbox", "animateCamera", [options]);
-  },
+  // setTilt: function (options, successCallback, errorCallback) {
+  //   cordova.exec(successCallback, errorCallback, "Mapbox", "setTilt", [options]);
+  // },
 
-  addGeoJSON: function (options, successCallback, errorCallback) {
-    cordova.exec(successCallback, errorCallback, "Mapbox", "addGeoJSON", [options]);
-  },
+  // getTilt: function (successCallback, errorCallback) {
+  //   cordova.exec(successCallback, errorCallback, "Mapbox", "getTilt", []);
+  // },
 
+<<<<<<< 881cb4ce1ff2ab3dcdbb4afb9e31b6c4dbeb58be
   setCenter: function (options, successCallback, errorCallback) {
     cordova.exec(successCallback, errorCallback, "Mapbox", "setCenter", [options]);
   },
diff --git a/www/map-instance.js b/www/map-instance.js
index 34ea06f..122dfc6 100644
--- a/www/map-instance.js
+++ b/www/map-instance.js
@@ -1,5 +1,44 @@
+var exec = require("cordova/exec");
+
 function MapInstance(id) {
     this._id = id;
 }
 
+MapInstance.prototype._exec = function (successCallback, errorCallback, method, args) {
+    args = [this._id].concat(args || []);
+    exec(successCallback, errorCallback, "Mapbox", method, args);
+};
+
+MapInstance.prototype.setCenter = function (options, successCallback, errorCallback) {
+    this._exec(successCallback, errorCallback, "setCenter", [options]);
+};
+
+MapInstance.prototype.getCenter = function (successCallback, errorCallback) {
+    this._exec(successCallback, errorCallback, "getCenter");
+};
+
+MapInstance.prototype.addMarkers = function (options, successCallback, errorCallback) {
+    this._exec(successCallback, errorCallback, "addMarkers", [options]);
+};
+
+MapInstance.prototype.addMarkerCallback = function (callback) {
+    this._exec(callback, null, "addMarkerCallback");
+};
+
+MapInstance.prototype.setCenter = function (center, successCallback, errorCallback) {
+    this._exec(successCallback, errorCallback, "setCenter", [center]);
+};
+
+MapInstance.prototype.getCenter = function (successCallback, errorCallback) {
+    this._exec(successCallback, errorCallback, "getCenter");
+};
+
+MapInstance.prototype.getZoomLevel: function (successCallback, errorCallback) {
+    this._exec(successCallback, errorCallback, "getZoomLevel");
+};
+
+MapInstance.prototype.setZoomLevel: function (zoom, successCallback, errorCallback) {
+    this._exec(successCallback, errorCallback, "setZoomLevel", [zoom]);
+};
+
 module.exports = MapInstance;

From 0fb3d2a801530c58775d69a7874a8a7e17c43bfa Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Fri, 18 Mar 2016 14:46:20 -0700
Subject: [PATCH 06/37] Cleaning up map creation API, particularly options
 processing/handling.

---
 src/android/MapInstance.java | 98 +++++++++++++++---------------------
 src/android/Mapbox.java      | 20 ++------
 www/Mapbox.js                |  2 +-
 www/map-instance.js          |  4 +-
 4 files changed, 47 insertions(+), 77 deletions(-)

diff --git a/src/android/MapInstance.java b/src/android/MapInstance.java
index e140ad3..468f0d5 100644
--- a/src/android/MapInstance.java
+++ b/src/android/MapInstance.java
@@ -1,6 +1,5 @@
 package com.telerik.plugins.mapbox;
 
-import com.mapbox.mapboxsdk.annotations.Marker;
 import com.mapbox.mapboxsdk.annotations.MarkerOptions;
 import com.mapbox.mapboxsdk.camera.CameraPosition;
 import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
@@ -11,9 +10,6 @@
 import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
 import com.mapbox.mapboxsdk.maps.UiSettings;
 
-import org.apache.cordova.CallbackContext;
-import org.apache.cordova.CordovaWebView;
-import org.apache.cordova.PluginResult;
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -26,8 +22,8 @@ public interface MapCreatedCallback {
         void onMapReady(MapInstance map);
     }
 
-    public static MapInstance createMap(MapView mapView, MapCreatedCallback callback) {
-        MapInstance map = new MapInstance(mapView, callback);
+    public static MapInstance createMap(MapView mapView, JSONObject options, MapCreatedCallback callback) {
+        MapInstance map = new MapInstance(mapView, options, callback);
         maps.put(map.getId(), map);
         return map;
     }
@@ -48,7 +44,7 @@ public static MapInstance getMap(int id) {
 
     private MapCreatedCallback constructorCallback;
 
-    private MapInstance(MapView mapView, MapCreatedCallback callback) {
+    private MapInstance(final MapView mapView, final JSONObject options, final MapCreatedCallback callback) {
         this.id = this.ids++;
         this.constructorCallback = callback;
         this.mapView = mapView;
@@ -58,6 +54,7 @@ private MapInstance(MapView mapView, MapCreatedCallback callback) {
             public void onMapReady(MapboxMap mMap) {
                 mapboxMap = mMap;
                 mapboxMap.setMyLocationEnabled(false);
+                applyOptions(options);
                 constructorCallback.onMapReady(MapInstance.this);
             }
         });
@@ -75,15 +72,6 @@ public MapboxMap getMapboxMap() {
         return this.mapboxMap;
     }
 
-    public void configure(JSONObject options) throws JSONException {
-        UiSettings uiSettings = mapboxMap.getUiSettings();
-        uiSettings.setCompassEnabled(options.isNull("hideCompass") || !options.getBoolean("hideCompass"));
-        uiSettings.setRotateGesturesEnabled(options.isNull("disableRotation") || !options.getBoolean("disableRotation"));
-        uiSettings.setScrollGesturesEnabled(options.isNull("disableScroll") || !options.getBoolean("disableScroll"));
-        uiSettings.setZoomGesturesEnabled(options.isNull("disableZoom") || !options.getBoolean("disableZoom"));
-        uiSettings.setTiltGesturesEnabled(options.isNull("disableTilt") || !options.getBoolean("disableTilt"));
-    }
-
     public JSONArray getCenter() throws JSONException {
         CameraPosition cameraPosition = mapboxMap.getCameraPosition();
         double lat = cameraPosition.target.getLatitude();
@@ -153,45 +141,41 @@ private static String getStyle(final String requested) {
         }
     }
 
-//    public void show(JSONObject options, CallbackContext callbackContext) {
-//
-//
-//        try {
-//
-//
-//            // placing these offscreen in case the user wants to hide them
-//            if (!options.isNull("hideAttribution") && options.getBoolean("hideAttribution")) {
-//                mapView.setAttributionMargins(-300, 0, 0, 0);
-//            }
-//            if (!options.isNull("hideLogo") && options.getBoolean("hideLogo")) {
-//                mapView.setLogoMargins(-300, 0, 0, 0);
-//            }
-//
-//            if (showUserLocation) {
-//                showUserLocation();
-//            }
-//
-//            Double zoom = options.isNull("zoomLevel") ? 10 : options.getDouble("zoomLevel");
-//            float zoomLevel = zoom.floatValue();
-//            if (center != null) {
-//                final double lat = center.getDouble("lat");
-//                final double lng = center.getDouble("lng");
-//                mapView.setLatLng(new LatLngZoom(lat, lng, zoomLevel));
-//            } else {
-//                if (zoomLevel > 18.0) {
-//                    zoomLevel = 18.0f;
-//                }
-//                mapView.setZoom(zoomLevel);
-//            }
-//
-//            if (options.has("markers")) {
-//                addMarkers(options.getJSONArray("markers"));
-//            }
-//        } catch (JSONException e) {
-//            callbackContext.error(e.getMessage());
-//            return;
-//        }
-//
-//        mapView.setStyleUrl(style);
-//    }
+    private void applyOptions(JSONObject options) {
+        try {
+            UiSettings uiSettings = mapboxMap.getUiSettings();
+            uiSettings.setCompassEnabled(options.isNull("hideCompass") || !options.getBoolean("hideCompass"));
+            uiSettings.setRotateGesturesEnabled(options.isNull("disableRotation") || !options.getBoolean("disableRotation"));
+            uiSettings.setScrollGesturesEnabled(options.isNull("disableScroll") || !options.getBoolean("disableScroll"));
+            uiSettings.setZoomGesturesEnabled(options.isNull("disableZoom") || !options.getBoolean("disableZoom"));
+            uiSettings.setTiltGesturesEnabled(options.isNull("disableTilt") || !options.getBoolean("disableTilt"));
+
+            if (!options.isNull("hideAttribution") && options.getBoolean("hideAttribution")) {
+                uiSettings.setAttributionMargins(-300, 0, 0, 0);
+            }
+
+            if (!options.isNull("hideLogo") && options.getBoolean("hideLogo")) {
+                uiSettings.setLogoMargins(-300, 0, 0, 0);
+            }
+
+            if (!options.isNull("center")) {
+                this.setCenter(options.getJSONArray("center"));
+            }
+
+            if (!options.isNull("zoomLevel")) {
+                this.setZoom(options.getDouble("zoom"));
+            }
+
+            if (options.has("markers")) {
+                this.addMarkers(options.getJSONArray("markers"));
+            }
+
+            if (options.has("style")) {
+                this.mapView.setStyleUrl(this.getStyle(options.optString("style")));
+            }
+        }
+        catch (JSONException e) {
+            e.printStackTrace();
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index d319f57..899f0bb 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -268,16 +268,16 @@ private void create(final JSONObject options, final CallbackContext callback) {
       @Override
       public void run() {
         MapView mapView = createMapView(accessToken, options);
-        MapInstance.createMap(mapView, new MapInstance.MapCreatedCallback() {
+        MapInstance.createMap(mapView, options, new MapInstance.MapCreatedCallback() {
           @Override
           public void onMapReady(final MapInstance map) {
             JSONObject resp = new JSONObject();
             try {
-              map.configure(options);
               resp.put("id", map.getId());
               callback.success(resp);
               return;
-            } catch (JSONException e) {
+            }
+            catch (JSONException e) {
               e.printStackTrace();
               callback.error("Failed to create map.");
               return;
@@ -293,8 +293,6 @@ private MapView createMapView(String accessToken, JSONObject options) {
     mapView.setAccessToken(accessToken);
 
     try {
-  //    final String style = getStyle(options.optString("style"));
-  //    final JSONObject center = options.isNull("center") ? null : options.getJSONObject("center");
       final JSONObject margins = options.isNull("margins") ? null : options.getJSONObject("margins");
       final int left = (int) (retinaFactor * (margins == null || margins.isNull("left") ? 0 : margins.getInt("left")));
       final int right = (int) (retinaFactor * (margins == null || margins.isNull("right") ? 0 : margins.getInt("right")));
@@ -320,18 +318,6 @@ private MapView createMapView(String accessToken, JSONObject options) {
     return mapView;
   }
 
-//  private void addMarkers(JSONArray markers) throws JSONException {
-//    for (int i=0; i<markers.length(); i++) {
-//      final JSONObject marker = markers.getJSONObject(i);
-//      final MarkerOptions mo = new MarkerOptions();
-//      mo.title(marker.isNull("title") ? null : marker.getString("title"));
-//      mo.snippet(marker.isNull("subtitle") ? null : marker.getString("subtitle"));
-//      mo.position(new LatLng(marker.getDouble("lat"), marker.getDouble("lng")));
-//      mapView.addMarker(mo);
-//    }
-//  }
-//
-
   private boolean permissionGranted(String... types) {
     if (Build.VERSION.SDK_INT < 23) {
       return true;
diff --git a/www/Mapbox.js b/www/Mapbox.js
index 8804509..9a91c51 100644
--- a/www/Mapbox.js
+++ b/www/Mapbox.js
@@ -9,7 +9,7 @@ module.exports = {
             var map = new MapInstance(resp.id);
             successCallback(map);
         }, errorCallback, "Mapbox", "create", [options]);
-    },
+    }
 
   // hide: function (options, successCallback, errorCallback) {
   //   cordova.exec(successCallback, errorCallback, "Mapbox", "hide", [options]);
diff --git a/www/map-instance.js b/www/map-instance.js
index 122dfc6..6fa3d38 100644
--- a/www/map-instance.js
+++ b/www/map-instance.js
@@ -33,11 +33,11 @@ MapInstance.prototype.getCenter = function (successCallback, errorCallback) {
     this._exec(successCallback, errorCallback, "getCenter");
 };
 
-MapInstance.prototype.getZoomLevel: function (successCallback, errorCallback) {
+MapInstance.prototype.getZoomLevel = function (successCallback, errorCallback) {
     this._exec(successCallback, errorCallback, "getZoomLevel");
 };
 
-MapInstance.prototype.setZoomLevel: function (zoom, successCallback, errorCallback) {
+MapInstance.prototype.setZoomLevel = function (zoom, successCallback, errorCallback) {
     this._exec(successCallback, errorCallback, "setZoomLevel", [zoom]);
 };
 

From 36cc05233313aa005592a9374d166285f8129224 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Fri, 18 Mar 2016 16:11:34 -0700
Subject: [PATCH 07/37] Added support for permission requests via command
 pattern. Implemented showUserLocation.

---
 src/android/MapInstance.java |   9 ++-
 src/android/Mapbox.java      | 151 +++++++++++++++++++++++++++--------
 www/map-instance.js          |   4 +
 3 files changed, 131 insertions(+), 33 deletions(-)

diff --git a/src/android/MapInstance.java b/src/android/MapInstance.java
index 468f0d5..2e1e77e 100644
--- a/src/android/MapInstance.java
+++ b/src/android/MapInstance.java
@@ -53,7 +53,6 @@ private MapInstance(final MapView mapView, final JSONObject options, final MapCr
             @Override
             public void onMapReady(MapboxMap mMap) {
                 mapboxMap = mMap;
-                mapboxMap.setMyLocationEnabled(false);
                 applyOptions(options);
                 constructorCallback.onMapReady(MapInstance.this);
             }
@@ -122,6 +121,10 @@ public void addMarkerListener(MapboxMap.OnInfoWindowClickListener listener) {
         mapboxMap.setOnInfoWindowClickListener(listener);
     }
 
+    public void showUserLocation(boolean enabled) {
+        mapboxMap.setMyLocationEnabled(enabled);
+    }
+
     private static String getStyle(final String requested) {
         if ("light".equalsIgnoreCase(requested)) {
             return Style.LIGHT;
@@ -150,6 +153,10 @@ private void applyOptions(JSONObject options) {
             uiSettings.setZoomGesturesEnabled(options.isNull("disableZoom") || !options.getBoolean("disableZoom"));
             uiSettings.setTiltGesturesEnabled(options.isNull("disableTilt") || !options.getBoolean("disableTilt"));
 
+            if (!options.isNull("showUserLocation") && !options.getBoolean("showUserLocation")) {
+                this.showUserLocation(false);
+            }
+
             if (!options.isNull("hideAttribution") && options.getBoolean("hideAttribution")) {
                 uiSettings.setAttributionMargins(-300, 0, 0, 0);
             }
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 899f0bb..c689f84 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -18,12 +18,13 @@
 import org.apache.cordova.CordovaInterface;
 import org.apache.cordova.CordovaPlugin;
 import org.apache.cordova.CordovaWebView;
-import org.apache.cordova.PluginResult;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.util.HashMap;
+
 // TODO for screen rotation, see https://www.mapbox.com/mapbox-android-sdk/#screen-rotation
 // TODO fox Xwalk compat, see nativepagetransitions plugin
 // TODO look at demo app: https://github.com/mapbox/mapbox-gl-native/blob/master/android/java/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxgl/testapp/MainActivity.java
@@ -31,14 +32,13 @@ public class Mapbox extends CordovaPlugin {
 
   public static final String FINE_LOCATION = Manifest.permission.ACCESS_FINE_LOCATION;
   public static final String COARSE_LOCATION = Manifest.permission.ACCESS_COARSE_LOCATION;
-  public static final int LOCATION_REQ_CODE = 0;
 
   public static final int PERMISSION_DENIED_ERROR = 20;
 
   private static final String MAPBOX_ACCESSTOKEN_RESOURCE_KEY = "mapbox_accesstoken";
 
   private static final String ACTION_CREATE = "create";
-  private static final String ACTION_HIDE = "hide";
+  private static final String ACTION_SHOW_USER_LOCATION = "showUserLocation";
   private static final String ACTION_ADD_MARKERS = "addMarkers";
   private static final String ACTION_ADD_MARKER_CALLBACK = "addMarkerCallback";
   private static final String ACTION_ADD_POLYGON = "addPolygon";
@@ -53,9 +53,6 @@ public class Mapbox extends CordovaPlugin {
 
   private static float retinaFactor;
   private String accessToken;
-  private CallbackContext callback;
-
-//  private boolean showUserLocation;
 
   @Override
   public void initialize(CordovaInterface cordova, CordovaWebView webView) {
@@ -65,16 +62,35 @@ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
     this.accessToken = this.getAccessToken();
   }
 
-
   @Override
   public boolean execute(final String action, final CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
-    callback = callbackContext;
+    Command command = Command.create(action, args, callbackContext);
+    return execute(command);
+  }
+
+  public boolean execute(Command command) throws JSONException {
+    final String action = command.getAction();
+    final CordovaArgs args = command.getArgs();
+    final CallbackContext callbackContext = command.getCallbackContext();
 
     if (ACTION_CREATE.equals(action)) {
       final JSONObject options = args.getJSONObject(0);
+      boolean showUserLocation = !options.isNull("showUserLocation") && options.getBoolean("showUserLocation");
+      if (showUserLocation && !requestPermission(command, COARSE_LOCATION, FINE_LOCATION)) {
+        return false;
+      }
       this.create(options, callbackContext);
     }
 
+    else if (ACTION_SHOW_USER_LOCATION.equals(action)) {
+      final int mapId = args.getInt(0);
+      final MapInstance map = MapInstance.getMap(mapId);
+      boolean enabled = args.getBoolean(1);
+      if (requestPermission(command, COARSE_LOCATION, FINE_LOCATION)) {
+        map.showUserLocation(enabled);
+      }
+    }
+
     else if (ACTION_GET_CENTER.equals(action)) {
       final int mapId = args.getInt(0);
       final MapInstance map = MapInstance.getMap(mapId);
@@ -255,15 +271,11 @@ public void run() {
   }
 
   private void create(final JSONObject options, final CallbackContext callback) {
-    if (!this.permissionGranted(COARSE_LOCATION, FINE_LOCATION)) {
-      this.requestPermission(COARSE_LOCATION, FINE_LOCATION);
-      return;
-    }
-
     if (accessToken == null) {
       callback.error(MAPBOX_ACCESSTOKEN_RESOURCE_KEY + " not set in strings.xml");
       return;
     }
+
     cordova.getActivity().runOnUiThread(new Runnable() {
       @Override
       public void run() {
@@ -276,8 +288,7 @@ public void onMapReady(final MapInstance map) {
               resp.put("id", map.getId());
               callback.success(resp);
               return;
-            }
-            catch (JSONException e) {
+            } catch (JSONException e) {
               e.printStackTrace();
               callback.error("Failed to create map.");
               return;
@@ -331,34 +342,30 @@ private boolean permissionGranted(String... types) {
   }
 
   protected void showUserLocation() {
-    if (permissionGranted(COARSE_LOCATION, FINE_LOCATION)) {
-      //noinspection MissingPermission
-//      mapView.setMyLocationEnabled(showUserLocation);
-    } else {
-      requestPermission(COARSE_LOCATION, FINE_LOCATION);
-    }
+
   }
 
 
-  private void requestPermission(String... types) {
-    ActivityCompat.requestPermissions(
-        this.cordova.getActivity(),
-        types,
-        LOCATION_REQ_CODE);
+  private boolean requestPermission(Command command, String... types) {
+    if (!permissionGranted(types)) {
+      int commandId = Command.save(command);
+      ActivityCompat.requestPermissions(this.cordova.getActivity(), types, commandId);
+      return false;
+    } else {
+      return true;
+    }
   }
 
   // TODO
-  public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException {
+  public void onRequestPermissionResult(int commandId, String[] permissions, int[] grantResults) throws JSONException {
     for (int r : grantResults) {
       if (r == PackageManager.PERMISSION_DENIED) {
-        this.callback.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR));
+        Command.error(commandId, PERMISSION_DENIED_ERROR);
         return;
       }
     }
-    switch (requestCode) {
-      case LOCATION_REQ_CODE:
-        break;
-    }
+
+    Command.execute(this, commandId);
   }
 
 //  public void onPause(boolean multitasking) {
@@ -397,4 +404,84 @@ private String getAccessToken() {
 
     return accessToken;
   }
+
+}
+
+class Command {
+  private static int ids = 0;
+
+  private static HashMap<Integer, Command> commands = new HashMap<Integer, Command>();
+
+  public static Command create(final String action, final CordovaArgs args, final CallbackContext callbackContext) {
+    return new Command(action, args, callbackContext);
+  }
+
+  public static int save(final String action, final CordovaArgs args, final CallbackContext callbackContext) {
+    return save(create(action, args, callbackContext));
+  }
+
+  public static int save(Command command) {
+    int id = command.getId();
+    commands.put(id, command);
+    return id;
+  }
+
+  public static void execute(Mapbox plugin, int id) throws JSONException {
+    Command command = commands.remove(id);
+    plugin.execute(command);
+  }
+
+  public static void error(final int id, final int errorCode) {
+    error(id, errorCode, null);
+  }
+
+  public static void error(final int id, final int errorCode, final String errorMessage) {
+    Command command = commands.remove(id);
+    CallbackContext callback = command.getCallbackContext();
+    JSONObject error = new JSONObject();
+    String message = "Error (" + errorCode + ")";
+
+    if (errorMessage != null) {
+      message += ": "  + errorMessage;
+    }
+
+    try {
+      error.put("code", id);
+      error.put("message", message);
+      callback.error(error);
+    } catch (JSONException e) {
+      callback.error(message);
+    }
+  }
+
+  private int id;
+
+  private String action;
+
+  private CordovaArgs args;
+
+  private CallbackContext callbackContext;
+
+  private Command(final String action, final CordovaArgs args, final CallbackContext callbackContext) {
+    this.id = ids++;
+    this.action = action;
+    this.args = args;
+    this.callbackContext = callbackContext;
+  }
+
+  public int getId() {
+    return id;
+  }
+
+  public String getAction() {
+    return action;
+  }
+
+  public CordovaArgs getArgs() {
+    return args;
+  }
+
+  public CallbackContext getCallbackContext() {
+    return callbackContext;
+  }
 }
diff --git a/www/map-instance.js b/www/map-instance.js
index 6fa3d38..cd5139e 100644
--- a/www/map-instance.js
+++ b/www/map-instance.js
@@ -41,4 +41,8 @@ MapInstance.prototype.setZoomLevel = function (zoom, successCallback, errorCallb
     this._exec(successCallback, errorCallback, "setZoomLevel", [zoom]);
 };
 
+MapInstance.prototype.showUserLocation = function (enabled, successCallback, errorCallback) {
+    this._exec(successCallback, errorCallback, "showUserLocation", [enabled]);
+};
+
 module.exports = MapInstance;

From 961c4d98c5761244a1b234ea436cd7c6bf27ffbc Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Mon, 21 Mar 2016 12:51:14 -0700
Subject: [PATCH 08/37] Attempt at matching mapbox-gl-js api.

---
 src/android/MapInstance.java |   4 +-
 src/android/Mapbox.java      |  12 +---
 www/Mapbox.js                |  71 +---------------------
 www/map-instance.js          | 110 +++++++++++++++++++++++++++++++----
 4 files changed, 104 insertions(+), 93 deletions(-)

diff --git a/src/android/MapInstance.java b/src/android/MapInstance.java
index 2e1e77e..72a3342 100644
--- a/src/android/MapInstance.java
+++ b/src/android/MapInstance.java
@@ -153,8 +153,8 @@ private void applyOptions(JSONObject options) {
             uiSettings.setZoomGesturesEnabled(options.isNull("disableZoom") || !options.getBoolean("disableZoom"));
             uiSettings.setTiltGesturesEnabled(options.isNull("disableTilt") || !options.getBoolean("disableTilt"));
 
-            if (!options.isNull("showUserLocation") && !options.getBoolean("showUserLocation")) {
-                this.showUserLocation(false);
+            if (!options.isNull("showUserLocation")) {
+                this.showUserLocation(options.getBoolean("showUserLocation"));
             }
 
             if (!options.isNull("hideAttribution") && options.getBoolean("hideAttribution")) {
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index c689f84..15158c9 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -76,10 +76,9 @@ public boolean execute(Command command) throws JSONException {
     if (ACTION_CREATE.equals(action)) {
       final JSONObject options = args.getJSONObject(0);
       boolean showUserLocation = !options.isNull("showUserLocation") && options.getBoolean("showUserLocation");
-      if (showUserLocation && !requestPermission(command, COARSE_LOCATION, FINE_LOCATION)) {
-        return false;
+      if (!showUserLocation || requestPermission(command, COARSE_LOCATION, FINE_LOCATION)) {
+        this.create(options, callbackContext);
       }
-      this.create(options, callbackContext);
     }
 
     else if (ACTION_SHOW_USER_LOCATION.equals(action)) {
@@ -341,15 +340,10 @@ private boolean permissionGranted(String... types) {
     return true;
   }
 
-  protected void showUserLocation() {
-
-  }
-
-
   private boolean requestPermission(Command command, String... types) {
     if (!permissionGranted(types)) {
       int commandId = Command.save(command);
-      ActivityCompat.requestPermissions(this.cordova.getActivity(), types, commandId);
+      cordova.requestPermissions(this, commandId, types);
       return false;
     } else {
       return true;
diff --git a/www/Mapbox.js b/www/Mapbox.js
index 9a91c51..d6a9d1e 100644
--- a/www/Mapbox.js
+++ b/www/Mapbox.js
@@ -1,70 +1 @@
-var exec = require("cordova/exec"),
-    MapInstance = require("./MapInstance");
-
-module.exports = {
-    create: function (options, successCallback, errorCallback) {
-        console.log('Mapbox.js create()');
-        cordova.exec(function(resp) {
-            console.log('Mapbox.js create()', resp);
-            var map = new MapInstance(resp.id);
-            successCallback(map);
-        }, errorCallback, "Mapbox", "create", [options]);
-    }
-
-  // hide: function (options, successCallback, errorCallback) {
-  //   cordova.exec(successCallback, errorCallback, "Mapbox", "hide", [options]);
-  // },
-
-  // animateCamera: function (options, successCallback, errorCallback) {
-  //   cordova.exec(successCallback, errorCallback, "Mapbox", "animateCamera", [options]);
-  // },
-
-  // addGeoJSON: function (options, successCallback, errorCallback) {
-  //   cordova.exec(successCallback, errorCallback, "Mapbox", "addGeoJSON", [options]);
-  // },
-
-  // setTilt: function (options, successCallback, errorCallback) {
-  //   cordova.exec(successCallback, errorCallback, "Mapbox", "setTilt", [options]);
-  // },
-
-  // getTilt: function (successCallback, errorCallback) {
-  //   cordova.exec(successCallback, errorCallback, "Mapbox", "getTilt", []);
-  // },
-
-<<<<<<< 881cb4ce1ff2ab3dcdbb4afb9e31b6c4dbeb58be
-  setCenter: function (options, successCallback, errorCallback) {
-    cordova.exec(successCallback, errorCallback, "Mapbox", "setCenter", [options]);
-  },
-
-  getCenter: function (successCallback, errorCallback) {
-    cordova.exec(successCallback, errorCallback, "Mapbox", "getCenter", []);
-  },
-
-  setTilt: function (options, successCallback, errorCallback) {
-    cordova.exec(successCallback, errorCallback, "Mapbox", "setTilt", [options]);
-  },
-
-  getTilt: function (successCallback, errorCallback) {
-    cordova.exec(successCallback, errorCallback, "Mapbox", "getTilt", []);
-  },
-
-  getZoomLevel: function (successCallback, errorCallback) {
-    cordova.exec(successCallback, errorCallback, "Mapbox", "getZoomLevel", []);
-  },
-
-  setZoomLevel: function (options, successCallback, errorCallback) {
-    cordova.exec(successCallback, errorCallback, "Mapbox", "setZoomLevel", [options]);
-  },
-
-  addPolygon: function (options, successCallback, errorCallback) {
-    cordova.exec(successCallback, errorCallback, "Mapbox", "addPolygon", [options]);
-  },
-
-  convertCoordinate: function(options, successCallback, errorCallback){
-    cordova.exec(successCallback, errorCallback, "Mapbox", "convertCoordinate", [options]);
-  },
-
-  convertPoint: function(options, successCallback, errorCallback){
-    cordova.exec(successCallback, errorCallback, "Mapbox", "convertPoint", [options]);
-  }
-};
+module.exports = require("./MapInstance");
diff --git a/www/map-instance.js b/www/map-instance.js
index cd5139e..118653c 100644
--- a/www/map-instance.js
+++ b/www/map-instance.js
@@ -1,48 +1,134 @@
-var exec = require("cordova/exec");
+var exec = require("cordova/exec"),
+    channel = require("cordova/channel"),
+    channelIds = 0;
 
-function MapInstance(id) {
-    this._id = id;
+var Events = Mixin({
+        initEvents: function (prefix) {
+            if (!this._channelPrefix) {
+                this._channelPrefix = prefix + "." + (channelIds++);
+            }
+        },
+
+        _prefix: function (type) {
+            return this._channelPrefix + "." + type;
+        },
+
+        _channel: function (type) {
+            var t = this._prefix(type);
+            return this._channels[t];
+        },
+
+        createChannel: function (type) {
+            var t = this._prefix(type),
+                c = channel.create(t);
+            this._channels[t] = c;
+        },
+
+        createStickyChannel: function (type) {
+            var t = this._prefix(type),
+                c = channel.createSticky(t);
+            this._channels[type] = c;
+        },
+
+        once: function (type, listener) {
+            var t = this._prefix(type),
+                onEvent = function (e) {
+                    listener(e);
+                    this.off(t, onEvent);
+                };
+            this.on(t, onEvent);
+        },
+
+        on: function (type, listener) {
+            this._channel(type).subscribe(listener);
+        },
+
+        off: function (type, listener) {
+            this._channel(type).unsubscribe(listener);
+        },
+
+        fire: function (type, e) {
+            this._channel(type).fire(e);
+        }
+    });
+
+function Mixin(behaviour) {
+    return function(target) {
+        return Object.assign(target.behaviour);
+    };
 }
 
+function MapInstance(options) {
+    var onLoad = _onLoad.bind(this),
+        onError = this._error.bind(this);
+
+    this._error = onError;
+
+    this.initEvents("Mapbox.MapInstance");
+    this.createStickyChannel("load");
+
+    this._exec(onLoad, this._error, "Mapbox", "create", [options]);
+
+    function _onLoad(resp) {
+        this._id = resp.id;
+        this.loaded = true;
+
+        this.fire("load", {map: this});
+    }
+}
+
+Events(MapInstance.prototype);
+
+MapInstance.prototype._error = function (err) {
+    console.error("Map error (ID: " + this._id + "): ", err);
+};
+
 MapInstance.prototype._exec = function (successCallback, errorCallback, method, args) {
     args = [this._id].concat(args || []);
     exec(successCallback, errorCallback, "Mapbox", method, args);
 };
 
+MapInstance.prototype._execAfterLoad = function () {
+    var args = arguments;
+    this.once('load', function (map) {
+        this._exec.apply(this, args);
+    }.bind(this));
+};
+
 MapInstance.prototype.setCenter = function (options, successCallback, errorCallback) {
-    this._exec(successCallback, errorCallback, "setCenter", [options]);
+    this._execAfterLoad(successCallback, errorCallback, "setCenter", [options]);
 };
 
 MapInstance.prototype.getCenter = function (successCallback, errorCallback) {
-    this._exec(successCallback, errorCallback, "getCenter");
+    this._execAfterLoad(successCallback, errorCallback, "getCenter");
 };
 
 MapInstance.prototype.addMarkers = function (options, successCallback, errorCallback) {
-    this._exec(successCallback, errorCallback, "addMarkers", [options]);
+    this._execAfterLoad(successCallback, errorCallback, "addMarkers", [options]);
 };
 
 MapInstance.prototype.addMarkerCallback = function (callback) {
-    this._exec(callback, null, "addMarkerCallback");
+    this._execAfterLoad(callback, null, "addMarkerCallback");
 };
 
 MapInstance.prototype.setCenter = function (center, successCallback, errorCallback) {
-    this._exec(successCallback, errorCallback, "setCenter", [center]);
+    this._execAfterLoad(successCallback, errorCallback, "setCenter", [center]);
 };
 
 MapInstance.prototype.getCenter = function (successCallback, errorCallback) {
-    this._exec(successCallback, errorCallback, "getCenter");
+    this._execAfterLoad(successCallback, errorCallback, "getCenter");
 };
 
 MapInstance.prototype.getZoomLevel = function (successCallback, errorCallback) {
-    this._exec(successCallback, errorCallback, "getZoomLevel");
+    this._execAfterLoad(successCallback, errorCallback, "getZoomLevel");
 };
 
 MapInstance.prototype.setZoomLevel = function (zoom, successCallback, errorCallback) {
-    this._exec(successCallback, errorCallback, "setZoomLevel", [zoom]);
+    this._execAfterLoad(successCallback, errorCallback, "setZoomLevel", [zoom]);
 };
 
 MapInstance.prototype.showUserLocation = function (enabled, successCallback, errorCallback) {
-    this._exec(successCallback, errorCallback, "showUserLocation", [enabled]);
+    this._execAfterLoad(successCallback, errorCallback, "showUserLocation", [enabled]);
 };
 
 module.exports = MapInstance;

From cbe2269c3783ba6b21da6ff75bb76499dd5c00c7 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Mon, 21 Mar 2016 12:56:33 -0700
Subject: [PATCH 09/37] adding assign function (based on Object.assign polyfill
 from MDN).

---
 www/Mapbox.js       |  5 ++++-
 www/map-instance.js | 52 +++++++++++++++++++++++++++++++++------------
 2 files changed, 42 insertions(+), 15 deletions(-)

diff --git a/www/Mapbox.js b/www/Mapbox.js
index d6a9d1e..b68f7db 100644
--- a/www/Mapbox.js
+++ b/www/Mapbox.js
@@ -1 +1,4 @@
-module.exports = require("./MapInstance");
+var MapInstance = require("./MapInstance");
+module.exports = {
+    Map: MapInstance
+};
diff --git a/www/map-instance.js b/www/map-instance.js
index 118653c..e4ee50d 100644
--- a/www/map-instance.js
+++ b/www/map-instance.js
@@ -13,30 +13,33 @@ var Events = Mixin({
             return this._channelPrefix + "." + type;
         },
 
-        _channel: function (type) {
+        _channel: function (type, sticky) {
             var t = this._prefix(type);
+            if (!this._channels) {
+                this._channels = {};
+            }
+            if (sticky !== undefined) {
+                this._channels[t] = sticky ?
+                    channel.createSticky(t) :
+                    channel.create(t);
+            }
             return this._channels[t];
         },
 
         createChannel: function (type) {
-            var t = this._prefix(type),
-                c = channel.create(t);
-            this._channels[t] = c;
+            this._channel(type, false);
         },
 
         createStickyChannel: function (type) {
-            var t = this._prefix(type),
-                c = channel.createSticky(t);
-            this._channels[type] = c;
+            this._channel(type, true);
         },
 
         once: function (type, listener) {
-            var t = this._prefix(type),
-                onEvent = function (e) {
+            var onEvent = function (e) {
                     listener(e);
-                    this.off(t, onEvent);
+                    this.off(type, onEvent);
                 };
-            this.on(t, onEvent);
+            this.on(type, onEvent.bind(this));
         },
 
         on: function (type, listener) {
@@ -52,9 +55,28 @@ var Events = Mixin({
         }
     });
 
+function assign(target) {
+    if (target === undefined || target === null) {
+        throw new TypeError('Cannot convert undefined or null to object');
+    }
+
+    var output = Object(target);
+    for (var index = 1; index < arguments.length; index++) {
+        var source = arguments[index];
+        if (source !== undefined && source !== null) {
+            for (var nextKey in source) {
+                if (source.hasOwnProperty(nextKey)) {
+                    output[nextKey] = source[nextKey];
+                }
+            }
+        }
+    }
+    return output;
+}
+
 function Mixin(behaviour) {
     return function(target) {
-        return Object.assign(target.behaviour);
+        return assign(target, behaviour);
     };
 }
 
@@ -67,7 +89,7 @@ function MapInstance(options) {
     this.initEvents("Mapbox.MapInstance");
     this.createStickyChannel("load");
 
-    this._exec(onLoad, this._error, "Mapbox", "create", [options]);
+    exec(onLoad, this._error, "Mapbox", "create", [options]);
 
     function _onLoad(resp) {
         this._id = resp.id;
@@ -80,7 +102,9 @@ function MapInstance(options) {
 Events(MapInstance.prototype);
 
 MapInstance.prototype._error = function (err) {
-    console.error("Map error (ID: " + this._id + "): ", err);
+    var error = new Error("Map error (ID: " + this._id + "): " + err);
+    console.warn("throwing MapError: ", error);
+    throw error;
 };
 
 MapInstance.prototype._exec = function (successCallback, errorCallback, method, args) {

From 47b73738e7dd60d93bacb4fa0da54e8d5aaca4e3 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Mon, 21 Mar 2016 16:44:10 -0700
Subject: [PATCH 10/37] Implemented jumpTo() from mapbox-gl-js api.

---
 src/android/MapInstance.java | 57 +++++++++++++++++++++++-------------
 1 file changed, 37 insertions(+), 20 deletions(-)

diff --git a/src/android/MapInstance.java b/src/android/MapInstance.java
index 72a3342..63070d9 100644
--- a/src/android/MapInstance.java
+++ b/src/android/MapInstance.java
@@ -80,13 +80,13 @@ public JSONArray getCenter() throws JSONException {
     }
 
     public void setCenter(JSONArray coords) throws JSONException {
-        double lat = coords.getDouble(0);
-        double lng = coords.getDouble(1);
-        double alt = coords.getDouble(2);
+        double lng = coords.getDouble(0);
+        double lat = coords.getDouble(1);
+        //double alt = coords.getDouble(2);
 
         mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(
                 new CameraPosition.Builder()
-                        .target(new LatLng(lat, lng, alt))
+                        .target(new LatLng(lat, lng))
                         .build()
         ));
     }
@@ -104,6 +104,29 @@ public void setZoom(double zoom) {
         ));
     }
 
+    public void jumpTo(JSONObject options) throws JSONException {
+        CameraPosition current = mapboxMap.getCameraPosition();
+        CameraPosition.Builder builder = new CameraPosition.Builder(current);
+
+        if (!options.isNull("zoom")) {
+            builder.zoom(options.getDouble("zoom"));
+        }
+
+        if (!options.isNull("center")) {
+            JSONArray center = options.getJSONArray("center");
+            double lng = center.getDouble(0);
+            double lat = center.getDouble(1);
+            builder.target(new LatLng(lat, lng));
+        }
+
+        // TODO: Bearing
+
+        // TODO: Pitch
+
+        CameraPosition position = builder.build();
+        mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(position));
+    }
+
     public void addMarkers(JSONArray markers) throws JSONException {
         for (int i = 0; i < markers.length(); i++) {
             final JSONObject marker = markers.getJSONObject(i);
@@ -144,8 +167,16 @@ private static String getStyle(final String requested) {
         }
     }
 
-    private void applyOptions(JSONObject options) {
+    public void applyOptions(JSONObject options) {
         try {
+            if (options.has("style")) {
+                this.mapView.setStyleUrl(this.getStyle(options.optString("style")));
+            }
+
+            if (!options.isNull("showUserLocation")) {
+                this.showUserLocation(options.getBoolean("showUserLocation"));
+            }
+
             UiSettings uiSettings = mapboxMap.getUiSettings();
             uiSettings.setCompassEnabled(options.isNull("hideCompass") || !options.getBoolean("hideCompass"));
             uiSettings.setRotateGesturesEnabled(options.isNull("disableRotation") || !options.getBoolean("disableRotation"));
@@ -153,10 +184,6 @@ private void applyOptions(JSONObject options) {
             uiSettings.setZoomGesturesEnabled(options.isNull("disableZoom") || !options.getBoolean("disableZoom"));
             uiSettings.setTiltGesturesEnabled(options.isNull("disableTilt") || !options.getBoolean("disableTilt"));
 
-            if (!options.isNull("showUserLocation")) {
-                this.showUserLocation(options.getBoolean("showUserLocation"));
-            }
-
             if (!options.isNull("hideAttribution") && options.getBoolean("hideAttribution")) {
                 uiSettings.setAttributionMargins(-300, 0, 0, 0);
             }
@@ -165,21 +192,11 @@ private void applyOptions(JSONObject options) {
                 uiSettings.setLogoMargins(-300, 0, 0, 0);
             }
 
-            if (!options.isNull("center")) {
-                this.setCenter(options.getJSONArray("center"));
-            }
-
-            if (!options.isNull("zoomLevel")) {
-                this.setZoom(options.getDouble("zoom"));
-            }
-
             if (options.has("markers")) {
                 this.addMarkers(options.getJSONArray("markers"));
             }
 
-            if (options.has("style")) {
-                this.mapView.setStyleUrl(this.getStyle(options.optString("style")));
-            }
+            this.jumpTo(options);
         }
         catch (JSONException e) {
             e.printStackTrace();

From cb505f47fea60ad19cc90cf3ef8904c07e28d37f Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Mon, 21 Mar 2016 16:44:31 -0700
Subject: [PATCH 11/37] Fixed thread issue with setUserLocation().

---
 src/android/Mapbox.java | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 15158c9..cc6362c 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -84,9 +84,14 @@ public boolean execute(Command command) throws JSONException {
     else if (ACTION_SHOW_USER_LOCATION.equals(action)) {
       final int mapId = args.getInt(0);
       final MapInstance map = MapInstance.getMap(mapId);
-      boolean enabled = args.getBoolean(1);
+      final boolean enabled = args.getBoolean(1);
       if (requestPermission(command, COARSE_LOCATION, FINE_LOCATION)) {
-        map.showUserLocation(enabled);
+        cordova.getActivity().runOnUiThread(new Runnable() {
+          @Override
+          public void run() {
+            map.showUserLocation(enabled);
+          }
+        });
       }
     }
 

From df8d69b98dc540c5007e1ac7f165d4fd372a7fc5 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Mon, 21 Mar 2016 16:53:51 -0700
Subject: [PATCH 12/37] Cleaning up file layout.

---
 plugin.xml              |  4 +-
 src/android/Mapbox.java | 19 +++++++++
 www/Mapbox.js           |  2 +-
 www/events-mixin.js     | 56 ++++++++++++++++++++++++++
 www/map-instance.js     | 87 +++--------------------------------------
 www/mixin.js            | 26 ++++++++++++
 6 files changed, 111 insertions(+), 83 deletions(-)
 create mode 100644 www/events-mixin.js
 create mode 100644 www/mixin.js

diff --git a/plugin.xml b/plugin.xml
index 37cc5ee..136bec2 100755
--- a/plugin.xml
+++ b/plugin.xml
@@ -24,7 +24,9 @@
     <engine name="cordova-plugman" version=">=4.2.0"/><!-- needed for gradleReference support -->
   </engines>
 
-  <js-module src="www/map-instance.js" name="MapInstance" />
+  <js-module src="www/mixin.js" name="mixin" />
+  <js-module src="www/events-mixin.js" name="events-mixin" />
+  <js-module src="www/map-instance.js" name="map-instance" />
 
   <js-module src="www/Mapbox.js" name="Mapbox">
     <clobbers target="window.Mapbox" />
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index cc6362c..a7736c2 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -38,6 +38,7 @@ public class Mapbox extends CordovaPlugin {
   private static final String MAPBOX_ACCESSTOKEN_RESOURCE_KEY = "mapbox_accesstoken";
 
   private static final String ACTION_CREATE = "create";
+  private static final String ACTION_JUMP_TO = "jumpTo";
   private static final String ACTION_SHOW_USER_LOCATION = "showUserLocation";
   private static final String ACTION_ADD_MARKERS = "addMarkers";
   private static final String ACTION_ADD_MARKER_CALLBACK = "addMarkerCallback";
@@ -95,6 +96,24 @@ public void run() {
       }
     }
 
+    else if (ACTION_JUMP_TO.equals(action)) {
+      final int mapId = args.getInt(0);
+      final MapInstance map = MapInstance.getMap(mapId);
+      final JSONObject options = args.getJSONObject(1);
+
+      cordova.getActivity().runOnUiThread(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            map.jumpTo(options);
+            callbackContext.success();
+          } catch (JSONException e) {
+            callbackContext.error(e.getMessage());
+          }
+        }
+      });
+    }
+
     else if (ACTION_GET_CENTER.equals(action)) {
       final int mapId = args.getInt(0);
       final MapInstance map = MapInstance.getMap(mapId);
diff --git a/www/Mapbox.js b/www/Mapbox.js
index b68f7db..7d947d0 100644
--- a/www/Mapbox.js
+++ b/www/Mapbox.js
@@ -1,4 +1,4 @@
-var MapInstance = require("./MapInstance");
+var MapInstance = require("./map-instance");
 module.exports = {
     Map: MapInstance
 };
diff --git a/www/events-mixin.js b/www/events-mixin.js
new file mode 100644
index 0000000..09f0e32
--- /dev/null
+++ b/www/events-mixin.js
@@ -0,0 +1,56 @@
+var Mixin = require('./mixin'),
+    channel = require("cordova/channel"),
+    channelIds = 0;
+
+module.exports = Mixin({
+    initEvents: function (prefix) {
+        if (!this._channelPrefix) {
+            this._channelPrefix = prefix + "." + (channelIds++);
+        }
+    },
+
+    _prefix: function (type) {
+        return this._channelPrefix + "." + type;
+    },
+
+    _channel: function (type, sticky) {
+        var t = this._prefix(type);
+        if (!this._channels) {
+            this._channels = {};
+        }
+        if (sticky !== undefined) {
+            this._channels[t] = sticky ?
+                channel.createSticky(t) :
+                channel.create(t);
+        }
+        return this._channels[t];
+    },
+
+    createChannel: function (type) {
+        this._channel(type, false);
+    },
+
+    createStickyChannel: function (type) {
+        this._channel(type, true);
+    },
+
+    once: function (type, listener) {
+        var onEvent = function (e) {
+                listener(e);
+                this.off(type, onEvent);
+            };
+        this.on(type, onEvent.bind(this));
+    },
+
+    on: function (type, listener) {
+        this._channel(type).subscribe(listener);
+    },
+
+    off: function (type, listener) {
+        this._channel(type).unsubscribe(listener);
+    },
+
+    fire: function (type, e) {
+        this._channel(type).fire(e);
+    }
+});
diff --git a/www/map-instance.js b/www/map-instance.js
index e4ee50d..9ee406f 100644
--- a/www/map-instance.js
+++ b/www/map-instance.js
@@ -1,84 +1,5 @@
 var exec = require("cordova/exec"),
-    channel = require("cordova/channel"),
-    channelIds = 0;
-
-var Events = Mixin({
-        initEvents: function (prefix) {
-            if (!this._channelPrefix) {
-                this._channelPrefix = prefix + "." + (channelIds++);
-            }
-        },
-
-        _prefix: function (type) {
-            return this._channelPrefix + "." + type;
-        },
-
-        _channel: function (type, sticky) {
-            var t = this._prefix(type);
-            if (!this._channels) {
-                this._channels = {};
-            }
-            if (sticky !== undefined) {
-                this._channels[t] = sticky ?
-                    channel.createSticky(t) :
-                    channel.create(t);
-            }
-            return this._channels[t];
-        },
-
-        createChannel: function (type) {
-            this._channel(type, false);
-        },
-
-        createStickyChannel: function (type) {
-            this._channel(type, true);
-        },
-
-        once: function (type, listener) {
-            var onEvent = function (e) {
-                    listener(e);
-                    this.off(type, onEvent);
-                };
-            this.on(type, onEvent.bind(this));
-        },
-
-        on: function (type, listener) {
-            this._channel(type).subscribe(listener);
-        },
-
-        off: function (type, listener) {
-            this._channel(type).unsubscribe(listener);
-        },
-
-        fire: function (type, e) {
-            this._channel(type).fire(e);
-        }
-    });
-
-function assign(target) {
-    if (target === undefined || target === null) {
-        throw new TypeError('Cannot convert undefined or null to object');
-    }
-
-    var output = Object(target);
-    for (var index = 1; index < arguments.length; index++) {
-        var source = arguments[index];
-        if (source !== undefined && source !== null) {
-            for (var nextKey in source) {
-                if (source.hasOwnProperty(nextKey)) {
-                    output[nextKey] = source[nextKey];
-                }
-            }
-        }
-    }
-    return output;
-}
-
-function Mixin(behaviour) {
-    return function(target) {
-        return assign(target, behaviour);
-    };
-}
+    EventsMixin = require("./events-mixin");
 
 function MapInstance(options) {
     var onLoad = _onLoad.bind(this),
@@ -99,7 +20,7 @@ function MapInstance(options) {
     }
 }
 
-Events(MapInstance.prototype);
+EventsMixin(MapInstance.prototype);
 
 MapInstance.prototype._error = function (err) {
     var error = new Error("Map error (ID: " + this._id + "): " + err);
@@ -119,6 +40,10 @@ MapInstance.prototype._execAfterLoad = function () {
     }.bind(this));
 };
 
+MapInstance.prototype.jumpTo = function (options, successCallback, errorCallback) {
+    this._execAfterLoad(successCallback, errorCallback, "jumpTo", [options]);
+};
+
 MapInstance.prototype.setCenter = function (options, successCallback, errorCallback) {
     this._execAfterLoad(successCallback, errorCallback, "setCenter", [options]);
 };
diff --git a/www/mixin.js b/www/mixin.js
new file mode 100644
index 0000000..f4178e9
--- /dev/null
+++ b/www/mixin.js
@@ -0,0 +1,26 @@
+module.exports = Mixin;
+
+function Mixin(behaviour) {
+    return function(target) {
+        return assign(target, behaviour);
+    };
+}
+
+function assign(target) {
+    if (target === undefined || target === null) {
+        throw new TypeError('Cannot convert undefined or null to object');
+    }
+
+    var output = Object(target);
+    for (var index = 1; index < arguments.length; index++) {
+        var source = arguments[index];
+        if (source !== undefined && source !== null) {
+            for (var nextKey in source) {
+                if (source.hasOwnProperty(nextKey)) {
+                    output[nextKey] = source[nextKey];
+                }
+            }
+        }
+    }
+    return output;
+}

From 864084abff282691ead91c7db20b06b8a8098b98 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Tue, 22 Mar 2016 12:15:14 -0700
Subject: [PATCH 13/37] initial offline pieces

---
 src/android/MapInstance.java | 33 +++++++++++++++++++++++++++++++++
 src/android/Mapbox.java      |  8 ++++++++
 2 files changed, 41 insertions(+)

diff --git a/src/android/MapInstance.java b/src/android/MapInstance.java
index 63070d9..01afec6 100644
--- a/src/android/MapInstance.java
+++ b/src/android/MapInstance.java
@@ -5,10 +5,14 @@
 import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
 import com.mapbox.mapboxsdk.constants.Style;
 import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
 import com.mapbox.mapboxsdk.maps.MapView;
 import com.mapbox.mapboxsdk.maps.MapboxMap;
 import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
 import com.mapbox.mapboxsdk.maps.UiSettings;
+import com.mapbox.mapboxsdk.offline.OfflineManager;
+import com.mapbox.mapboxsdk.offline.OfflineRegion;
+import com.mapbox.mapboxsdk.offline.OfflineRegionDefinition;
 
 import org.json.JSONArray;
 import org.json.JSONException;
@@ -167,6 +171,35 @@ private static String getStyle(final String requested) {
         }
     }
 
+    public void createOfflineRegion(float pixelRatio, JSONObject options) {
+        OfflineManager mOfflineManager = OfflineManager.getInstance(this);
+        mOfflineManager.setAccessToken(ApiAccess.getToken(this));
+
+        // Definition
+        String styleURL = mapboxMap.getStyleUrl();
+        LatLngBounds bounds = mapboxMap.getProjection().getVisibleRegion().latLngBounds;
+        double minZoom = mapView.getZoom();
+        double maxZoom = mapView.getMaxZoom();
+        OfflineRegionDefinition definition = new OfflineRegionDefinition(styleURL, bounds, minZoom, maxZoom, pixelRatio);
+
+        // Your metadata
+        OfflineRegionMetadata metadata =...;
+
+        // Create region
+        mOfflineManager.createOfflineRegion(definition, metadata,
+                new OfflineManager.CreateOfflineRegionCallback() {
+                    @Override
+                    public void onCreate(OfflineRegion offlineRegion) {
+                        Log.d(LOG_TAG, "Offline region created: " + regionName);
+                    }
+
+                    @Override
+                    public void onError(String error) {
+                        Log.e(LOG_TAG, "Error: " + error);
+                    }
+                });
+    }
+
     public void applyOptions(JSONObject options) {
         try {
             if (options.has("style")) {
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index a7736c2..763680a 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -10,8 +10,10 @@
 import android.widget.FrameLayout;
 
 import com.mapbox.mapboxsdk.annotations.Marker;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
 import com.mapbox.mapboxsdk.maps.MapView;
 import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.offline.OfflineManager;
 
 import org.apache.cordova.CallbackContext;
 import org.apache.cordova.CordovaArgs;
@@ -40,6 +42,7 @@ public class Mapbox extends CordovaPlugin {
   private static final String ACTION_CREATE = "create";
   private static final String ACTION_JUMP_TO = "jumpTo";
   private static final String ACTION_SHOW_USER_LOCATION = "showUserLocation";
+  private static final String ACTION_CREATE_OFFLINE_REGION = "createOfflineRegion";
   private static final String ACTION_ADD_MARKERS = "addMarkers";
   private static final String ACTION_ADD_MARKER_CALLBACK = "addMarkerCallback";
   private static final String ACTION_ADD_POLYGON = "addPolygon";
@@ -186,6 +189,11 @@ public boolean onInfoWindowClick(Marker marker) {
         }
       );
     }
+    else if (ACTION_CREATE_OFFLINE_REGION.equals(action)) {
+      final int mapId = args.getInt(0);
+      final JSONObject options = args.getJSONObject(1);
+      final MapInstance map = MapInstance.getMap(mapId);
+    }
     else if (ACTION_GET_TILT.equals(action)) {
 //        if (mapView != null) {
 //          cordova.getActivity().runOnUiThread(new Runnable() {

From 846b174516aaa036b195dba4ae6f058ff9dd38bc Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Tue, 22 Mar 2016 13:10:27 -0700
Subject: [PATCH 14/37] Offline implementation independenant of map instances.

---
 src/android/MapInstance.java | 37 ++------------------
 src/android/Mapbox.java      | 68 +++++++++++++++++++++++++++++++++---
 src/android/mapbox.gradle    |  2 +-
 3 files changed, 67 insertions(+), 40 deletions(-)

diff --git a/src/android/MapInstance.java b/src/android/MapInstance.java
index 01afec6..e37f0c5 100644
--- a/src/android/MapInstance.java
+++ b/src/android/MapInstance.java
@@ -5,14 +5,10 @@
 import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
 import com.mapbox.mapboxsdk.constants.Style;
 import com.mapbox.mapboxsdk.geometry.LatLng;
-import com.mapbox.mapboxsdk.geometry.LatLngBounds;
 import com.mapbox.mapboxsdk.maps.MapView;
 import com.mapbox.mapboxsdk.maps.MapboxMap;
 import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
 import com.mapbox.mapboxsdk.maps.UiSettings;
-import com.mapbox.mapboxsdk.offline.OfflineManager;
-import com.mapbox.mapboxsdk.offline.OfflineRegion;
-import com.mapbox.mapboxsdk.offline.OfflineRegionDefinition;
 
 import org.json.JSONArray;
 import org.json.JSONException;
@@ -22,6 +18,8 @@
 
 public class MapInstance {
 
+    private final static String LOG_TAG = "MapInstance";
+
     public interface MapCreatedCallback {
         void onMapReady(MapInstance map);
     }
@@ -171,36 +169,7 @@ private static String getStyle(final String requested) {
         }
     }
 
-    public void createOfflineRegion(float pixelRatio, JSONObject options) {
-        OfflineManager mOfflineManager = OfflineManager.getInstance(this);
-        mOfflineManager.setAccessToken(ApiAccess.getToken(this));
-
-        // Definition
-        String styleURL = mapboxMap.getStyleUrl();
-        LatLngBounds bounds = mapboxMap.getProjection().getVisibleRegion().latLngBounds;
-        double minZoom = mapView.getZoom();
-        double maxZoom = mapView.getMaxZoom();
-        OfflineRegionDefinition definition = new OfflineRegionDefinition(styleURL, bounds, minZoom, maxZoom, pixelRatio);
-
-        // Your metadata
-        OfflineRegionMetadata metadata =...;
-
-        // Create region
-        mOfflineManager.createOfflineRegion(definition, metadata,
-                new OfflineManager.CreateOfflineRegionCallback() {
-                    @Override
-                    public void onCreate(OfflineRegion offlineRegion) {
-                        Log.d(LOG_TAG, "Offline region created: " + regionName);
-                    }
-
-                    @Override
-                    public void onError(String error) {
-                        Log.e(LOG_TAG, "Error: " + error);
-                    }
-                });
-    }
-
-    public void applyOptions(JSONObject options) {
+    private void applyOptions(JSONObject options) {
         try {
             if (options.has("style")) {
                 this.mapView.setStyleUrl(this.getStyle(options.optString("style")));
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 763680a..ec7271e 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -7,13 +7,18 @@
 import android.os.Build;
 import android.support.v4.app.ActivityCompat;
 import android.util.DisplayMetrics;
+import android.util.Log;
 import android.widget.FrameLayout;
 
 import com.mapbox.mapboxsdk.annotations.Marker;
+import com.mapbox.mapboxsdk.geometry.LatLng;
 import com.mapbox.mapboxsdk.geometry.LatLngBounds;
 import com.mapbox.mapboxsdk.maps.MapView;
 import com.mapbox.mapboxsdk.maps.MapboxMap;
 import com.mapbox.mapboxsdk.offline.OfflineManager;
+import com.mapbox.mapboxsdk.offline.OfflineRegion;
+import com.mapbox.mapboxsdk.offline.OfflineRegionDefinition;
+import com.mapbox.mapboxsdk.offline.OfflineTilePyramidRegionDefinition;
 
 import org.apache.cordova.CallbackContext;
 import org.apache.cordova.CordovaArgs;
@@ -32,6 +37,12 @@
 // TODO look at demo app: https://github.com/mapbox/mapbox-gl-native/blob/master/android/java/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxgl/testapp/MainActivity.java
 public class Mapbox extends CordovaPlugin {
 
+  private static final String LOG_TAG = "MapboxCordovaPlugin";
+
+  // JSON encoding/decoding
+  public static final String JSON_CHARSET = "UTF-8";
+  public static final String JSON_FIELD_REGION_NAME = "FIELD_REGION_NAME";
+
   public static final String FINE_LOCATION = Manifest.permission.ACCESS_FINE_LOCATION;
   public static final String COARSE_LOCATION = Manifest.permission.ACCESS_COARSE_LOCATION;
 
@@ -190,9 +201,8 @@ public boolean onInfoWindowClick(Marker marker) {
       );
     }
     else if (ACTION_CREATE_OFFLINE_REGION.equals(action)) {
-      final int mapId = args.getInt(0);
-      final JSONObject options = args.getJSONObject(1);
-      final MapInstance map = MapInstance.getMap(mapId);
+      final JSONObject options = args.getJSONObject(0);
+      this.createOfflineRegion(options, callbackContext);
     }
     else if (ACTION_GET_TILT.equals(action)) {
 //        if (mapView != null) {
@@ -360,6 +370,53 @@ private MapView createMapView(String accessToken, JSONObject options) {
     return mapView;
   }
 
+  public void createOfflineRegion(JSONObject options, final CallbackContext callbackContext) throws JSONException {
+    String styleURL = options.getString("style");
+
+    final String regionName = options.getString("name");
+    double minZoom = options.getDouble("minZoom");
+    double maxZoom = options.getDouble("maxZoom");
+    JSONObject boundsOptions = options.getJSONObject("bounds");
+    double north = boundsOptions.getDouble("north");
+    double east = boundsOptions.getDouble("east");
+    double south = boundsOptions.getDouble("south");
+    double west = boundsOptions.getDouble("west");
+
+    LatLngBounds bounds = new LatLngBounds.Builder()
+            .include(new LatLng(north, west))
+            .include(new LatLng(south, east))
+            .build();
+    OfflineRegionDefinition definition = new OfflineTilePyramidRegionDefinition(styleURL, bounds, minZoom, maxZoom, this.retinaFactor);
+
+    // Sample way of encoding metadata from a JSONObject
+    final JSONObject metadata = new JSONObject();
+    byte[] encodedMetadata;
+    try {
+      metadata.put(JSON_FIELD_REGION_NAME, regionName);
+      String json = metadata.toString();
+      encodedMetadata = json.getBytes(JSON_CHARSET);
+    } catch (Exception e) {
+      Log.e(LOG_TAG, "Failed to encode metadata: " + e.getMessage());
+      encodedMetadata = null;
+    }
+
+    OfflineManager offlineManager = OfflineManager.getInstance(this.webView.getContext());
+    offlineManager.setAccessToken(this.accessToken);
+    offlineManager.createOfflineRegion(definition, encodedMetadata, new OfflineManager.CreateOfflineRegionCallback() {
+      @Override
+      public void onCreate(OfflineRegion offlineRegion) {
+        Log.d(LOG_TAG, "Offline region created: " + regionName);
+        callbackContext.success(metadata);
+      }
+
+      @Override
+      public void onError(String error) {
+        Log.e(LOG_TAG, "Error: " + error);
+        callbackContext.error(error);
+      }
+    });
+  }
+
   private boolean permissionGranted(String... types) {
     if (Build.VERSION.SDK_INT < 23) {
       return true;
@@ -407,8 +464,9 @@ public void onRequestPermissionResult(int commandId, String[] permissions, int[]
 //  }
 
   private float getRetinaFactor() {
-    DisplayMetrics metrics = new DisplayMetrics();
-    this.cordova.getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
+    Activity activity = this.cordova.getActivity();
+    Resources res = activity.getResources();
+    DisplayMetrics metrics = res.getDisplayMetrics();
     return metrics.density;
   }
 
diff --git a/src/android/mapbox.gradle b/src/android/mapbox.gradle
index 4bbe68b..57aca98 100644
--- a/src/android/mapbox.gradle
+++ b/src/android/mapbox.gradle
@@ -6,7 +6,7 @@ repositories {
 
 dependencies {
     compile 'com.android.support:appcompat-v7:23.0.1'
-    compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:4.0.0-beta.1@aar'){
+    compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:4.0.0-beta.2@aar'){
         transitive=true
     }
 }

From 3dd2ad6f4ce6afc341a5c1d6870b631d24608665 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Tue, 22 Mar 2016 16:44:53 -0700
Subject: [PATCH 15/37] Offline regions.

---
 plugin.xml                                 |   4 +-
 src/android/{MapInstance.java => Map.java} | 103 ++++++--------
 src/android/Mapbox.java                    | 140 ++++++++++--------
 src/android/OfflineRegion.java             | 156 +++++++++++++++++++++
 www/Mapbox.js                              |   7 +-
 www/offline-region.js                      |  99 +++++++++++++
 6 files changed, 387 insertions(+), 122 deletions(-)
 rename src/android/{MapInstance.java => Map.java} (57%)
 create mode 100644 src/android/OfflineRegion.java
 create mode 100644 www/offline-region.js

diff --git a/plugin.xml b/plugin.xml
index 136bec2..91ab77a 100755
--- a/plugin.xml
+++ b/plugin.xml
@@ -27,6 +27,7 @@
   <js-module src="www/mixin.js" name="mixin" />
   <js-module src="www/events-mixin.js" name="events-mixin" />
   <js-module src="www/map-instance.js" name="map-instance" />
+  <js-module src="www/offline-region.js" name="offline-region" />
 
   <js-module src="www/Mapbox.js" name="Mapbox">
     <clobbers target="window.Mapbox" />
@@ -56,7 +57,8 @@
 
     <framework src="src/android/mapbox.gradle" custom="true" type="gradleReference"/>
     <source-file src="src/android/Mapbox.java" target-dir="src/com/telerik/plugins/mapbox"/>
-    <source-file src="src/android/MapInstance.java" target-dir="src/com/telerik/plugins/mapbox"/>
+    <source-file src="src/android/OfflineRegion.java" target-dir="src/com/telerik/plugins/mapbox"/>
+    <source-file src="src/android/Map.java" target-dir="src/com/telerik/plugins/mapbox"/>
 
     <!-- This leads to trouble in AppBuilder when compiling for Cordova-Android 4 -->
     <!--source-file src="src/android/res/values/mapboxstrings.xml" target-dir="res/values" />
diff --git a/src/android/MapInstance.java b/src/android/Map.java
similarity index 57%
rename from src/android/MapInstance.java
rename to src/android/Map.java
index e37f0c5..a606857 100644
--- a/src/android/MapInstance.java
+++ b/src/android/Map.java
@@ -3,7 +3,6 @@
 import com.mapbox.mapboxsdk.annotations.MarkerOptions;
 import com.mapbox.mapboxsdk.camera.CameraPosition;
 import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
-import com.mapbox.mapboxsdk.constants.Style;
 import com.mapbox.mapboxsdk.geometry.LatLng;
 import com.mapbox.mapboxsdk.maps.MapView;
 import com.mapbox.mapboxsdk.maps.MapboxMap;
@@ -16,27 +15,28 @@
 
 import java.util.HashMap;
 
-public class MapInstance {
+public class Map {
+    private static HashMap<Integer, Map> maps = new HashMap<Integer, Map>();
 
-    private final static String LOG_TAG = "MapInstance";
+    private static int ids = 0;
 
     public interface MapCreatedCallback {
-        void onMapReady(MapInstance map);
+        void onCreate(Map map);
+        void onError(String error);
     }
 
-    public static MapInstance createMap(MapView mapView, JSONObject options, MapCreatedCallback callback) {
-        MapInstance map = new MapInstance(mapView, options, callback);
+    public static void create(MapView mapView, JSONObject options, MapCreatedCallback callback) {
+        Map map = new Map(mapView, options, callback);
         maps.put(map.getId(), map);
-        return map;
     }
 
-    public static MapInstance getMap(int id) {
+    public static Map getMap(int id) {
         return maps.get(id);
     }
 
-    private static HashMap<Integer, MapInstance> maps = new HashMap<Integer, MapInstance>();
-
-    private static int ids = 0;
+    public static void removeMap(int id) {
+        maps.remove(id);
+    }
 
     private int id;
 
@@ -46,7 +46,7 @@ public static MapInstance getMap(int id) {
 
     private MapCreatedCallback constructorCallback;
 
-    private MapInstance(final MapView mapView, final JSONObject options, final MapCreatedCallback callback) {
+    private Map(final MapView mapView, final JSONObject options, final MapCreatedCallback callback) {
         this.id = this.ids++;
         this.constructorCallback = callback;
         this.mapView = mapView;
@@ -55,8 +55,13 @@ private MapInstance(final MapView mapView, final JSONObject options, final MapCr
             @Override
             public void onMapReady(MapboxMap mMap) {
                 mapboxMap = mMap;
-                applyOptions(options);
-                constructorCallback.onMapReady(MapInstance.this);
+                try {
+                    applyOptions(options);
+                    constructorCallback.onCreate(Map.this);
+                } catch (JSONException e) {
+                    Map.removeMap(getId());
+                    constructorCallback.onError(e.getMessage());
+                }
             }
         });
     }
@@ -150,58 +155,34 @@ public void showUserLocation(boolean enabled) {
         mapboxMap.setMyLocationEnabled(enabled);
     }
 
-    private static String getStyle(final String requested) {
-        if ("light".equalsIgnoreCase(requested)) {
-            return Style.LIGHT;
-        } else if ("dark".equalsIgnoreCase(requested)) {
-            return Style.DARK;
-        } else if ("emerald".equalsIgnoreCase(requested)) {
-            return Style.EMERALD;
-        } else if ("satellite".equalsIgnoreCase(requested)) {
-            return Style.SATELLITE;
-        // TODO not currently supported on Android
-        //} else if ("hybrid".equalsIgnoreCase(requested)) {
-        //    return Style.HYBRID;
-        } else if ("streets".equalsIgnoreCase(requested)) {
-            return Style.MAPBOX_STREETS;
-        } else {
-            return requested;
+    private void applyOptions(JSONObject options) throws JSONException {
+        if (options.has("style")) {
+            this.mapView.setStyleUrl(Mapbox.getStyle(options.optString("style")));
         }
-    }
-
-    private void applyOptions(JSONObject options) {
-        try {
-            if (options.has("style")) {
-                this.mapView.setStyleUrl(this.getStyle(options.optString("style")));
-            }
-
-            if (!options.isNull("showUserLocation")) {
-                this.showUserLocation(options.getBoolean("showUserLocation"));
-            }
 
-            UiSettings uiSettings = mapboxMap.getUiSettings();
-            uiSettings.setCompassEnabled(options.isNull("hideCompass") || !options.getBoolean("hideCompass"));
-            uiSettings.setRotateGesturesEnabled(options.isNull("disableRotation") || !options.getBoolean("disableRotation"));
-            uiSettings.setScrollGesturesEnabled(options.isNull("disableScroll") || !options.getBoolean("disableScroll"));
-            uiSettings.setZoomGesturesEnabled(options.isNull("disableZoom") || !options.getBoolean("disableZoom"));
-            uiSettings.setTiltGesturesEnabled(options.isNull("disableTilt") || !options.getBoolean("disableTilt"));
-
-            if (!options.isNull("hideAttribution") && options.getBoolean("hideAttribution")) {
-                uiSettings.setAttributionMargins(-300, 0, 0, 0);
-            }
+        if (!options.isNull("showUserLocation")) {
+            this.showUserLocation(options.getBoolean("showUserLocation"));
+        }
 
-            if (!options.isNull("hideLogo") && options.getBoolean("hideLogo")) {
-                uiSettings.setLogoMargins(-300, 0, 0, 0);
-            }
+        UiSettings uiSettings = mapboxMap.getUiSettings();
+        uiSettings.setCompassEnabled(options.isNull("hideCompass") || !options.getBoolean("hideCompass"));
+        uiSettings.setRotateGesturesEnabled(options.isNull("disableRotation") || !options.getBoolean("disableRotation"));
+        uiSettings.setScrollGesturesEnabled(options.isNull("disableScroll") || !options.getBoolean("disableScroll"));
+        uiSettings.setZoomGesturesEnabled(options.isNull("disableZoom") || !options.getBoolean("disableZoom"));
+        uiSettings.setTiltGesturesEnabled(options.isNull("disableTilt") || !options.getBoolean("disableTilt"));
 
-            if (options.has("markers")) {
-                this.addMarkers(options.getJSONArray("markers"));
-            }
+        if (!options.isNull("hideAttribution") && options.getBoolean("hideAttribution")) {
+            uiSettings.setAttributionMargins(-300, 0, 0, 0);
+        }
 
-            this.jumpTo(options);
+        if (!options.isNull("hideLogo") && options.getBoolean("hideLogo")) {
+            uiSettings.setLogoMargins(-300, 0, 0, 0);
         }
-        catch (JSONException e) {
-            e.printStackTrace();
+
+        if (options.has("markers")) {
+            this.addMarkers(options.getJSONArray("markers"));
         }
+
+        this.jumpTo(options);
     }
-}
\ No newline at end of file
+}
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index ec7271e..ca15f83 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -11,14 +11,10 @@
 import android.widget.FrameLayout;
 
 import com.mapbox.mapboxsdk.annotations.Marker;
-import com.mapbox.mapboxsdk.geometry.LatLng;
-import com.mapbox.mapboxsdk.geometry.LatLngBounds;
+import com.mapbox.mapboxsdk.constants.Style;
 import com.mapbox.mapboxsdk.maps.MapView;
 import com.mapbox.mapboxsdk.maps.MapboxMap;
 import com.mapbox.mapboxsdk.offline.OfflineManager;
-import com.mapbox.mapboxsdk.offline.OfflineRegion;
-import com.mapbox.mapboxsdk.offline.OfflineRegionDefinition;
-import com.mapbox.mapboxsdk.offline.OfflineTilePyramidRegionDefinition;
 
 import org.apache.cordova.CallbackContext;
 import org.apache.cordova.CordovaArgs;
@@ -26,6 +22,7 @@
 import org.apache.cordova.CordovaPlugin;
 import org.apache.cordova.CordovaWebView;
 
+import org.apache.cordova.PluginResult;
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -39,10 +36,6 @@ public class Mapbox extends CordovaPlugin {
 
   private static final String LOG_TAG = "MapboxCordovaPlugin";
 
-  // JSON encoding/decoding
-  public static final String JSON_CHARSET = "UTF-8";
-  public static final String JSON_FIELD_REGION_NAME = "FIELD_REGION_NAME";
-
   public static final String FINE_LOCATION = Manifest.permission.ACCESS_FINE_LOCATION;
   public static final String COARSE_LOCATION = Manifest.permission.ACCESS_COARSE_LOCATION;
 
@@ -54,6 +47,8 @@ public class Mapbox extends CordovaPlugin {
   private static final String ACTION_JUMP_TO = "jumpTo";
   private static final String ACTION_SHOW_USER_LOCATION = "showUserLocation";
   private static final String ACTION_CREATE_OFFLINE_REGION = "createOfflineRegion";
+  private static final String ACTION_DOWNLOAD_OFFLINE_REGION = "downloadOfflineRegion";
+  private static final String ACTION_PAUSE_OFFLINE_REGION = "pauseOfflineRegion";
   private static final String ACTION_ADD_MARKERS = "addMarkers";
   private static final String ACTION_ADD_MARKER_CALLBACK = "addMarkerCallback";
   private static final String ACTION_ADD_POLYGON = "addPolygon";
@@ -98,7 +93,7 @@ public boolean execute(Command command) throws JSONException {
 
     else if (ACTION_SHOW_USER_LOCATION.equals(action)) {
       final int mapId = args.getInt(0);
-      final MapInstance map = MapInstance.getMap(mapId);
+      final Map map = Map.getMap(mapId);
       final boolean enabled = args.getBoolean(1);
       if (requestPermission(command, COARSE_LOCATION, FINE_LOCATION)) {
         cordova.getActivity().runOnUiThread(new Runnable() {
@@ -112,7 +107,7 @@ public void run() {
 
     else if (ACTION_JUMP_TO.equals(action)) {
       final int mapId = args.getInt(0);
-      final MapInstance map = MapInstance.getMap(mapId);
+      final Map map = Map.getMap(mapId);
       final JSONObject options = args.getJSONObject(1);
 
       cordova.getActivity().runOnUiThread(new Runnable() {
@@ -130,7 +125,7 @@ public void run() {
 
     else if (ACTION_GET_CENTER.equals(action)) {
       final int mapId = args.getInt(0);
-      final MapInstance map = MapInstance.getMap(mapId);
+      final Map map = Map.getMap(mapId);
       try {
         callbackContext.success(map.getCenter());
       } catch (JSONException e) {
@@ -140,7 +135,7 @@ else if (ACTION_GET_CENTER.equals(action)) {
 
     else if (ACTION_SET_CENTER.equals(action)) {
       final int mapId = args.getInt(0);
-      final MapInstance map = MapInstance.getMap(mapId);
+      final Map map = Map.getMap(mapId);
       final JSONArray center = args.getJSONArray(1);
       try {
         map.setCenter(center);
@@ -152,13 +147,13 @@ else if (ACTION_SET_CENTER.equals(action)) {
 
     else if (ACTION_GET_ZOOMLEVEL.equals(action)) {
       final int mapId = args.getInt(0);
-      final MapInstance map = MapInstance.getMap(mapId);
+      final Map map = Map.getMap(mapId);
       callbackContext.success("" + map.getZoom());
     }
 
     else if (ACTION_SET_ZOOMLEVEL.equals(action)) {
       final int mapId = args.getInt(0);
-      final MapInstance map = MapInstance.getMap(mapId);
+      final Map map = Map.getMap(mapId);
       final double zoom = args.getDouble(1);
       map.setZoom(zoom);
       callbackContext.success();
@@ -166,7 +161,7 @@ else if (ACTION_SET_ZOOMLEVEL.equals(action)) {
 
     else if (ACTION_ADD_MARKERS.equals(action)) {
       final int mapId = args.getInt(0);
-      final MapInstance map = MapInstance.getMap(mapId);
+      final Map map = Map.getMap(mapId);
       try {
         map.addMarkers(args.getJSONArray(1));
         callbackContext.success();
@@ -177,7 +172,7 @@ else if (ACTION_ADD_MARKERS.equals(action)) {
 
     else if (ACTION_ADD_MARKER_CALLBACK.equals(action)) {
       final int mapId = args.getInt(0);
-      final MapInstance map = MapInstance.getMap(mapId);
+      final Map map = Map.getMap(mapId);
       map.addMarkerListener(
         new MapboxMap.OnInfoWindowClickListener() {
           @Override
@@ -200,10 +195,24 @@ public boolean onInfoWindowClick(Marker marker) {
         }
       );
     }
+
     else if (ACTION_CREATE_OFFLINE_REGION.equals(action)) {
       final JSONObject options = args.getJSONObject(0);
-      this.createOfflineRegion(options, callbackContext);
+      final String progressCallbackId = args.getString(1);
+      final CallbackContext onProgress = new CallbackContext(progressCallbackId, this.webView);
+      this.createOfflineRegion(options, callbackContext, onProgress);
+    }
+
+    else if (ACTION_DOWNLOAD_OFFLINE_REGION.equals(action)) {
+      final int offlineRegionId = args.getInt(0);
+      final OfflineRegion region = OfflineRegion.getOfflineRegion(offlineRegionId);
+      region.download();
     }
+
+    else if (ACTION_PAUSE_OFFLINE_REGION.equals(action)) {
+
+    }
+
     else if (ACTION_GET_TILT.equals(action)) {
 //        if (mapView != null) {
 //          cordova.getActivity().runOnUiThread(new Runnable() {
@@ -321,20 +330,28 @@ private void create(final JSONObject options, final CallbackContext callback) {
       @Override
       public void run() {
         MapView mapView = createMapView(accessToken, options);
-        MapInstance.createMap(mapView, options, new MapInstance.MapCreatedCallback() {
+        Map.create(mapView, options, new Map.MapCreatedCallback() {
           @Override
-          public void onMapReady(final MapInstance map) {
+          public void onCreate(final Map map) {
             JSONObject resp = new JSONObject();
             try {
               resp.put("id", map.getId());
               callback.success(resp);
               return;
             } catch (JSONException e) {
-              e.printStackTrace();
-              callback.error("Failed to create map.");
+              String error = "Failed to create map: " + e.getMessage();
+              Log.e(LOG_TAG, error);
+              callback.error(error);
               return;
             }
           }
+
+          @Override
+          public void onError(String error) {
+            String message = "Failed to create map: " + error;
+            Log.e(LOG_TAG, message);
+            callback.error(message);
+          }
         });
       }
     });
@@ -370,49 +387,37 @@ private MapView createMapView(String accessToken, JSONObject options) {
     return mapView;
   }
 
-  public void createOfflineRegion(JSONObject options, final CallbackContext callbackContext) throws JSONException {
-    String styleURL = options.getString("style");
-
-    final String regionName = options.getString("name");
-    double minZoom = options.getDouble("minZoom");
-    double maxZoom = options.getDouble("maxZoom");
-    JSONObject boundsOptions = options.getJSONObject("bounds");
-    double north = boundsOptions.getDouble("north");
-    double east = boundsOptions.getDouble("east");
-    double south = boundsOptions.getDouble("south");
-    double west = boundsOptions.getDouble("west");
-
-    LatLngBounds bounds = new LatLngBounds.Builder()
-            .include(new LatLng(north, west))
-            .include(new LatLng(south, east))
-            .build();
-    OfflineRegionDefinition definition = new OfflineTilePyramidRegionDefinition(styleURL, bounds, minZoom, maxZoom, this.retinaFactor);
-
-    // Sample way of encoding metadata from a JSONObject
-    final JSONObject metadata = new JSONObject();
-    byte[] encodedMetadata;
-    try {
-      metadata.put(JSON_FIELD_REGION_NAME, regionName);
-      String json = metadata.toString();
-      encodedMetadata = json.getBytes(JSON_CHARSET);
-    } catch (Exception e) {
-      Log.e(LOG_TAG, "Failed to encode metadata: " + e.getMessage());
-      encodedMetadata = null;
-    }
-
+  public void createOfflineRegion(JSONObject options, final CallbackContext callback, final CallbackContext onProgress) throws JSONException {
     OfflineManager offlineManager = OfflineManager.getInstance(this.webView.getContext());
     offlineManager.setAccessToken(this.accessToken);
-    offlineManager.createOfflineRegion(definition, encodedMetadata, new OfflineManager.CreateOfflineRegionCallback() {
+    OfflineRegion.create(offlineManager, this.retinaFactor, options, new OfflineRegion.OfflineRegionCreatedCallback() {
       @Override
-      public void onCreate(OfflineRegion offlineRegion) {
-        Log.d(LOG_TAG, "Offline region created: " + regionName);
-        callbackContext.success(metadata);
+      public void onCreate(OfflineRegion region) {
+        JSONObject resp = new JSONObject();
+        try {
+          resp.put("id", region.getId());
+          callback.success(resp);
+          return;
+        } catch (JSONException e) {
+          String error = "Failed to create offline region: " + e.getMessage();
+          Log.e(LOG_TAG, error);
+          callback.error(error);
+          return;
+        }
+      }
+
+      @Override
+      public void onProgress(JSONObject progress) {
+        PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
+        result.setKeepCallback(true);
+        onProgress.sendPluginResult(result);
       }
 
       @Override
       public void onError(String error) {
-        Log.e(LOG_TAG, "Error: " + error);
-        callbackContext.error(error);
+        String message = "Failed to create offline region: " + error;
+        Log.e(LOG_TAG, message);
+        callback.error(message);
       }
     });
   }
@@ -489,6 +494,25 @@ private String getAccessToken() {
     return accessToken;
   }
 
+  public static String getStyle(final String requested) {
+    if ("light".equalsIgnoreCase(requested)) {
+      return Style.LIGHT;
+    } else if ("dark".equalsIgnoreCase(requested)) {
+      return Style.DARK;
+    } else if ("emerald".equalsIgnoreCase(requested)) {
+      return Style.EMERALD;
+    } else if ("satellite".equalsIgnoreCase(requested)) {
+      return Style.SATELLITE;
+      // TODO not currently supported on Android
+      //} else if ("hybrid".equalsIgnoreCase(requested)) {
+      //    return Style.HYBRID;
+    } else if ("streets".equalsIgnoreCase(requested)) {
+      return Style.MAPBOX_STREETS;
+    } else {
+      return requested;
+    }
+  }
+
 }
 
 class Command {
diff --git a/src/android/OfflineRegion.java b/src/android/OfflineRegion.java
new file mode 100644
index 0000000..2a1ad6e
--- /dev/null
+++ b/src/android/OfflineRegion.java
@@ -0,0 +1,156 @@
+package com.telerik.plugins.mapbox;
+
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
+import com.mapbox.mapboxsdk.offline.OfflineManager;
+import com.mapbox.mapboxsdk.offline.OfflineRegionDefinition;
+import com.mapbox.mapboxsdk.offline.OfflineRegionError;
+import com.mapbox.mapboxsdk.offline.OfflineRegionStatus;
+import com.mapbox.mapboxsdk.offline.OfflineTilePyramidRegionDefinition;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+
+public class OfflineRegion {
+    // JSON encoding/decoding
+    public static final String JSON_CHARSET = "UTF-8";
+    public static final String JSON_FIELD_REGION_NAME = "FIELD_REGION_NAME";
+
+    private static HashMap<Integer, OfflineRegion> regions = new HashMap<Integer, OfflineRegion>();
+
+    private static HashMap<Long, com.mapbox.mapboxsdk.offline.OfflineRegion> mapboxRegions = new HashMap<Long, com.mapbox.mapboxsdk.offline.OfflineRegion>();
+
+    private static int ids = 0;
+
+    public interface OfflineRegionCreatedCallback {
+        void onCreate(OfflineRegion region);
+        void onProgress(JSONObject progress);
+        void onError(String error);
+    }
+
+    public static void create(OfflineManager offlineManager, float retinaFactor, JSONObject options, OfflineRegionCreatedCallback callback) {
+        OfflineRegion region = new OfflineRegion(offlineManager, retinaFactor, options, callback);
+        regions.put(region.getId(), region);
+    }
+
+    public static OfflineRegion getOfflineRegion(int id) {
+        return regions.get(id);
+    }
+
+    public static void removeOfflineRegion(int id) {
+        regions.remove(id);
+    }
+
+    private int id;
+
+    private long mapboxOfflineRegionId;
+
+    private OfflineRegionCreatedCallback constructorCallback;
+
+    private String regionName;
+
+    private OfflineRegion(final OfflineManager offlineManager, final float retinaFactor, final JSONObject options, final OfflineRegionCreatedCallback callback) {
+        this.id = this.ids++;
+        this.constructorCallback = callback;
+
+        try {
+            this.regionName = options.getString("name");
+
+            OfflineRegionDefinition definition = this.createOfflineRegionDefinition(retinaFactor, options);
+            byte[] encodedMetadata =  this.getMetadata().toString().getBytes(JSON_CHARSET);
+
+            offlineManager.createOfflineRegion(definition, encodedMetadata, new OfflineManager.CreateOfflineRegionCallback() {
+                @Override
+                public void onCreate(com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion) {
+                    offlineRegion.setObserver(new OfflineRegionObserver(constructorCallback));
+                    mapboxOfflineRegionId = offlineRegion.getID();
+                    mapboxRegions.put(mapboxOfflineRegionId, offlineRegion);
+                    constructorCallback.onCreate(OfflineRegion.this);
+                }
+
+                @Override
+                public void onError(String error) {
+                    constructorCallback.onError(error);
+                    OfflineRegion.removeOfflineRegion(getId());
+                }
+            });
+        } catch (JSONException e) {
+            constructorCallback.onError(e.getMessage());
+        } catch (UnsupportedEncodingException e) {
+            constructorCallback.onError(e.getMessage());
+        } finally {
+            OfflineRegion.removeOfflineRegion(getId());
+        }
+    }
+
+    public int getId() {
+        return this.id;
+    }
+
+    public JSONObject getMetadata() throws JSONException {
+        JSONObject metadata = new JSONObject();
+        metadata.put(JSON_FIELD_REGION_NAME, this.regionName);
+        return metadata;
+    }
+
+    public void download() {
+        mapboxRegions.get(this.mapboxOfflineRegionId).setDownloadState(com.mapbox.mapboxsdk.offline.OfflineRegion.STATE_ACTIVE);
+    }
+
+    public void pause() {
+        mapboxRegions.get(this.mapboxOfflineRegionId).setDownloadState(com.mapbox.mapboxsdk.offline.OfflineRegion.STATE_INACTIVE);
+    }
+
+    private OfflineRegionDefinition createOfflineRegionDefinition(float retinaFactor, JSONObject options) throws JSONException {
+        String styleURL = Mapbox.getStyle(options.getString("style"));
+        double minZoom = options.getDouble("minZoom");
+        double maxZoom = options.getDouble("maxZoom");
+        JSONObject boundsOptions = options.getJSONObject("bounds");
+        double north = boundsOptions.getDouble("north");
+        double east = boundsOptions.getDouble("east");
+        double south = boundsOptions.getDouble("south");
+        double west = boundsOptions.getDouble("west");
+
+        LatLngBounds bounds = new LatLngBounds.Builder()
+                .include(new LatLng(north, west))
+                .include(new LatLng(south, east))
+                .build();
+
+        return new OfflineTilePyramidRegionDefinition(styleURL, bounds, minZoom, maxZoom, retinaFactor);
+    }
+
+    private class OfflineRegionObserver implements com.mapbox.mapboxsdk.offline.OfflineRegion.OfflineRegionObserver {
+        OfflineRegionCreatedCallback constructorCallback;
+
+        OfflineRegionObserver(OfflineRegionCreatedCallback callback) {
+            this.constructorCallback = callback;
+        }
+
+        @Override
+        public void onStatusChanged(OfflineRegionStatus status) {
+            try {
+                JSONObject progress = new JSONObject();
+                progress.put("completedCount", status.getCompletedResourceCount());
+                progress.put("completedSize", status.getCompletedResourceSize());
+                progress.put("requiredCount", status.getRequiredResourceCount());
+                constructorCallback.onProgress(progress);
+            } catch (JSONException e) {
+                constructorCallback.onError(e.getMessage());
+            }
+        }
+
+        @Override
+        public void onError(OfflineRegionError error) {
+            constructorCallback.onError(error.getMessage());
+        }
+
+        @Override
+        public void mapboxTileCountLimitExceeded(long limit) {
+            constructorCallback.onError("Tile limit exceeded (limit: " + limit + ")");
+        }
+    }
+
+}
diff --git a/www/Mapbox.js b/www/Mapbox.js
index 7d947d0..3c72836 100644
--- a/www/Mapbox.js
+++ b/www/Mapbox.js
@@ -1,4 +1,7 @@
-var MapInstance = require("./map-instance");
+var MapInstance = require("./map-instance"),
+    OfflineRegion = require("./offline-region");
+
 module.exports = {
-    Map: MapInstance
+    Map: MapInstance,
+    OfflineRegion: OfflineRegion
 };
diff --git a/www/offline-region.js b/www/offline-region.js
new file mode 100644
index 0000000..1442cc3
--- /dev/null
+++ b/www/offline-region.js
@@ -0,0 +1,99 @@
+var cordova = require("cordova"),
+    exec = require("cordova/exec"),
+    EventsMixin = require("./events-mixin");
+
+function OfflineRegion(options) {
+    var onLoad = _onLoad.bind(this),
+        onProgress = _onProgress.bind(this),
+        onError = this._error.bind(this),
+        onProgressId = this._registerCallback('onProgress', onProgress);
+
+    this._error = onError;
+    this._downloaded = false;
+    this._downloading = false;
+
+    this.initEvents("Mapbox.MapInstance");
+    this.createStickyChannel("load");
+    this.createStickyChannel("complete");
+    this.createStickyChannel("error");
+    this.createChannel("progress");
+
+    if (options.progress) {
+        var progress = options.progress;
+        delete options.progress;
+        this.on("progress", progress);
+    }
+
+    exec(onLoad, this._error, "Mapbox", "createOfflineRegion", [options, onProgressId]);
+
+    function _onLoad(resp) {
+        this._id = resp.id;
+        this.loaded = true;
+
+        this.fire("load", {map: this});
+    }
+
+    function _onProgress(progress) {
+        this.fire("progress", progress);
+    }
+}
+
+EventsMixin(OfflineRegion.prototype);
+
+OfflineRegion.prototype._error = function (err) {
+    var error = new Error("OfflineRegion error (ID: " + this._id + "): " + err);
+    console.warn("throwing OfflineRegionError: ", error);
+    throw error;
+};
+
+OfflineRegion.prototype._exec = function (successCallback, errorCallback, method, args) {
+    args = [this._id].concat(args || []);
+    exec(successCallback, errorCallback, "Mapbox", method, args);
+};
+
+OfflineRegion.prototype._execAfterLoad = function () {
+    var args = arguments;
+    this.once('load', function (map) {
+        this._exec.apply(this, args);
+    }.bind(this));
+};
+
+OfflineRegion.prototype._registerCallback = function (name, success, fail) {
+    var callbackId = "MapboxOfflineRegion" + name + cordova.callbackId++;
+
+    console.log("_registerCallback(): " + callbackId);
+
+    success = success ||  function () { console.log(callbackId + "() success!", arguments); };
+    fail = fail ||  function () { console.log(callbackId + "() fail :(", arguments); };
+
+    cordova.callbacks[callbackId] = {success: success, fail: fail};
+    return callbackId;
+};
+
+OfflineRegion.prototype.download = function () {
+    this._downloading = true;
+
+    this._execAfterLoad(onSuccess, onError, "downloadOfflineRegion");
+
+    function onSuccess(resp) {
+        this._downloading = false;
+        this._downloaded = true;
+        this.fire("complete", resp);
+    }
+
+    function onError(error) {
+        this._downloading = false;
+        this._downloaded = false;
+        try {
+            this._error(error);
+        } catch (e) {
+            this.fire("error", e);
+        }
+    }
+};
+
+OfflineRegion.prototype.pause = function () {
+    this._execAfterLoad(successCallback, errorCallback, "pauseOfflineRegion");
+};
+
+module.exports = OfflineRegion;

From 9b003e8333455c6c7b4fac87bf6ba717e38b90f0 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Tue, 22 Mar 2016 18:09:00 -0700
Subject: [PATCH 16/37] Added some todo notes.

---
 src/android/Mapbox.java | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index ca15f83..a4b6002 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -207,10 +207,14 @@ else if (ACTION_DOWNLOAD_OFFLINE_REGION.equals(action)) {
       final int offlineRegionId = args.getInt(0);
       final OfflineRegion region = OfflineRegion.getOfflineRegion(offlineRegionId);
       region.download();
+      // TODO: Need to fire callbackContext.success() upon completion and callbackContext.error() upon error.
     }
 
     else if (ACTION_PAUSE_OFFLINE_REGION.equals(action)) {
-
+      final int offlineRegionId = args.getInt(0);
+      final OfflineRegion region = OfflineRegion.getOfflineRegion(offlineRegionId);
+      region.pause();
+      // TODO: Need to fire callbackContext.success() upon completion and callbackContext.error() upon error.
     }
 
     else if (ACTION_GET_TILT.equals(action)) {

From 77ec66391cb35fed651a3527863deb20eede6dce Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Wed, 23 Mar 2016 11:05:50 -0700
Subject: [PATCH 17/37] Fixing up complete and error callbacks

---
 src/android/Mapbox.java        | 14 +++++---
 src/android/OfflineRegion.java | 14 ++++++--
 www/offline-region.js          | 64 +++++++++++++++++++---------------
 3 files changed, 58 insertions(+), 34 deletions(-)

diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index a4b6002..04fa223 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -198,9 +198,9 @@ public boolean onInfoWindowClick(Marker marker) {
 
     else if (ACTION_CREATE_OFFLINE_REGION.equals(action)) {
       final JSONObject options = args.getJSONObject(0);
-      final String progressCallbackId = args.getString(1);
-      final CallbackContext onProgress = new CallbackContext(progressCallbackId, this.webView);
-      this.createOfflineRegion(options, callbackContext, onProgress);
+      final CallbackContext onProgress = new CallbackContext(args.getString(1), this.webView);
+      final CallbackContext onComplete = new CallbackContext(args.getString(2), this.webView);
+      this.createOfflineRegion(options, callbackContext, onProgress, onComplete);
     }
 
     else if (ACTION_DOWNLOAD_OFFLINE_REGION.equals(action)) {
@@ -391,7 +391,7 @@ private MapView createMapView(String accessToken, JSONObject options) {
     return mapView;
   }
 
-  public void createOfflineRegion(JSONObject options, final CallbackContext callback, final CallbackContext onProgress) throws JSONException {
+  public void createOfflineRegion(JSONObject options, final CallbackContext callback, final CallbackContext onProgress, final CallbackContext onComplete) throws JSONException {
     OfflineManager offlineManager = OfflineManager.getInstance(this.webView.getContext());
     offlineManager.setAccessToken(this.accessToken);
     OfflineRegion.create(offlineManager, this.retinaFactor, options, new OfflineRegion.OfflineRegionCreatedCallback() {
@@ -417,11 +417,17 @@ public void onProgress(JSONObject progress) {
         onProgress.sendPluginResult(result);
       }
 
+      @Override
+      public void onComplete(JSONObject progress) {
+        onComplete.success(progress);
+      }
+
       @Override
       public void onError(String error) {
         String message = "Failed to create offline region: " + error;
         Log.e(LOG_TAG, message);
         callback.error(message);
+        onComplete.error(message);
       }
     });
   }
diff --git a/src/android/OfflineRegion.java b/src/android/OfflineRegion.java
index 2a1ad6e..4d3061a 100644
--- a/src/android/OfflineRegion.java
+++ b/src/android/OfflineRegion.java
@@ -27,6 +27,7 @@ public class OfflineRegion {
 
     public interface OfflineRegionCreatedCallback {
         void onCreate(OfflineRegion region);
+        void onComplete(JSONObject progress);
         void onProgress(JSONObject progress);
         void onError(String error);
     }
@@ -131,14 +132,23 @@ private class OfflineRegionObserver implements com.mapbox.mapboxsdk.offline.Offl
 
         @Override
         public void onStatusChanged(OfflineRegionStatus status) {
+            long completedCount = status.getCompletedResourceCount();
+            long requiredCount = status.getRequiredResourceCount();
+            JSONObject progress = new JSONObject();
+
             try {
-                JSONObject progress = new JSONObject();
                 progress.put("completedCount", status.getCompletedResourceCount());
                 progress.put("completedSize", status.getCompletedResourceSize());
                 progress.put("requiredCount", status.getRequiredResourceCount());
-                constructorCallback.onProgress(progress);
             } catch (JSONException e) {
                 constructorCallback.onError(e.getMessage());
+                return;
+            }
+
+            constructorCallback.onProgress(progress);
+
+            if (completedCount == requiredCount) {
+                constructorCallback.onComplete(progress);
             }
         }
 
diff --git a/www/offline-region.js b/www/offline-region.js
index 1442cc3..042fc81 100644
--- a/www/offline-region.js
+++ b/www/offline-region.js
@@ -5,26 +5,22 @@ var cordova = require("cordova"),
 function OfflineRegion(options) {
     var onLoad = _onLoad.bind(this),
         onProgress = _onProgress.bind(this),
-        onError = this._error.bind(this),
-        onProgressId = this._registerCallback('onProgress', onProgress);
+        onComplete = _onComplete.bind(this),
+        onError = _onError.bind(this),
+        onProgressId = this._registerCallback('onProgress', onProgress),
+        onCompleteId = this._registerCallback('onComplete', onComplete, onError);
 
-    this._error = onError;
+    this._error = this._error.bind(this);
     this._downloaded = false;
     this._downloading = false;
 
     this.initEvents("Mapbox.MapInstance");
     this.createStickyChannel("load");
+    this.createChannel("progress");
     this.createStickyChannel("complete");
     this.createStickyChannel("error");
-    this.createChannel("progress");
 
-    if (options.progress) {
-        var progress = options.progress;
-        delete options.progress;
-        this.on("progress", progress);
-    }
-
-    exec(onLoad, this._error, "Mapbox", "createOfflineRegion", [options, onProgressId]);
+    exec(onLoad, this._error, "Mapbox", "createOfflineRegion", [options, onProgressId, onCompleteId]);
 
     function _onLoad(resp) {
         this._id = resp.id;
@@ -36,6 +32,22 @@ function OfflineRegion(options) {
     function _onProgress(progress) {
         this.fire("progress", progress);
     }
+
+    function _onComplete(resp) {
+        this._downloading = false;
+        this._downloaded = true;
+        this.fire("complete", resp);
+    }
+
+    function _onError(error) {
+        this._downloading = false;
+        this._downloaded = false;
+        try {
+            this._error(error);
+        } catch (e) {
+            this.fire("error", e);
+        }
+    }
 }
 
 EventsMixin(OfflineRegion.prototype);
@@ -72,28 +84,24 @@ OfflineRegion.prototype._registerCallback = function (name, success, fail) {
 
 OfflineRegion.prototype.download = function () {
     this._downloading = true;
-
     this._execAfterLoad(onSuccess, onError, "downloadOfflineRegion");
-
-    function onSuccess(resp) {
-        this._downloading = false;
-        this._downloaded = true;
-        this.fire("complete", resp);
-    }
-
-    function onError(error) {
-        this._downloading = false;
-        this._downloaded = false;
-        try {
-            this._error(error);
-        } catch (e) {
-            this.fire("error", e);
-        }
-    }
 };
 
 OfflineRegion.prototype.pause = function () {
+    this._downloading = false;
     this._execAfterLoad(successCallback, errorCallback, "pauseOfflineRegion");
 };
 
+Object.defineProperty(OfflineRegion, "downloading", {
+    get: function () {
+        return this._downloading;
+    }
+});
+
+Object.defineProperty(OfflineRegion, "downloaded", {
+    get: function () {
+        return this._downloaded;
+    }
+});
+
 module.exports = OfflineRegion;

From 2c557327804c89fe97e1544d055c09c0b46a958d Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Thu, 24 Mar 2016 12:11:10 -0700
Subject: [PATCH 18/37] fixing complete and error callbacks.

---
 src/android/Mapbox.java        | 12 ++++++++----
 src/android/OfflineRegion.java | 24 ++++++++++++++++++------
 www/offline-region.js          | 22 ++++++++++++++++------
 3 files changed, 42 insertions(+), 16 deletions(-)

diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 04fa223..97594f7 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -207,14 +207,14 @@ else if (ACTION_DOWNLOAD_OFFLINE_REGION.equals(action)) {
       final int offlineRegionId = args.getInt(0);
       final OfflineRegion region = OfflineRegion.getOfflineRegion(offlineRegionId);
       region.download();
-      // TODO: Need to fire callbackContext.success() upon completion and callbackContext.error() upon error.
+      callbackContext.success();
     }
 
     else if (ACTION_PAUSE_OFFLINE_REGION.equals(action)) {
       final int offlineRegionId = args.getInt(0);
       final OfflineRegion region = OfflineRegion.getOfflineRegion(offlineRegionId);
       region.pause();
-      // TODO: Need to fire callbackContext.success() upon completion and callbackContext.error() upon error.
+      callbackContext.success();
     }
 
     else if (ACTION_GET_TILT.equals(action)) {
@@ -419,14 +419,18 @@ public void onProgress(JSONObject progress) {
 
       @Override
       public void onComplete(JSONObject progress) {
-        onComplete.success(progress);
+        Log.d(LOG_TAG, "complete");
+        PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
+        result.setKeepCallback(true);
+        onComplete.sendPluginResult(result);
       }
 
       @Override
       public void onError(String error) {
         String message = "Failed to create offline region: " + error;
         Log.e(LOG_TAG, message);
-        callback.error(message);
+        PluginResult result = new PluginResult(PluginResult.Status.ERROR, message);
+        result.setKeepCallback(true);
         onComplete.error(message);
       }
     });
diff --git a/src/android/OfflineRegion.java b/src/android/OfflineRegion.java
index 4d3061a..a2b71f1 100644
--- a/src/android/OfflineRegion.java
+++ b/src/android/OfflineRegion.java
@@ -1,5 +1,7 @@
 package com.telerik.plugins.mapbox;
 
+import android.util.Log;
+
 import com.mapbox.mapboxsdk.geometry.LatLng;
 import com.mapbox.mapboxsdk.geometry.LatLngBounds;
 import com.mapbox.mapboxsdk.offline.OfflineManager;
@@ -15,6 +17,8 @@
 import java.util.HashMap;
 
 public class OfflineRegion {
+    public static final String LOG_TAG = "OfflineRegion";
+
     // JSON encoding/decoding
     public static final String JSON_CHARSET = "UTF-8";
     public static final String JSON_FIELD_REGION_NAME = "FIELD_REGION_NAME";
@@ -75,6 +79,7 @@ public void onCreate(com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion) {
                 @Override
                 public void onError(String error) {
                     constructorCallback.onError(error);
+                    Log.e(LOG_TAG, error);
                     OfflineRegion.removeOfflineRegion(getId());
                 }
             });
@@ -98,10 +103,12 @@ public JSONObject getMetadata() throws JSONException {
     }
 
     public void download() {
+        Log.d(LOG_TAG, "download()");
         mapboxRegions.get(this.mapboxOfflineRegionId).setDownloadState(com.mapbox.mapboxsdk.offline.OfflineRegion.STATE_ACTIVE);
     }
 
     public void pause() {
+        Log.d(LOG_TAG, "pause()");
         mapboxRegions.get(this.mapboxOfflineRegionId).setDownloadState(com.mapbox.mapboxsdk.offline.OfflineRegion.STATE_INACTIVE);
     }
 
@@ -134,27 +141,32 @@ private class OfflineRegionObserver implements com.mapbox.mapboxsdk.offline.Offl
         public void onStatusChanged(OfflineRegionStatus status) {
             long completedCount = status.getCompletedResourceCount();
             long requiredCount = status.getRequiredResourceCount();
+            double percentage = requiredCount >= 0 ? (100.0 * completedCount / requiredCount) : 0.0;
             JSONObject progress = new JSONObject();
 
             try {
-                progress.put("completedCount", status.getCompletedResourceCount());
+                progress.put("completedCount", completedCount);
                 progress.put("completedSize", status.getCompletedResourceSize());
-                progress.put("requiredCount", status.getRequiredResourceCount());
+                progress.put("requiredCount", requiredCount);
+                progress.put("percentage", percentage);
             } catch (JSONException e) {
                 constructorCallback.onError(e.getMessage());
                 return;
             }
 
-            constructorCallback.onProgress(progress);
-
-            if (completedCount == requiredCount) {
+            if (status.isComplete()) {
                 constructorCallback.onComplete(progress);
+            } else {
+                constructorCallback.onProgress(progress);
             }
         }
 
         @Override
         public void onError(OfflineRegionError error) {
-            constructorCallback.onError(error.getMessage());
+            String message = "OfflineRegionError: [" + error.getReason() + "] " + error.getMessage();
+            constructorCallback.onError(message);
+            Log.e(LOG_TAG, message);
+            pause();
         }
 
         @Override
diff --git a/www/offline-region.js b/www/offline-region.js
index 042fc81..86f52e3 100644
--- a/www/offline-region.js
+++ b/www/offline-region.js
@@ -36,12 +36,12 @@ function OfflineRegion(options) {
     function _onComplete(resp) {
         this._downloading = false;
         this._downloaded = true;
+        console.log("_onComplete()", resp);
         this.fire("complete", resp);
     }
 
     function _onError(error) {
-        this._downloading = false;
-        this._downloaded = false;
+        console.log("_onError()", error);
         try {
             this._error(error);
         } catch (e) {
@@ -54,6 +54,8 @@ EventsMixin(OfflineRegion.prototype);
 
 OfflineRegion.prototype._error = function (err) {
     var error = new Error("OfflineRegion error (ID: " + this._id + "): " + err);
+    this._downloading = false;
+    this._downloaded = false;
     console.warn("throwing OfflineRegionError: ", error);
     throw error;
 };
@@ -83,22 +85,30 @@ OfflineRegion.prototype._registerCallback = function (name, success, fail) {
 };
 
 OfflineRegion.prototype.download = function () {
+    console.log("download", this);
     this._downloading = true;
-    this._execAfterLoad(onSuccess, onError, "downloadOfflineRegion");
+    this._execAfterLoad(onSuccess, this._error, "downloadOfflineRegion");
+    function onSuccess() {
+        console.log("Download started!");
+    }
 };
 
 OfflineRegion.prototype.pause = function () {
+    console.log("pause", this);
     this._downloading = false;
-    this._execAfterLoad(successCallback, errorCallback, "pauseOfflineRegion");
+    this._execAfterLoad(onSuccess, this._error, "pauseOfflineRegion");
+    function onSuccess() {
+        console.log("Download paused!");
+    }
 };
 
-Object.defineProperty(OfflineRegion, "downloading", {
+Object.defineProperty(OfflineRegion.prototype, "downloading", {
     get: function () {
         return this._downloading;
     }
 });
 
-Object.defineProperty(OfflineRegion, "downloaded", {
+Object.defineProperty(OfflineRegion.prototype, "downloaded", {
     get: function () {
         return this._downloaded;
     }

From 9452eb0c03a571bde2334a1a2e49e1917894b50f Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Thu, 24 Mar 2016 13:20:24 -0700
Subject: [PATCH 19/37] fixing multiple complete callback issue and cordova
 freezing issue

---
 src/android/Mapbox.java        | 1 -
 src/android/OfflineRegion.java | 8 --------
 www/offline-region.js          | 7 ++++++-
 3 files changed, 6 insertions(+), 10 deletions(-)

diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 97594f7..ac3e29c 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -421,7 +421,6 @@ public void onProgress(JSONObject progress) {
       public void onComplete(JSONObject progress) {
         Log.d(LOG_TAG, "complete");
         PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
-        result.setKeepCallback(true);
         onComplete.sendPluginResult(result);
       }
 
diff --git a/src/android/OfflineRegion.java b/src/android/OfflineRegion.java
index a2b71f1..c33797f 100644
--- a/src/android/OfflineRegion.java
+++ b/src/android/OfflineRegion.java
@@ -1,7 +1,5 @@
 package com.telerik.plugins.mapbox;
 
-import android.util.Log;
-
 import com.mapbox.mapboxsdk.geometry.LatLng;
 import com.mapbox.mapboxsdk.geometry.LatLngBounds;
 import com.mapbox.mapboxsdk.offline.OfflineManager;
@@ -17,8 +15,6 @@
 import java.util.HashMap;
 
 public class OfflineRegion {
-    public static final String LOG_TAG = "OfflineRegion";
-
     // JSON encoding/decoding
     public static final String JSON_CHARSET = "UTF-8";
     public static final String JSON_FIELD_REGION_NAME = "FIELD_REGION_NAME";
@@ -79,7 +75,6 @@ public void onCreate(com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion) {
                 @Override
                 public void onError(String error) {
                     constructorCallback.onError(error);
-                    Log.e(LOG_TAG, error);
                     OfflineRegion.removeOfflineRegion(getId());
                 }
             });
@@ -103,12 +98,10 @@ public JSONObject getMetadata() throws JSONException {
     }
 
     public void download() {
-        Log.d(LOG_TAG, "download()");
         mapboxRegions.get(this.mapboxOfflineRegionId).setDownloadState(com.mapbox.mapboxsdk.offline.OfflineRegion.STATE_ACTIVE);
     }
 
     public void pause() {
-        Log.d(LOG_TAG, "pause()");
         mapboxRegions.get(this.mapboxOfflineRegionId).setDownloadState(com.mapbox.mapboxsdk.offline.OfflineRegion.STATE_INACTIVE);
     }
 
@@ -165,7 +158,6 @@ public void onStatusChanged(OfflineRegionStatus status) {
         public void onError(OfflineRegionError error) {
             String message = "OfflineRegionError: [" + error.getReason() + "] " + error.getMessage();
             constructorCallback.onError(message);
-            Log.e(LOG_TAG, message);
             pause();
         }
 
diff --git a/www/offline-region.js b/www/offline-region.js
index 86f52e3..9f0b716 100644
--- a/www/offline-region.js
+++ b/www/offline-region.js
@@ -20,7 +20,12 @@ function OfflineRegion(options) {
     this.createStickyChannel("complete");
     this.createStickyChannel("error");
 
-    exec(onLoad, this._error, "Mapbox", "createOfflineRegion", [options, onProgressId, onCompleteId]);
+    // TODO: For some reason calling exec within Cordova's 'deviceready'
+    //       callback causes cordova to freeze and stop loading. Delaying it by
+    //       one 'tick' seems to avoid the issue.
+    window.setTimeout(function () {
+        exec(onLoad, this._error, "Mapbox", "createOfflineRegion", [options, onProgressId, onCompleteId]);
+    }, 0);
 
     function _onLoad(resp) {
         this._id = resp.id;

From af2e379ef370b262786373611c34521d060c622d Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Thu, 24 Mar 2016 13:23:15 -0700
Subject: [PATCH 20/37] removing debug statments

---
 www/offline-region.js | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/www/offline-region.js b/www/offline-region.js
index 9f0b716..ba31fd6 100644
--- a/www/offline-region.js
+++ b/www/offline-region.js
@@ -80,8 +80,6 @@ OfflineRegion.prototype._execAfterLoad = function () {
 OfflineRegion.prototype._registerCallback = function (name, success, fail) {
     var callbackId = "MapboxOfflineRegion" + name + cordova.callbackId++;
 
-    console.log("_registerCallback(): " + callbackId);
-
     success = success ||  function () { console.log(callbackId + "() success!", arguments); };
     fail = fail ||  function () { console.log(callbackId + "() fail :(", arguments); };
 
@@ -90,7 +88,6 @@ OfflineRegion.prototype._registerCallback = function (name, success, fail) {
 };
 
 OfflineRegion.prototype.download = function () {
-    console.log("download", this);
     this._downloading = true;
     this._execAfterLoad(onSuccess, this._error, "downloadOfflineRegion");
     function onSuccess() {
@@ -99,7 +96,6 @@ OfflineRegion.prototype.download = function () {
 };
 
 OfflineRegion.prototype.pause = function () {
-    console.log("pause", this);
     this._downloading = false;
     this._execAfterLoad(onSuccess, this._error, "pauseOfflineRegion");
     function onSuccess() {

From 527f68c1726381f3ac76083b3c10a3442b43c7ed Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Thu, 24 Mar 2016 14:17:12 -0700
Subject: [PATCH 21/37] Updated debug statements.

---
 www/offline-region.js | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/www/offline-region.js b/www/offline-region.js
index ba31fd6..7a9b598 100644
--- a/www/offline-region.js
+++ b/www/offline-region.js
@@ -41,12 +41,10 @@ function OfflineRegion(options) {
     function _onComplete(resp) {
         this._downloading = false;
         this._downloaded = true;
-        console.log("_onComplete()", resp);
         this.fire("complete", resp);
     }
 
     function _onError(error) {
-        console.log("_onError()", error);
         try {
             this._error(error);
         } catch (e) {
@@ -91,7 +89,7 @@ OfflineRegion.prototype.download = function () {
     this._downloading = true;
     this._execAfterLoad(onSuccess, this._error, "downloadOfflineRegion");
     function onSuccess() {
-        console.log("Download started!");
+        console.log("Mapbox OfflineRegion download started.");
     }
 };
 
@@ -99,7 +97,7 @@ OfflineRegion.prototype.pause = function () {
     this._downloading = false;
     this._execAfterLoad(onSuccess, this._error, "pauseOfflineRegion");
     function onSuccess() {
-        console.log("Download paused!");
+        console.log("Mapbox OfflineRegion download paused.");
     }
 };
 

From b42a016027dbaaf9675f66a179efebcd71430884 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Thu, 24 Mar 2016 14:37:32 -0700
Subject: [PATCH 22/37] Implemented Android lifecycle methods.

---
 src/android/Map.java    |  5 ++++
 src/android/Mapbox.java | 53 +++++++++++++++++++++++++++++------------
 www/map-instance.js     |  2 +-
 3 files changed, 44 insertions(+), 16 deletions(-)

diff --git a/src/android/Map.java b/src/android/Map.java
index a606857..3b3e7de 100644
--- a/src/android/Map.java
+++ b/src/android/Map.java
@@ -13,6 +13,7 @@
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.util.Collection;
 import java.util.HashMap;
 
 public class Map {
@@ -30,6 +31,10 @@ public static void create(MapView mapView, JSONObject options, MapCreatedCallbac
         maps.put(map.getId(), map);
     }
 
+    public static Collection<Map> maps() {
+        return maps.values();
+    }
+
     public static Map getMap(int id) {
         return maps.get(id);
     }
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index ac3e29c..d1c30cd 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -43,7 +43,7 @@ public class Mapbox extends CordovaPlugin {
 
   private static final String MAPBOX_ACCESSTOKEN_RESOURCE_KEY = "mapbox_accesstoken";
 
-  private static final String ACTION_CREATE = "create";
+  private static final String ACTION_CREATE_MAP = "createMap";
   private static final String ACTION_JUMP_TO = "jumpTo";
   private static final String ACTION_SHOW_USER_LOCATION = "showUserLocation";
   private static final String ACTION_CREATE_OFFLINE_REGION = "createOfflineRegion";
@@ -83,11 +83,11 @@ public boolean execute(Command command) throws JSONException {
     final CordovaArgs args = command.getArgs();
     final CallbackContext callbackContext = command.getCallbackContext();
 
-    if (ACTION_CREATE.equals(action)) {
+    if (ACTION_CREATE_MAP.equals(action)) {
       final JSONObject options = args.getJSONObject(0);
       boolean showUserLocation = !options.isNull("showUserLocation") && options.getBoolean("showUserLocation");
       if (!showUserLocation || requestPermission(command, COARSE_LOCATION, FINE_LOCATION)) {
-        this.create(options, callbackContext);
+        this.createMap(options, callbackContext);
       }
     }
 
@@ -324,7 +324,7 @@ public void run() {
     return true;
   }
 
-  private void create(final JSONObject options, final CallbackContext callback) {
+  private void createMap(final JSONObject options, final CallbackContext callback) {
     if (accessToken == null) {
       callback.error(MAPBOX_ACCESSTOKEN_RESOURCE_KEY + " not set in strings.xml");
       return;
@@ -469,17 +469,40 @@ public void onRequestPermissionResult(int commandId, String[] permissions, int[]
     Command.execute(this, commandId);
   }
 
-//  public void onPause(boolean multitasking) {
-//    mapView.onPause();
-//  }
-//
-//  public void onResume(boolean multitasking) {
-//    mapView.onResume();
-//  }
-//
-//  public void onDestroy() {
-//    mapView.onDestroy();
-//  }
+  @Override
+  public void onStart() {
+    for (Map map : Map.maps()) {
+      map.getMapView().onStart();
+    }
+  }
+
+  @Override
+  public void onResume(boolean multitasking) {
+    for (Map map : Map.maps()) {
+      map.getMapView().onResume();
+    }
+  }
+
+  @Override
+  public void onPause(boolean multitasking) {
+    for (Map map : Map.maps()) {
+      map.getMapView().onPause();
+    }
+  }
+
+  @Override
+  public void onStop() {
+    for (Map map : Map.maps()) {
+      map.getMapView().onStop();
+    }
+  }
+
+  @Override
+  public void onDestroy() {
+    for (Map map : Map.maps()) {
+      map.getMapView().onDestroy();
+    }
+  }
 
   private float getRetinaFactor() {
     Activity activity = this.cordova.getActivity();
diff --git a/www/map-instance.js b/www/map-instance.js
index 9ee406f..75c2074 100644
--- a/www/map-instance.js
+++ b/www/map-instance.js
@@ -10,7 +10,7 @@ function MapInstance(options) {
     this.initEvents("Mapbox.MapInstance");
     this.createStickyChannel("load");
 
-    exec(onLoad, this._error, "Mapbox", "create", [options]);
+    exec(onLoad, this._error, "Mapbox", "createMap", [options]);
 
     function _onLoad(resp) {
         this._id = resp.id;

From a95a49067f4cb4620913fbbc954c4af91b7a1e8c Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Thu, 24 Mar 2016 17:56:13 -0700
Subject: [PATCH 23/37] Implmented loadOfflineRegions()

---
 plugin.xml                     |   1 +
 src/android/Mapbox.java        |  92 ++++++++++++--------
 src/android/MapboxManager.java | 154 +++++++++++++++++++++++++++++++++
 src/android/OfflineRegion.java | 115 +++++-------------------
 src/android/mapbox.gradle      |   3 +-
 www/Mapbox.js                  |  20 ++++-
 www/offline-region.js          |   7 +-
 7 files changed, 253 insertions(+), 139 deletions(-)
 create mode 100644 src/android/MapboxManager.java

diff --git a/plugin.xml b/plugin.xml
index 91ab77a..3d0516e 100755
--- a/plugin.xml
+++ b/plugin.xml
@@ -57,6 +57,7 @@
 
     <framework src="src/android/mapbox.gradle" custom="true" type="gradleReference"/>
     <source-file src="src/android/Mapbox.java" target-dir="src/com/telerik/plugins/mapbox"/>
+    <source-file src="src/android/MapboxManager.java" target-dir="src/com/telerik/plugins/mapbox"/>
     <source-file src="src/android/OfflineRegion.java" target-dir="src/com/telerik/plugins/mapbox"/>
     <source-file src="src/android/Map.java" target-dir="src/com/telerik/plugins/mapbox"/>
 
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index d1c30cd..990a29f 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -46,6 +46,7 @@ public class Mapbox extends CordovaPlugin {
   private static final String ACTION_CREATE_MAP = "createMap";
   private static final String ACTION_JUMP_TO = "jumpTo";
   private static final String ACTION_SHOW_USER_LOCATION = "showUserLocation";
+  private static final String ACTION_LIST_OFFLINE_REGIONS = "listOfflineRegions";
   private static final String ACTION_CREATE_OFFLINE_REGION = "createOfflineRegion";
   private static final String ACTION_DOWNLOAD_OFFLINE_REGION = "downloadOfflineRegion";
   private static final String ACTION_PAUSE_OFFLINE_REGION = "pauseOfflineRegion";
@@ -64,12 +65,15 @@ public class Mapbox extends CordovaPlugin {
   private static float retinaFactor;
   private String accessToken;
 
+  private MapboxManager mapboxManager;
+
   @Override
   public void initialize(CordovaInterface cordova, CordovaWebView webView) {
     super.initialize(cordova, webView);
 
     this.retinaFactor = this.getRetinaFactor();
     this.accessToken = this.getAccessToken();
+    this.mapboxManager = new MapboxManager(accessToken, retinaFactor, webView);
   }
 
   @Override
@@ -196,6 +200,10 @@ public boolean onInfoWindowClick(Marker marker) {
       );
     }
 
+    else if (ACTION_LIST_OFFLINE_REGIONS.equals(action)) {
+      this.listOfflineRegions(callbackContext);
+    }
+
     else if (ACTION_CREATE_OFFLINE_REGION.equals(action)) {
       final JSONObject options = args.getJSONObject(0);
       final CallbackContext onProgress = new CallbackContext(args.getString(1), this.webView);
@@ -205,14 +213,14 @@ else if (ACTION_CREATE_OFFLINE_REGION.equals(action)) {
 
     else if (ACTION_DOWNLOAD_OFFLINE_REGION.equals(action)) {
       final int offlineRegionId = args.getInt(0);
-      final OfflineRegion region = OfflineRegion.getOfflineRegion(offlineRegionId);
+      final OfflineRegion region = this.mapboxManager.getOfflineRegion(offlineRegionId);
       region.download();
       callbackContext.success();
     }
 
     else if (ACTION_PAUSE_OFFLINE_REGION.equals(action)) {
       final int offlineRegionId = args.getInt(0);
-      final OfflineRegion region = OfflineRegion.getOfflineRegion(offlineRegionId);
+      final OfflineRegion region = this.mapboxManager.getOfflineRegion(offlineRegionId);
       region.pause();
       callbackContext.success();
     }
@@ -391,46 +399,58 @@ private MapView createMapView(String accessToken, JSONObject options) {
     return mapView;
   }
 
-  public void createOfflineRegion(JSONObject options, final CallbackContext callback, final CallbackContext onProgress, final CallbackContext onComplete) throws JSONException {
-    OfflineManager offlineManager = OfflineManager.getInstance(this.webView.getContext());
-    offlineManager.setAccessToken(this.accessToken);
-    OfflineRegion.create(offlineManager, this.retinaFactor, options, new OfflineRegion.OfflineRegionCreatedCallback() {
+  public void listOfflineRegions(final CallbackContext callback) {
+    cordova.getActivity().runOnUiThread(new Runnable() {
       @Override
-      public void onCreate(OfflineRegion region) {
-        JSONObject resp = new JSONObject();
-        try {
-          resp.put("id", region.getId());
-          callback.success(resp);
-          return;
-        } catch (JSONException e) {
-          String error = "Failed to create offline region: " + e.getMessage();
-          Log.e(LOG_TAG, error);
-          callback.error(error);
-          return;
-        }
-      }
+      public void run() {
+        OfflineManager offlineManager = OfflineManager.getInstance(webView.getContext());
+        offlineManager.setAccessToken(accessToken);
+        mapboxManager.loadOfflineRegions(new MapboxManager.LoadOfflineRegionsCallback() {
+          @Override
+          public void onList(JSONArray offlineRegions) {
+            callback.success(offlineRegions);
+          }
 
-      @Override
-      public void onProgress(JSONObject progress) {
-        PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
-        result.setKeepCallback(true);
-        onProgress.sendPluginResult(result);
+          @Override
+          public void onError(String error) {
+            String message = "Error loading offline regions: " + error;
+            callback.error(message);
+          }
+        });
       }
+    });
+  }
 
+  public void createOfflineRegion(final JSONObject options, final CallbackContext callback, final CallbackContext onProgress, final CallbackContext onComplete) throws JSONException {
+    cordova.getActivity().runOnUiThread(new Runnable() {
       @Override
-      public void onComplete(JSONObject progress) {
-        Log.d(LOG_TAG, "complete");
-        PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
-        onComplete.sendPluginResult(result);
-      }
+      public void run() {
+        OfflineManager offlineManager = OfflineManager.getInstance(webView.getContext());
+        offlineManager.setAccessToken(accessToken);
+        mapboxManager.createOfflineRegion(options, callback, new MapboxManager.OfflineRegionStatusCallback() {
+          @Override
+          public void onProgress(JSONObject progress) {
+            PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
+            result.setKeepCallback(true);
+            onProgress.sendPluginResult(result);
+          }
 
-      @Override
-      public void onError(String error) {
-        String message = "Failed to create offline region: " + error;
-        Log.e(LOG_TAG, message);
-        PluginResult result = new PluginResult(PluginResult.Status.ERROR, message);
-        result.setKeepCallback(true);
-        onComplete.error(message);
+          @Override
+          public void onComplete(JSONObject progress) {
+            Log.d(LOG_TAG, "complete");
+            PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
+            onComplete.sendPluginResult(result);
+          }
+
+          @Override
+          public void onError(String error) {
+            String message = "Failed to create offline region: " + error;
+            Log.e(LOG_TAG, message);
+            PluginResult result = new PluginResult(PluginResult.Status.ERROR, message);
+            result.setKeepCallback(true);
+            onComplete.error(message);
+          }
+        });
       }
     });
   }
diff --git a/src/android/MapboxManager.java b/src/android/MapboxManager.java
new file mode 100644
index 0000000..8e99a58
--- /dev/null
+++ b/src/android/MapboxManager.java
@@ -0,0 +1,154 @@
+package com.telerik.plugins.mapbox;
+
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
+import com.mapbox.mapboxsdk.offline.OfflineManager;
+import com.mapbox.mapboxsdk.offline.OfflineRegionDefinition;
+import com.mapbox.mapboxsdk.offline.OfflineTilePyramidRegionDefinition;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaWebView;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+
+class MapboxManager {
+
+    // JSON encoding/decoding
+    public static final String JSON_CHARSET = "UTF-8";
+    public static final String JSON_FIELD_REGION_NAME = "FIELD_REGION_NAME";
+
+    private String mapboxAccessToken;
+
+    private Float density;
+
+    private CordovaWebView cordovaWebView;
+
+    private OfflineManager offlineManager;
+
+    private HashMap<Integer, OfflineRegion> regions = new HashMap<Integer, OfflineRegion>();
+
+    private HashMap<Long, com.mapbox.mapboxsdk.offline.OfflineRegion> mapboxRegions = new HashMap<Long, com.mapbox.mapboxsdk.offline.OfflineRegion>();
+
+    private int ids = 0;
+
+    public interface OfflineRegionStatusCallback {
+        void onComplete(JSONObject progress);
+        void onProgress(JSONObject progress);
+        void onError(String error);
+    }
+
+    public interface LoadOfflineRegionsCallback {
+        void onList(JSONArray regions);
+        void onError(String error);
+    }
+
+    public MapboxManager(String accessToken, Float screenDensity, CordovaWebView webView) {
+        this.mapboxAccessToken = accessToken;
+        this.density = screenDensity;
+        this.cordovaWebView = webView;
+        this.offlineManager = OfflineManager.getInstance(webView.getContext());
+    }
+
+    public void loadOfflineRegions(final LoadOfflineRegionsCallback callback) {
+        this.offlineManager.listOfflineRegions(new OfflineManager.ListOfflineRegionsCallback() {
+            @Override
+            public void onList(com.mapbox.mapboxsdk.offline.OfflineRegion[] offlineRegions) {
+                try {
+                    JSONArray regions = new JSONArray();
+                    JSONObject response;
+                    for (com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion : offlineRegions) {
+                        OfflineRegion region = createOfflineRegion(offlineRegion);
+                        response = new JSONObject();
+                        response.put("id", region.getId());
+                        regions.put(response);
+                    }
+                    callback.onList(regions);
+                } catch (JSONException e) {
+                    String error = "Error loading OfflineRegions: " + e.getMessage();
+                    callback.onError(error);
+                }
+            }
+
+            @Override
+            public void onError(String error) {
+
+            }
+        });
+    }
+
+    public void createOfflineRegion(final JSONObject options, final CallbackContext callback, final OfflineRegionStatusCallback offlineRegionStatusCallback) {
+        try {
+            final String regionName = options.getString("name");
+
+            JSONObject metadata = new JSONObject();
+            metadata.put(JSON_FIELD_REGION_NAME, regionName);
+            byte[] encodedMetadata =  metadata.toString().getBytes(JSON_CHARSET);
+            OfflineRegionDefinition definition = this.createOfflineRegionDefinition(density, options);
+
+            offlineManager.createOfflineRegion(definition, encodedMetadata, new OfflineManager.CreateOfflineRegionCallback() {
+                @Override
+                public void onCreate(com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion) {
+                    try {
+                        OfflineRegion region = createOfflineRegion(offlineRegion);
+                        region.setObserver(offlineRegionStatusCallback);
+                        JSONObject resp = new JSONObject();
+                        resp.put("id", region.getId());
+                        callback.success(resp);
+                    } catch (JSONException e) {
+                        this.onError(e.getMessage());
+                    }
+                }
+
+                @Override
+                public void onError(String error) {
+                    String message = "Failed to create offline region: " + error;
+                    callback.error(message);
+                }
+            });
+        } catch (JSONException e) {
+            callback.error(e.getMessage());
+        } catch (UnsupportedEncodingException e) {
+            callback.error(e.getMessage());
+        }
+    }
+
+    private OfflineRegion createOfflineRegion(com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion) throws JSONException {
+        int id = this.ids++;
+        OfflineRegion region = new OfflineRegion(id, offlineRegion);
+        byte[] encodedMetadata = offlineRegion.getMetadata();
+        JSONObject metadata = new JSONObject(encodedMetadata.toString());
+        region.setRegionName(metadata.getString(JSON_FIELD_REGION_NAME));
+        regions.put(id, region);
+        return region;
+    }
+
+    public OfflineRegion getOfflineRegion(int id) {
+        return regions.get(id);
+    }
+
+    public void removeOfflineRegion(int id) {
+        regions.remove(id);
+    }
+
+    private OfflineRegionDefinition createOfflineRegionDefinition(float retinaFactor, JSONObject options) throws JSONException {
+        String styleURL = Mapbox.getStyle(options.getString("style"));
+        double minZoom = options.getDouble("minZoom");
+        double maxZoom = options.getDouble("maxZoom");
+        JSONObject boundsOptions = options.getJSONObject("bounds");
+        double north = boundsOptions.getDouble("north");
+        double east = boundsOptions.getDouble("east");
+        double south = boundsOptions.getDouble("south");
+        double west = boundsOptions.getDouble("west");
+
+        LatLngBounds bounds = new LatLngBounds.Builder()
+                .include(new LatLng(north, west))
+                .include(new LatLng(south, east))
+                .build();
+
+        return new OfflineTilePyramidRegionDefinition(styleURL, bounds, minZoom, maxZoom, retinaFactor);
+    }
+}
diff --git a/src/android/OfflineRegion.java b/src/android/OfflineRegion.java
index c33797f..41f5a23 100644
--- a/src/android/OfflineRegion.java
+++ b/src/android/OfflineRegion.java
@@ -6,128 +6,56 @@
 import com.mapbox.mapboxsdk.offline.OfflineRegionDefinition;
 import com.mapbox.mapboxsdk.offline.OfflineRegionError;
 import com.mapbox.mapboxsdk.offline.OfflineRegionStatus;
-import com.mapbox.mapboxsdk.offline.OfflineTilePyramidRegionDefinition;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
-import java.io.UnsupportedEncodingException;
-import java.util.HashMap;
-
 public class OfflineRegion {
-    // JSON encoding/decoding
-    public static final String JSON_CHARSET = "UTF-8";
-    public static final String JSON_FIELD_REGION_NAME = "FIELD_REGION_NAME";
-
-    private static HashMap<Integer, OfflineRegion> regions = new HashMap<Integer, OfflineRegion>();
-
-    private static HashMap<Long, com.mapbox.mapboxsdk.offline.OfflineRegion> mapboxRegions = new HashMap<Long, com.mapbox.mapboxsdk.offline.OfflineRegion>();
-
-    private static int ids = 0;
-
-    public interface OfflineRegionCreatedCallback {
+    protected interface OfflineRegionCreateCallback {
         void onCreate(OfflineRegion region);
-        void onComplete(JSONObject progress);
-        void onProgress(JSONObject progress);
         void onError(String error);
     }
 
-    public static void create(OfflineManager offlineManager, float retinaFactor, JSONObject options, OfflineRegionCreatedCallback callback) {
-        OfflineRegion region = new OfflineRegion(offlineManager, retinaFactor, options, callback);
-        regions.put(region.getId(), region);
-    }
-
-    public static OfflineRegion getOfflineRegion(int id) {
-        return regions.get(id);
-    }
-
-    public static void removeOfflineRegion(int id) {
-        regions.remove(id);
-    }
-
     private int id;
 
     private long mapboxOfflineRegionId;
 
-    private OfflineRegionCreatedCallback constructorCallback;
+    private OfflineRegionCreateCallback createCallback;
 
     private String regionName;
 
-    private OfflineRegion(final OfflineManager offlineManager, final float retinaFactor, final JSONObject options, final OfflineRegionCreatedCallback callback) {
-        this.id = this.ids++;
-        this.constructorCallback = callback;
-
-        try {
-            this.regionName = options.getString("name");
-
-            OfflineRegionDefinition definition = this.createOfflineRegionDefinition(retinaFactor, options);
-            byte[] encodedMetadata =  this.getMetadata().toString().getBytes(JSON_CHARSET);
-
-            offlineManager.createOfflineRegion(definition, encodedMetadata, new OfflineManager.CreateOfflineRegionCallback() {
-                @Override
-                public void onCreate(com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion) {
-                    offlineRegion.setObserver(new OfflineRegionObserver(constructorCallback));
-                    mapboxOfflineRegionId = offlineRegion.getID();
-                    mapboxRegions.put(mapboxOfflineRegionId, offlineRegion);
-                    constructorCallback.onCreate(OfflineRegion.this);
-                }
-
-                @Override
-                public void onError(String error) {
-                    constructorCallback.onError(error);
-                    OfflineRegion.removeOfflineRegion(getId());
-                }
-            });
-        } catch (JSONException e) {
-            constructorCallback.onError(e.getMessage());
-        } catch (UnsupportedEncodingException e) {
-            constructorCallback.onError(e.getMessage());
-        } finally {
-            OfflineRegion.removeOfflineRegion(getId());
-        }
+    private com.mapbox.mapboxsdk.offline.OfflineRegion region;
+
+    protected OfflineRegion(int id, com.mapbox.mapboxsdk.offline.OfflineRegion region) {
+        this.id = id;
+        this.region = region;
     }
 
     public int getId() {
         return this.id;
     }
 
-    public JSONObject getMetadata() throws JSONException {
-        JSONObject metadata = new JSONObject();
-        metadata.put(JSON_FIELD_REGION_NAME, this.regionName);
-        return metadata;
+    public void setRegionName(String regionName) {
+        this.regionName = regionName;
     }
 
     public void download() {
-        mapboxRegions.get(this.mapboxOfflineRegionId).setDownloadState(com.mapbox.mapboxsdk.offline.OfflineRegion.STATE_ACTIVE);
+        this.region.setDownloadState(com.mapbox.mapboxsdk.offline.OfflineRegion.STATE_ACTIVE);
     }
 
     public void pause() {
-        mapboxRegions.get(this.mapboxOfflineRegionId).setDownloadState(com.mapbox.mapboxsdk.offline.OfflineRegion.STATE_INACTIVE);
+        this.region.setDownloadState(com.mapbox.mapboxsdk.offline.OfflineRegion.STATE_INACTIVE);
     }
 
-    private OfflineRegionDefinition createOfflineRegionDefinition(float retinaFactor, JSONObject options) throws JSONException {
-        String styleURL = Mapbox.getStyle(options.getString("style"));
-        double minZoom = options.getDouble("minZoom");
-        double maxZoom = options.getDouble("maxZoom");
-        JSONObject boundsOptions = options.getJSONObject("bounds");
-        double north = boundsOptions.getDouble("north");
-        double east = boundsOptions.getDouble("east");
-        double south = boundsOptions.getDouble("south");
-        double west = boundsOptions.getDouble("west");
-
-        LatLngBounds bounds = new LatLngBounds.Builder()
-                .include(new LatLng(north, west))
-                .include(new LatLng(south, east))
-                .build();
-
-        return new OfflineTilePyramidRegionDefinition(styleURL, bounds, minZoom, maxZoom, retinaFactor);
+    public void setObserver(MapboxManager.OfflineRegionStatusCallback statusCallback) {
+        this.region.setObserver(new OfflineRegionObserver(statusCallback));
     }
 
     private class OfflineRegionObserver implements com.mapbox.mapboxsdk.offline.OfflineRegion.OfflineRegionObserver {
-        OfflineRegionCreatedCallback constructorCallback;
+        MapboxManager.OfflineRegionStatusCallback statusCallback;
 
-        OfflineRegionObserver(OfflineRegionCreatedCallback callback) {
-            this.constructorCallback = callback;
+        OfflineRegionObserver(MapboxManager.OfflineRegionStatusCallback callback) {
+            this.statusCallback = callback;
         }
 
         @Override
@@ -143,28 +71,27 @@ public void onStatusChanged(OfflineRegionStatus status) {
                 progress.put("requiredCount", requiredCount);
                 progress.put("percentage", percentage);
             } catch (JSONException e) {
-                constructorCallback.onError(e.getMessage());
+                statusCallback.onError(e.getMessage());
                 return;
             }
 
             if (status.isComplete()) {
-                constructorCallback.onComplete(progress);
+                statusCallback.onComplete(progress);
             } else {
-                constructorCallback.onProgress(progress);
+                statusCallback.onProgress(progress);
             }
         }
 
         @Override
         public void onError(OfflineRegionError error) {
             String message = "OfflineRegionError: [" + error.getReason() + "] " + error.getMessage();
-            constructorCallback.onError(message);
+            statusCallback.onError(message);
             pause();
         }
 
         @Override
         public void mapboxTileCountLimitExceeded(long limit) {
-            constructorCallback.onError("Tile limit exceeded (limit: " + limit + ")");
+            statusCallback.onError("Tile limit exceeded (limit: " + limit + ")");
         }
     }
-
 }
diff --git a/src/android/mapbox.gradle b/src/android/mapbox.gradle
index 57aca98..a7910cd 100644
--- a/src/android/mapbox.gradle
+++ b/src/android/mapbox.gradle
@@ -2,11 +2,12 @@ ext.cdvMinSdkVersion = 15
 
 repositories {
     mavenCentral()
+    maven { url "http://oss.sonatype.org/content/repositories/snapshots/" }
 }
 
 dependencies {
     compile 'com.android.support:appcompat-v7:23.0.1'
-    compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:4.0.0-beta.2@aar'){
+    compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:4.0.0-SNAPSHOT@aar'){
         transitive=true
     }
 }
diff --git a/www/Mapbox.js b/www/Mapbox.js
index 3c72836..72b376c 100644
--- a/www/Mapbox.js
+++ b/www/Mapbox.js
@@ -1,5 +1,21 @@
-var MapInstance = require("./map-instance"),
-    OfflineRegion = require("./offline-region");
+var exec = require("cordova/exec"),
+    MapInstance = require("./map-instance"),
+    OfflineRegion = require("./offline-region"),
+    offlineRegions = [];
+
+
+function listOfflineRegions(successCallback, errorCallback) {
+    exec(
+        function (regions) {
+            console.log("Offline regions: ", regions);
+        },
+        function (error) {
+            console.error("Error getting offline regions: ", error);
+        },
+        "Mapbox",
+        "listOfflineRegions"
+    );
+}
 
 module.exports = {
     Map: MapInstance,
diff --git a/www/offline-region.js b/www/offline-region.js
index 7a9b598..4a92259 100644
--- a/www/offline-region.js
+++ b/www/offline-region.js
@@ -20,12 +20,7 @@ function OfflineRegion(options) {
     this.createStickyChannel("complete");
     this.createStickyChannel("error");
 
-    // TODO: For some reason calling exec within Cordova's 'deviceready'
-    //       callback causes cordova to freeze and stop loading. Delaying it by
-    //       one 'tick' seems to avoid the issue.
-    window.setTimeout(function () {
-        exec(onLoad, this._error, "Mapbox", "createOfflineRegion", [options, onProgressId, onCompleteId]);
-    }, 0);
+    exec(onLoad, this._error, "Mapbox", "createOfflineRegion", [options, onProgressId, onCompleteId]);
 
     function _onLoad(resp) {
         this._id = resp.id;

From 96e29b9df1af22999f27505bf2b512f49b72857a Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Fri, 25 Mar 2016 03:05:01 -0700
Subject: [PATCH 24/37] implemented region status

---
 src/android/Mapbox.java        | 120 ++++++++++++++++++---------------
 src/android/MapboxManager.java |  69 +++++++++----------
 src/android/OfflineRegion.java |  98 +++++++++++++++------------
 src/android/mapbox.gradle      |   3 +-
 www/Mapbox.js                  |  20 +-----
 www/offline-region.js          |  93 +++++++++++++++++++------
 6 files changed, 233 insertions(+), 170 deletions(-)

diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 990a29f..99cbb8d 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -50,6 +50,7 @@ public class Mapbox extends CordovaPlugin {
   private static final String ACTION_CREATE_OFFLINE_REGION = "createOfflineRegion";
   private static final String ACTION_DOWNLOAD_OFFLINE_REGION = "downloadOfflineRegion";
   private static final String ACTION_PAUSE_OFFLINE_REGION = "pauseOfflineRegion";
+  private static final String ACTION_OFFLINE_REGION_STATUS = "offlineRegionStatus";
   private static final String ACTION_ADD_MARKERS = "addMarkers";
   private static final String ACTION_ADD_MARKER_CALLBACK = "addMarkerCallback";
   private static final String ACTION_ADD_POLYGON = "addPolygon";
@@ -225,6 +226,23 @@ else if (ACTION_PAUSE_OFFLINE_REGION.equals(action)) {
       callbackContext.success();
     }
 
+    else if (ACTION_OFFLINE_REGION_STATUS.equals(action)) {
+      final int offlineRegionId = args.getInt(0);
+      final OfflineRegion region = this.mapboxManager.getOfflineRegion(offlineRegionId);
+
+      region.getStatus(new MapboxManager.OfflineRegionStatusCallback() {
+        @Override
+        public void onStatus(JSONObject status) {
+          callbackContext.success(status);
+        }
+
+        @Override
+        public void onError(String error) {
+          callbackContext.error(error);
+        }
+      });
+    }
+
     else if (ACTION_GET_TILT.equals(action)) {
 //        if (mapView != null) {
 //          cordova.getActivity().runOnUiThread(new Runnable() {
@@ -333,69 +351,65 @@ public void run() {
   }
 
   private void createMap(final JSONObject options, final CallbackContext callback) {
-    if (accessToken == null) {
-      callback.error(MAPBOX_ACCESSTOKEN_RESOURCE_KEY + " not set in strings.xml");
-      return;
-    }
-
-    cordova.getActivity().runOnUiThread(new Runnable() {
+      cordova.getActivity().runOnUiThread(new Runnable() {
       @Override
       public void run() {
-        MapView mapView = createMapView(accessToken, options);
-        Map.create(mapView, options, new Map.MapCreatedCallback() {
-          @Override
-          public void onCreate(final Map map) {
-            JSONObject resp = new JSONObject();
-            try {
-              resp.put("id", map.getId());
-              callback.success(resp);
-              return;
-            } catch (JSONException e) {
-              String error = "Failed to create map: " + e.getMessage();
-              Log.e(LOG_TAG, error);
-              callback.error(error);
-              return;
+        try {
+          MapView mapView = createMapView(accessToken, options);
+          Map.create(mapView, options, new Map.MapCreatedCallback() {
+            @Override
+            public void onCreate(final Map map) {
+              JSONObject resp = new JSONObject();
+              try {
+                resp.put("id", map.getId());
+                callback.success(resp);
+                return;
+              } catch (JSONException e) {
+                String error = "Failed to create map: " + e.getMessage();
+                Log.e(LOG_TAG, error);
+                callback.error(error);
+                return;
+              }
             }
-          }
 
-          @Override
-          public void onError(String error) {
-            String message = "Failed to create map: " + error;
-            Log.e(LOG_TAG, message);
-            callback.error(message);
-          }
-        });
+            @Override
+            public void onError(String error) {
+              String message = "Failed to create map: " + error;
+              Log.e(LOG_TAG, message);
+              callback.error(message);
+            }
+          });
+        } catch (JSONException e) {
+          callback.error(e.getMessage());
+        }
       }
     });
   }
 
-  private MapView createMapView(String accessToken, JSONObject options) {
+  private MapView createMapView(String accessToken, JSONObject options) throws JSONException {
     MapView mapView = new MapView(this.webView.getContext());
     mapView.setAccessToken(accessToken);
 
-    try {
-      final JSONObject margins = options.isNull("margins") ? null : options.getJSONObject("margins");
-      final int left = (int) (retinaFactor * (margins == null || margins.isNull("left") ? 0 : margins.getInt("left")));
-      final int right = (int) (retinaFactor * (margins == null || margins.isNull("right") ? 0 : margins.getInt("right")));
-      final int top = (int) (retinaFactor * (margins == null || margins.isNull("top") ? 0 : margins.getInt("top")));
-      final int bottom = (int) (retinaFactor * (margins == null || margins.isNull("bottom") ? 0 : margins.getInt("bottom")));
-
-      // need to do this to register a receiver which onPause later needs
-      mapView.onResume();
-      mapView.onCreate(null);
-
-      // position the mapView overlay
-      int webViewWidth = webView.getView().getWidth();
-      int webViewHeight = webView.getView().getHeight();
-      final FrameLayout layout = (FrameLayout) webView.getView().getParent();
-      FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(webViewWidth - left - right, webViewHeight - top - bottom);
-      params.setMargins(left, top, right, bottom);
-      mapView.setLayoutParams(params);
-
-      layout.addView(mapView);
-    } catch (JSONException e) {
-      e.printStackTrace();
-    }
+    final JSONObject margins = options.isNull("margins") ? null : options.getJSONObject("margins");
+    final int left = (int) (retinaFactor * (margins == null || margins.isNull("left") ? 0 : margins.getInt("left")));
+    final int right = (int) (retinaFactor * (margins == null || margins.isNull("right") ? 0 : margins.getInt("right")));
+    final int top = (int) (retinaFactor * (margins == null || margins.isNull("top") ? 0 : margins.getInt("top")));
+    final int bottom = (int) (retinaFactor * (margins == null || margins.isNull("bottom") ? 0 : margins.getInt("bottom")));
+
+    // need to do this to register a receiver which onPause later needs
+    mapView.onResume();
+    mapView.onCreate(null);
+
+    // position the mapView overlay
+    int webViewWidth = webView.getView().getWidth();
+    int webViewHeight = webView.getView().getHeight();
+    final FrameLayout layout = (FrameLayout) webView.getView().getParent();
+    FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(webViewWidth - left - right, webViewHeight - top - bottom);
+    params.setMargins(left, top, right, bottom);
+    mapView.setLayoutParams(params);
+
+    layout.addView(mapView);
+
     return mapView;
   }
 
@@ -427,7 +441,7 @@ public void createOfflineRegion(final JSONObject options, final CallbackContext
       public void run() {
         OfflineManager offlineManager = OfflineManager.getInstance(webView.getContext());
         offlineManager.setAccessToken(accessToken);
-        mapboxManager.createOfflineRegion(options, callback, new MapboxManager.OfflineRegionStatusCallback() {
+        mapboxManager.createOfflineRegion(options, callback, new MapboxManager.OfflineRegionProgressCallback() {
           @Override
           public void onProgress(JSONObject progress) {
             PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
diff --git a/src/android/MapboxManager.java b/src/android/MapboxManager.java
index 8e99a58..0190ee4 100644
--- a/src/android/MapboxManager.java
+++ b/src/android/MapboxManager.java
@@ -19,23 +19,21 @@ class MapboxManager {
 
     // JSON encoding/decoding
     public static final String JSON_CHARSET = "UTF-8";
-    public static final String JSON_FIELD_REGION_NAME = "FIELD_REGION_NAME";
-
-    private String mapboxAccessToken;
+    public static final String JSON_FIELD_ID = "id";
+    public static final String JSON_FIELD_REGION_NAME = "name";
 
     private Float density;
 
-    private CordovaWebView cordovaWebView;
-
     private OfflineManager offlineManager;
 
-    private HashMap<Integer, OfflineRegion> regions = new HashMap<Integer, OfflineRegion>();
-
-    private HashMap<Long, com.mapbox.mapboxsdk.offline.OfflineRegion> mapboxRegions = new HashMap<Long, com.mapbox.mapboxsdk.offline.OfflineRegion>();
-
-    private int ids = 0;
+    private HashMap<Long, OfflineRegion> regions = new HashMap<Long, OfflineRegion>();
 
     public interface OfflineRegionStatusCallback {
+        void onStatus(JSONObject status);
+        void onError(String error);
+    }
+
+    public interface OfflineRegionProgressCallback {
         void onComplete(JSONObject progress);
         void onProgress(JSONObject progress);
         void onError(String error);
@@ -47,10 +45,9 @@ public interface LoadOfflineRegionsCallback {
     }
 
     public MapboxManager(String accessToken, Float screenDensity, CordovaWebView webView) {
-        this.mapboxAccessToken = accessToken;
         this.density = screenDensity;
-        this.cordovaWebView = webView;
         this.offlineManager = OfflineManager.getInstance(webView.getContext());
+        this.offlineManager.setAccessToken(accessToken);
     }
 
     public void loadOfflineRegions(final LoadOfflineRegionsCallback callback) {
@@ -58,29 +55,35 @@ public void loadOfflineRegions(final LoadOfflineRegionsCallback callback) {
             @Override
             public void onList(com.mapbox.mapboxsdk.offline.OfflineRegion[] offlineRegions) {
                 try {
-                    JSONArray regions = new JSONArray();
+                    JSONArray responses = new JSONArray();
                     JSONObject response;
+                    OfflineRegion region;
                     for (com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion : offlineRegions) {
-                        OfflineRegion region = createOfflineRegion(offlineRegion);
-                        response = new JSONObject();
-                        response.put("id", region.getId());
-                        regions.put(response);
+                        if (regions.containsKey(offlineRegion.getID())) {
+                            region = regions.get(offlineRegion.getID());
+                        } else {
+                            region = createOfflineRegion(offlineRegion);
+                        }
+                        response = region.getMetadata();
+                        response.put(JSON_FIELD_ID, region.getId());
+                        responses.put(response);
                     }
-                    callback.onList(regions);
+                    callback.onList(responses);
                 } catch (JSONException e) {
-                    String error = "Error loading OfflineRegions: " + e.getMessage();
-                    callback.onError(error);
+                    this.onError(e.getMessage());
+                } catch (UnsupportedEncodingException e) {
+                    this.onError(e.getMessage());
                 }
             }
 
             @Override
             public void onError(String error) {
-
+                callback.onError(error);
             }
         });
     }
 
-    public void createOfflineRegion(final JSONObject options, final CallbackContext callback, final OfflineRegionStatusCallback offlineRegionStatusCallback) {
+    public void createOfflineRegion(final JSONObject options, final CallbackContext callback, final OfflineRegionProgressCallback offlineRegionStatusCallback) {
         try {
             final String regionName = options.getString("name");
 
@@ -95,11 +98,13 @@ public void onCreate(com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion) {
                     try {
                         OfflineRegion region = createOfflineRegion(offlineRegion);
                         region.setObserver(offlineRegionStatusCallback);
-                        JSONObject resp = new JSONObject();
-                        resp.put("id", region.getId());
-                        callback.success(resp);
+                        JSONObject response = region.getMetadata();
+                        response.put(JSON_FIELD_ID, region.getId());
+                        callback.success(response);
                     } catch (JSONException e) {
                         this.onError(e.getMessage());
+                    } catch (UnsupportedEncodingException e) {
+                        this.onError(e.getMessage());
                     }
                 }
 
@@ -116,21 +121,17 @@ public void onError(String error) {
         }
     }
 
-    private OfflineRegion createOfflineRegion(com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion) throws JSONException {
-        int id = this.ids++;
-        OfflineRegion region = new OfflineRegion(id, offlineRegion);
-        byte[] encodedMetadata = offlineRegion.getMetadata();
-        JSONObject metadata = new JSONObject(encodedMetadata.toString());
-        region.setRegionName(metadata.getString(JSON_FIELD_REGION_NAME));
-        regions.put(id, region);
+    private OfflineRegion createOfflineRegion(com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion) throws JSONException, UnsupportedEncodingException {
+        OfflineRegion region = new OfflineRegion(offlineRegion);
+        regions.put(offlineRegion.getID(), region);
         return region;
     }
 
-    public OfflineRegion getOfflineRegion(int id) {
+    public OfflineRegion getOfflineRegion(long id) {
         return regions.get(id);
     }
 
-    public void removeOfflineRegion(int id) {
+    public void removeOfflineRegion(long id) {
         regions.remove(id);
     }
 
diff --git a/src/android/OfflineRegion.java b/src/android/OfflineRegion.java
index 41f5a23..464a36b 100644
--- a/src/android/OfflineRegion.java
+++ b/src/android/OfflineRegion.java
@@ -1,42 +1,51 @@
 package com.telerik.plugins.mapbox;
 
-import com.mapbox.mapboxsdk.geometry.LatLng;
-import com.mapbox.mapboxsdk.geometry.LatLngBounds;
-import com.mapbox.mapboxsdk.offline.OfflineManager;
-import com.mapbox.mapboxsdk.offline.OfflineRegionDefinition;
 import com.mapbox.mapboxsdk.offline.OfflineRegionError;
 import com.mapbox.mapboxsdk.offline.OfflineRegionStatus;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
-public class OfflineRegion {
-    protected interface OfflineRegionCreateCallback {
-        void onCreate(OfflineRegion region);
-        void onError(String error);
-    }
+import java.io.UnsupportedEncodingException;
 
-    private int id;
-
-    private long mapboxOfflineRegionId;
+public class OfflineRegion {
 
-    private OfflineRegionCreateCallback createCallback;
+    public static final String JSON_CHARSET = "UTF-8";
 
-    private String regionName;
+    private JSONObject metadata;
 
     private com.mapbox.mapboxsdk.offline.OfflineRegion region;
 
-    protected OfflineRegion(int id, com.mapbox.mapboxsdk.offline.OfflineRegion region) {
-        this.id = id;
+    protected OfflineRegion(com.mapbox.mapboxsdk.offline.OfflineRegion region) throws JSONException, UnsupportedEncodingException {
         this.region = region;
+        byte[] encodedMetadata = region.getMetadata();
+        this.metadata = new JSONObject(new String(encodedMetadata, JSON_CHARSET));
     }
 
-    public int getId() {
-        return this.id;
+    public Long getId() {
+        return this.region.getID();
     }
 
-    public void setRegionName(String regionName) {
-        this.regionName = regionName;
+    public JSONObject getMetadata() {
+        return this.metadata;
+    }
+
+    public void getStatus(final MapboxManager.OfflineRegionStatusCallback statusCallback) {
+        this.region.getStatus(new com.mapbox.mapboxsdk.offline.OfflineRegion.OfflineRegionStatusCallback() {
+            @Override
+            public void onStatus(OfflineRegionStatus status) {
+                try {
+                    statusCallback.onStatus(statusToJSON(status));
+                } catch (JSONException e) {
+                    this.onError(e.getMessage());
+                }
+            }
+
+            @Override
+            public void onError(String error) {
+                statusCallback.onError(error);
+            }
+        });
     }
 
     public void download() {
@@ -47,51 +56,54 @@ public void pause() {
         this.region.setDownloadState(com.mapbox.mapboxsdk.offline.OfflineRegion.STATE_INACTIVE);
     }
 
-    public void setObserver(MapboxManager.OfflineRegionStatusCallback statusCallback) {
+    public void setObserver(MapboxManager.OfflineRegionProgressCallback statusCallback) {
         this.region.setObserver(new OfflineRegionObserver(statusCallback));
     }
 
+    private JSONObject statusToJSON(OfflineRegionStatus status) throws JSONException {
+        long completedCount = status.getCompletedResourceCount();
+        long requiredCount = status.getRequiredResourceCount();
+        double percentage = requiredCount >= 0 ? (100.0 * completedCount / requiredCount) : 0.0;
+        JSONObject jsonStatus = new JSONObject()
+            .put("completedCount", completedCount)
+            .put("completedSize", status.getCompletedResourceSize())
+            .put("requiredCount", requiredCount)
+            .put("percentage", percentage);
+
+        return jsonStatus;
+    }
+
     private class OfflineRegionObserver implements com.mapbox.mapboxsdk.offline.OfflineRegion.OfflineRegionObserver {
-        MapboxManager.OfflineRegionStatusCallback statusCallback;
+        private MapboxManager.OfflineRegionProgressCallback progressCallback;
 
-        OfflineRegionObserver(MapboxManager.OfflineRegionStatusCallback callback) {
-            this.statusCallback = callback;
+        OfflineRegionObserver(MapboxManager.OfflineRegionProgressCallback callback) {
+            this.progressCallback = callback;
         }
 
         @Override
         public void onStatusChanged(OfflineRegionStatus status) {
-            long completedCount = status.getCompletedResourceCount();
-            long requiredCount = status.getRequiredResourceCount();
-            double percentage = requiredCount >= 0 ? (100.0 * completedCount / requiredCount) : 0.0;
-            JSONObject progress = new JSONObject();
-
             try {
-                progress.put("completedCount", completedCount);
-                progress.put("completedSize", status.getCompletedResourceSize());
-                progress.put("requiredCount", requiredCount);
-                progress.put("percentage", percentage);
+                JSONObject progress = statusToJSON(status);
+                if (!status.isComplete()) {
+                    progressCallback.onProgress(progress);
+                } else {
+                    progressCallback.onComplete(progress);
+                }
             } catch (JSONException e) {
-                statusCallback.onError(e.getMessage());
-                return;
-            }
-
-            if (status.isComplete()) {
-                statusCallback.onComplete(progress);
-            } else {
-                statusCallback.onProgress(progress);
+                progressCallback.onError(e.getMessage());
             }
         }
 
         @Override
         public void onError(OfflineRegionError error) {
             String message = "OfflineRegionError: [" + error.getReason() + "] " + error.getMessage();
-            statusCallback.onError(message);
+            progressCallback.onError(message);
             pause();
         }
 
         @Override
         public void mapboxTileCountLimitExceeded(long limit) {
-            statusCallback.onError("Tile limit exceeded (limit: " + limit + ")");
+            progressCallback.onError("Tile limit exceeded (limit: " + limit + ")");
         }
     }
 }
diff --git a/src/android/mapbox.gradle b/src/android/mapbox.gradle
index a7910cd..57aca98 100644
--- a/src/android/mapbox.gradle
+++ b/src/android/mapbox.gradle
@@ -2,12 +2,11 @@ ext.cdvMinSdkVersion = 15
 
 repositories {
     mavenCentral()
-    maven { url "http://oss.sonatype.org/content/repositories/snapshots/" }
 }
 
 dependencies {
     compile 'com.android.support:appcompat-v7:23.0.1'
-    compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:4.0.0-SNAPSHOT@aar'){
+    compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:4.0.0-beta.2@aar'){
         transitive=true
     }
 }
diff --git a/www/Mapbox.js b/www/Mapbox.js
index 72b376c..fecf072 100644
--- a/www/Mapbox.js
+++ b/www/Mapbox.js
@@ -1,23 +1,9 @@
 var exec = require("cordova/exec"),
     MapInstance = require("./map-instance"),
-    OfflineRegion = require("./offline-region"),
-    offlineRegions = [];
-
-
-function listOfflineRegions(successCallback, errorCallback) {
-    exec(
-        function (regions) {
-            console.log("Offline regions: ", regions);
-        },
-        function (error) {
-            console.error("Error getting offline regions: ", error);
-        },
-        "Mapbox",
-        "listOfflineRegions"
-    );
-}
+    offline = require("./offline-region");
 
 module.exports = {
     Map: MapInstance,
-    OfflineRegion: OfflineRegion
+    createOfflineRegion: offline.createOfflineRegion,
+    listOfflineRegions: offline.listOfflineRegions
 };
diff --git a/www/offline-region.js b/www/offline-region.js
index 4a92259..b6601d1 100644
--- a/www/offline-region.js
+++ b/www/offline-region.js
@@ -2,16 +2,20 @@ var cordova = require("cordova"),
     exec = require("cordova/exec"),
     EventsMixin = require("./events-mixin");
 
-function OfflineRegion(options) {
-    var onLoad = _onLoad.bind(this),
-        onProgress = _onProgress.bind(this),
+var OFFLINE_REGIONS = {};
+
+function OfflineRegion() {
+    var onProgress = _onProgress.bind(this),
         onComplete = _onComplete.bind(this),
-        onError = _onError.bind(this),
-        onProgressId = this._registerCallback('onProgress', onProgress),
-        onCompleteId = this._registerCallback('onComplete', onComplete, onError);
+        onError = _onError.bind(this);
+
+    this._onProgressId = this._registerCallback('onProgress', onProgress);
+    this._onCompleteId = this._registerCallback('onComplete', onComplete, onError);
 
     this._error = this._error.bind(this);
-    this._downloaded = false;
+    this._create = this._create.bind(this);
+    this._instance = this._instance.bind(this);
+
     this._downloading = false;
 
     this.initEvents("Mapbox.MapInstance");
@@ -20,22 +24,12 @@ function OfflineRegion(options) {
     this.createStickyChannel("complete");
     this.createStickyChannel("error");
 
-    exec(onLoad, this._error, "Mapbox", "createOfflineRegion", [options, onProgressId, onCompleteId]);
-
-    function _onLoad(resp) {
-        this._id = resp.id;
-        this.loaded = true;
-
-        this.fire("load", {map: this});
-    }
-
     function _onProgress(progress) {
         this.fire("progress", progress);
     }
 
     function _onComplete(resp) {
         this._downloading = false;
-        this._downloaded = true;
         this.fire("complete", resp);
     }
 
@@ -50,10 +44,22 @@ function OfflineRegion(options) {
 
 EventsMixin(OfflineRegion.prototype);
 
+OfflineRegion.prototype._create = function (options) {
+    var args = [options, this._onProgressId, this._onCompleteId];
+    exec(this._instance, this._error, "Mapbox", "createOfflineRegion", args);
+};
+
+OfflineRegion.prototype._instance = function (response) {
+    this._id = response.id;
+    this._name = response.name;
+    this.loaded = true;
+    this.fire("load", {offlineRegion: this});
+    OFFLINE_REGIONS[this._id] = this;
+};
+
 OfflineRegion.prototype._error = function (err) {
     var error = new Error("OfflineRegion error (ID: " + this._id + "): " + err);
     this._downloading = false;
-    this._downloaded = false;
     console.warn("throwing OfflineRegionError: ", error);
     throw error;
 };
@@ -96,16 +102,61 @@ OfflineRegion.prototype.pause = function () {
     }
 };
 
+OfflineRegion.prototype.getStatus = function (callback) {
+    this._execAfterLoad(onSuccess, onError, "offlineRegionStatus");
+    function onSuccess(status) {
+        callback(null, status);
+    }
+    function onError(error) {
+        callback(error);
+    }
+};
+
 Object.defineProperty(OfflineRegion.prototype, "downloading", {
     get: function () {
         return this._downloading;
     }
 });
 
-Object.defineProperty(OfflineRegion.prototype, "downloaded", {
+Object.defineProperty(OfflineRegion.prototype, "name", {
     get: function () {
-        return this._downloaded;
+        return this._name;
     }
 });
 
-module.exports = OfflineRegion;
+module.exports = {
+    createOfflineRegion: function (options) {
+        var region = new OfflineRegion();
+        region._create(options);
+        return region;
+    },
+
+    listOfflineRegions: function (callback) {
+        exec(
+            function (responses) {
+                console.log("Offline regions: ", responses);
+                var regions = responses.map(function (response) {
+                        var region = OFFLINE_REGIONS[response.id];
+                        if (!region) {
+                            region = new OfflineRegion();
+                            region._instance(response);
+                        }
+                        return region;
+                    }),
+                    byName = regions.reduce(function (regionsByName, region) {
+                        regionsByName[region.name] = region;
+                        return regionsByName;
+                    }, {});
+                callback(null, byName);
+            },
+            function (errorMessage) {
+                var error = "Error getting offline regions: " + errorMessage;
+                console.error(error);
+                callback(error);
+            },
+            "Mapbox",
+            "listOfflineRegions",
+            []
+        );
+    }
+};

From ff8b3f4f34162f87454292a0dc20608b82b9c779 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Fri, 25 Mar 2016 03:15:20 -0700
Subject: [PATCH 25/37] fixed issue where no style was set when offline.

---
 src/android/Map.java    | 4 ----
 src/android/Mapbox.java | 1 +
 2 files changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/android/Map.java b/src/android/Map.java
index 3b3e7de..9ed7d8c 100644
--- a/src/android/Map.java
+++ b/src/android/Map.java
@@ -161,10 +161,6 @@ public void showUserLocation(boolean enabled) {
     }
 
     private void applyOptions(JSONObject options) throws JSONException {
-        if (options.has("style")) {
-            this.mapView.setStyleUrl(Mapbox.getStyle(options.optString("style")));
-        }
-
         if (!options.isNull("showUserLocation")) {
             this.showUserLocation(options.getBoolean("showUserLocation"));
         }
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 99cbb8d..4076f5c 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -389,6 +389,7 @@ public void onError(String error) {
   private MapView createMapView(String accessToken, JSONObject options) throws JSONException {
     MapView mapView = new MapView(this.webView.getContext());
     mapView.setAccessToken(accessToken);
+    mapView.setStyleUrl(Mapbox.getStyle(options.optString("style")));
 
     final JSONObject margins = options.isNull("margins") ? null : options.getJSONObject("margins");
     final int left = (int) (retinaFactor * (margins == null || margins.isNull("left") ? 0 : margins.getInt("left")));

From 331c5337eccd069b9cc8130d48e3851513c348ce Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Sat, 26 Mar 2016 15:28:23 -0700
Subject: [PATCH 26/37] updating to 4.0.0-rc.1

---
 src/android/mapbox.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/android/mapbox.gradle b/src/android/mapbox.gradle
index 57aca98..4580af0 100644
--- a/src/android/mapbox.gradle
+++ b/src/android/mapbox.gradle
@@ -6,7 +6,7 @@ repositories {
 
 dependencies {
     compile 'com.android.support:appcompat-v7:23.0.1'
-    compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:4.0.0-beta.2@aar'){
+    compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:4.0.0-rc.1@aar'){
         transitive=true
     }
 }

From 67e8736badd7ae4cb13b47359226817f5ff628fc Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Sat, 26 Mar 2016 15:38:26 -0700
Subject: [PATCH 27/37] Added example of proposed javascript api

---
 demo/mapboxgl-android-4.x.x-example.js | 87 ++++++++++++++++++++++++++
 1 file changed, 87 insertions(+)
 create mode 100644 demo/mapboxgl-android-4.x.x-example.js

diff --git a/demo/mapboxgl-android-4.x.x-example.js b/demo/mapboxgl-android-4.x.x-example.js
new file mode 100644
index 0000000..6d0afb4
--- /dev/null
+++ b/demo/mapboxgl-android-4.x.x-example.js
@@ -0,0 +1,87 @@
+var cabo = {
+    name: "Cabo San Lucas",
+    style: "emerald",
+    minZoom: 0,
+    maxZoom: 16,
+    bounds: {
+        north: 22.891,
+        east: -109.919,
+        south: 22.879,
+        west: -109.905
+    }
+};
+
+Mapbox.listOfflineRegions(function (err, regions) {
+    if (err) return onError(err);
+    console.log('listOfflineRegions()', regions);
+
+    var region = regions[cabo.name];
+
+    // First load will download region.
+    if (!region) {
+        region = Mapbox.createOfflineRegion(cabo);
+
+        region.on("error", function (e) {
+            console.error("OfflineRegion onError", e);
+        });
+
+        region.on("progress", function (progress) {
+            console.log("OfflineRegion download onProgress", progress);
+        });
+
+        region.on("complete", function (progress) {
+            console.log("OfflineRegion download onComplete", progress);
+        });
+
+        region.download();
+    }
+    // Subsequent loads will display offline region download status.
+    else {
+        region.getStatus(function (err, status) {
+            if (err) return onError(err);
+            console.log("OfflineRegion getStatus()", status);
+        });
+    }
+});
+
+var map = new Mapbox.Map({
+        style: 'emerald',
+        zoom: 15,
+        center: [-109.912, 22.885],
+        showUserLocation: true,
+        margins: {
+            left: 0,
+            right: 0,
+            top: 0,
+            bottom: 0
+        },
+        markers: [
+            {"title": "Marker 1", "lng": -109.912, "lat": 22.885}
+        ],
+        // NOTE: the options below are broken...
+        hideAttribution: true, // default false
+        hideLogo: true, // default false
+        hideCompass: false, // default false
+        disableRotation: false, // default false
+        disableScroll: false, // default false
+        disableZoom: false, // default false
+        disablePitch: false // default false
+    });
+
+map.on('load', function (e) {
+    map.addMarkers(
+        [
+            {"title": "Marker 2", "lng": -109.910, "lat": 22.886},
+            {"title": "Marker 3", "lng": -109.913, "lat": 22.883}
+        ],
+        function () { console.log("Markers added!"); },
+        function (e) { console.error("Error adding markers:", e); }
+    );
+
+    map.addMarkerCallback(printMarker);
+
+    function printMarker(selectedMarker) {
+        alert("Marker selected: " + JSON.stringify(selectedMarker));
+        map.addMarkerCallback(printMarker);
+    }
+});

From 26c519279d51b48beddde93c584b48e7f7d15d8f Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Sat, 26 Mar 2016 17:29:27 -0700
Subject: [PATCH 28/37] Moving static Map instance management into domain of
 mapboxManager.

---
 src/android/Map.java           | 108 ++++++++------------------
 src/android/Mapbox.java        | 137 +++++++--------------------------
 src/android/MapboxManager.java | 131 ++++++++++++++++++++++++++++++-
 3 files changed, 186 insertions(+), 190 deletions(-)

diff --git a/src/android/Map.java b/src/android/Map.java
index 9ed7d8c..4b13579 100644
--- a/src/android/Map.java
+++ b/src/android/Map.java
@@ -6,77 +6,58 @@
 import com.mapbox.mapboxsdk.geometry.LatLng;
 import com.mapbox.mapboxsdk.maps.MapView;
 import com.mapbox.mapboxsdk.maps.MapboxMap;
-import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
 import com.mapbox.mapboxsdk.maps.UiSettings;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
-import java.util.Collection;
-import java.util.HashMap;
-
 public class Map {
-    private static HashMap<Integer, Map> maps = new HashMap<Integer, Map>();
-
-    private static int ids = 0;
+    private long id;
 
-    public interface MapCreatedCallback {
-        void onCreate(Map map);
-        void onError(String error);
-    }
+    private MapView mapView;
 
-    public static void create(MapView mapView, JSONObject options, MapCreatedCallback callback) {
-        Map map = new Map(mapView, options, callback);
-        maps.put(map.getId(), map);
-    }
+    private MapboxMap mapboxMap;
 
-    public static Collection<Map> maps() {
-        return maps.values();
+    public Map(long id, final MapView mapView, final JSONObject options) throws JSONException {
+        this.id = id;
+        this.mapView = mapView;
     }
 
-    public static Map getMap(int id) {
-        return maps.get(id);
+    public long getId() {
+        return this.id;
     }
 
-    public static void removeMap(int id) {
-        maps.remove(id);
+    public MapView getMapView() {
+        return this.mapView;
     }
 
-    private int id;
-
-    private MapView mapView;
-
-    private MapboxMap mapboxMap;
+    public void setMapboxMap(MapboxMap mMap, JSONObject options) throws JSONException {
+        this.mapboxMap = mMap;
+        UiSettings uiSettings = mMap.getUiSettings();
+        uiSettings.setCompassEnabled(options.isNull("hideCompass") || !options.getBoolean("hideCompass"));
+        uiSettings.setRotateGesturesEnabled(options.isNull("disableRotation") || !options.getBoolean("disableRotation"));
+        uiSettings.setScrollGesturesEnabled(options.isNull("disableScroll") || !options.getBoolean("disableScroll"));
+        uiSettings.setZoomGesturesEnabled(options.isNull("disableZoom") || !options.getBoolean("disableZoom"));
+        uiSettings.setTiltGesturesEnabled(options.isNull("disableTilt") || !options.getBoolean("disableTilt"));
 
-    private MapCreatedCallback constructorCallback;
+        if (!options.isNull("hideAttribution") && options.getBoolean("hideAttribution")) {
+            uiSettings.setAttributionMargins(-300, 0, 0, 0);
+        }
 
-    private Map(final MapView mapView, final JSONObject options, final MapCreatedCallback callback) {
-        this.id = this.ids++;
-        this.constructorCallback = callback;
-        this.mapView = mapView;
+        if (!options.isNull("hideLogo") && options.getBoolean("hideLogo")) {
+            uiSettings.setLogoMargins(-300, 0, 0, 0);
+        }
 
-        mapView.getMapAsync(new OnMapReadyCallback() {
-            @Override
-            public void onMapReady(MapboxMap mMap) {
-                mapboxMap = mMap;
-                try {
-                    applyOptions(options);
-                    constructorCallback.onCreate(Map.this);
-                } catch (JSONException e) {
-                    Map.removeMap(getId());
-                    constructorCallback.onError(e.getMessage());
-                }
-            }
-        });
-    }
+        if (!options.isNull("showUserLocation")) {
+            this.showUserLocation(options.getBoolean("showUserLocation"));
+        }
 
-    public int getId() {
-        return this.id;
-    }
+        if (options.has("markers")) {
+            this.addMarkers(options.getJSONArray("markers"));
+        }
 
-    public MapView getMapView() {
-        return this.mapView;
+        this.jumpTo(options);
     }
 
     public MapboxMap getMapboxMap() {
@@ -159,31 +140,4 @@ public void addMarkerListener(MapboxMap.OnInfoWindowClickListener listener) {
     public void showUserLocation(boolean enabled) {
         mapboxMap.setMyLocationEnabled(enabled);
     }
-
-    private void applyOptions(JSONObject options) throws JSONException {
-        if (!options.isNull("showUserLocation")) {
-            this.showUserLocation(options.getBoolean("showUserLocation"));
-        }
-
-        UiSettings uiSettings = mapboxMap.getUiSettings();
-        uiSettings.setCompassEnabled(options.isNull("hideCompass") || !options.getBoolean("hideCompass"));
-        uiSettings.setRotateGesturesEnabled(options.isNull("disableRotation") || !options.getBoolean("disableRotation"));
-        uiSettings.setScrollGesturesEnabled(options.isNull("disableScroll") || !options.getBoolean("disableScroll"));
-        uiSettings.setZoomGesturesEnabled(options.isNull("disableZoom") || !options.getBoolean("disableZoom"));
-        uiSettings.setTiltGesturesEnabled(options.isNull("disableTilt") || !options.getBoolean("disableTilt"));
-
-        if (!options.isNull("hideAttribution") && options.getBoolean("hideAttribution")) {
-            uiSettings.setAttributionMargins(-300, 0, 0, 0);
-        }
-
-        if (!options.isNull("hideLogo") && options.getBoolean("hideLogo")) {
-            uiSettings.setLogoMargins(-300, 0, 0, 0);
-        }
-
-        if (options.has("markers")) {
-            this.addMarkers(options.getJSONArray("markers"));
-        }
-
-        this.jumpTo(options);
-    }
 }
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 4076f5c..8b2b76d 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -63,18 +63,12 @@ public class Mapbox extends CordovaPlugin {
   private static final String ACTION_SET_TILT = "setTilt";
   private static final String ACTION_ANIMATE_CAMERA = "animateCamera";
 
-  private static float retinaFactor;
-  private String accessToken;
-
   private MapboxManager mapboxManager;
 
   @Override
   public void initialize(CordovaInterface cordova, CordovaWebView webView) {
     super.initialize(cordova, webView);
-
-    this.retinaFactor = this.getRetinaFactor();
-    this.accessToken = this.getAccessToken();
-    this.mapboxManager = new MapboxManager(accessToken, retinaFactor, webView);
+    this.mapboxManager = new MapboxManager(this.getAccessToken(), this.getRetinaFactor(), webView);
   }
 
   @Override
@@ -97,8 +91,8 @@ public boolean execute(Command command) throws JSONException {
     }
 
     else if (ACTION_SHOW_USER_LOCATION.equals(action)) {
-      final int mapId = args.getInt(0);
-      final Map map = Map.getMap(mapId);
+      final long mapId = args.getLong(0);
+      final Map map = mapboxManager.getMap(mapId);
       final boolean enabled = args.getBoolean(1);
       if (requestPermission(command, COARSE_LOCATION, FINE_LOCATION)) {
         cordova.getActivity().runOnUiThread(new Runnable() {
@@ -111,8 +105,8 @@ public void run() {
     }
 
     else if (ACTION_JUMP_TO.equals(action)) {
-      final int mapId = args.getInt(0);
-      final Map map = Map.getMap(mapId);
+      final long mapId = args.getLong(0);
+      final Map map = mapboxManager.getMap(mapId);
       final JSONObject options = args.getJSONObject(1);
 
       cordova.getActivity().runOnUiThread(new Runnable() {
@@ -129,8 +123,8 @@ public void run() {
     }
 
     else if (ACTION_GET_CENTER.equals(action)) {
-      final int mapId = args.getInt(0);
-      final Map map = Map.getMap(mapId);
+      final long mapId = args.getLong(0);
+      final Map map = mapboxManager.getMap(mapId);
       try {
         callbackContext.success(map.getCenter());
       } catch (JSONException e) {
@@ -139,8 +133,8 @@ else if (ACTION_GET_CENTER.equals(action)) {
     }
 
     else if (ACTION_SET_CENTER.equals(action)) {
-      final int mapId = args.getInt(0);
-      final Map map = Map.getMap(mapId);
+      final long mapId = args.getLong(0);
+      final Map map = mapboxManager.getMap(mapId);
       final JSONArray center = args.getJSONArray(1);
       try {
         map.setCenter(center);
@@ -151,22 +145,22 @@ else if (ACTION_SET_CENTER.equals(action)) {
     }
 
     else if (ACTION_GET_ZOOMLEVEL.equals(action)) {
-      final int mapId = args.getInt(0);
-      final Map map = Map.getMap(mapId);
+      final long mapId = args.getLong(0);
+      final Map map = mapboxManager.getMap(mapId);
       callbackContext.success("" + map.getZoom());
     }
 
     else if (ACTION_SET_ZOOMLEVEL.equals(action)) {
-      final int mapId = args.getInt(0);
-      final Map map = Map.getMap(mapId);
+      final long mapId = args.getLong(0);
+      final Map map = mapboxManager.getMap(mapId);
       final double zoom = args.getDouble(1);
       map.setZoom(zoom);
       callbackContext.success();
     }
 
     else if (ACTION_ADD_MARKERS.equals(action)) {
-      final int mapId = args.getInt(0);
-      final Map map = Map.getMap(mapId);
+      final long mapId = args.getLong(0);
+      final Map map = mapboxManager.getMap(mapId);
       try {
         map.addMarkers(args.getJSONArray(1));
         callbackContext.success();
@@ -176,8 +170,8 @@ else if (ACTION_ADD_MARKERS.equals(action)) {
     }
 
     else if (ACTION_ADD_MARKER_CALLBACK.equals(action)) {
-      final int mapId = args.getInt(0);
-      final Map map = Map.getMap(mapId);
+      final long mapId = args.getLong(0);
+      final Map map = mapboxManager.getMap(mapId);
       map.addMarkerListener(
         new MapboxMap.OnInfoWindowClickListener() {
           @Override
@@ -213,21 +207,21 @@ else if (ACTION_CREATE_OFFLINE_REGION.equals(action)) {
     }
 
     else if (ACTION_DOWNLOAD_OFFLINE_REGION.equals(action)) {
-      final int offlineRegionId = args.getInt(0);
+      final long offlineRegionId = args.getLong(0);
       final OfflineRegion region = this.mapboxManager.getOfflineRegion(offlineRegionId);
       region.download();
       callbackContext.success();
     }
 
     else if (ACTION_PAUSE_OFFLINE_REGION.equals(action)) {
-      final int offlineRegionId = args.getInt(0);
+      final long offlineRegionId = args.getLong(0);
       final OfflineRegion region = this.mapboxManager.getOfflineRegion(offlineRegionId);
       region.pause();
       callbackContext.success();
     }
 
     else if (ACTION_OFFLINE_REGION_STATUS.equals(action)) {
-      final int offlineRegionId = args.getInt(0);
+      final long offlineRegionId = args.getLong(0);
       final OfflineRegion region = this.mapboxManager.getOfflineRegion(offlineRegionId);
 
       region.getStatus(new MapboxManager.OfflineRegionStatusCallback() {
@@ -354,72 +348,15 @@ private void createMap(final JSONObject options, final CallbackContext callback)
       cordova.getActivity().runOnUiThread(new Runnable() {
       @Override
       public void run() {
-        try {
-          MapView mapView = createMapView(accessToken, options);
-          Map.create(mapView, options, new Map.MapCreatedCallback() {
-            @Override
-            public void onCreate(final Map map) {
-              JSONObject resp = new JSONObject();
-              try {
-                resp.put("id", map.getId());
-                callback.success(resp);
-                return;
-              } catch (JSONException e) {
-                String error = "Failed to create map: " + e.getMessage();
-                Log.e(LOG_TAG, error);
-                callback.error(error);
-                return;
-              }
-            }
-
-            @Override
-            public void onError(String error) {
-              String message = "Failed to create map: " + error;
-              Log.e(LOG_TAG, message);
-              callback.error(message);
-            }
-          });
-        } catch (JSONException e) {
-          callback.error(e.getMessage());
-        }
+        mapboxManager.createMap(options, callback);
       }
     });
   }
 
-  private MapView createMapView(String accessToken, JSONObject options) throws JSONException {
-    MapView mapView = new MapView(this.webView.getContext());
-    mapView.setAccessToken(accessToken);
-    mapView.setStyleUrl(Mapbox.getStyle(options.optString("style")));
-
-    final JSONObject margins = options.isNull("margins") ? null : options.getJSONObject("margins");
-    final int left = (int) (retinaFactor * (margins == null || margins.isNull("left") ? 0 : margins.getInt("left")));
-    final int right = (int) (retinaFactor * (margins == null || margins.isNull("right") ? 0 : margins.getInt("right")));
-    final int top = (int) (retinaFactor * (margins == null || margins.isNull("top") ? 0 : margins.getInt("top")));
-    final int bottom = (int) (retinaFactor * (margins == null || margins.isNull("bottom") ? 0 : margins.getInt("bottom")));
-
-    // need to do this to register a receiver which onPause later needs
-    mapView.onResume();
-    mapView.onCreate(null);
-
-    // position the mapView overlay
-    int webViewWidth = webView.getView().getWidth();
-    int webViewHeight = webView.getView().getHeight();
-    final FrameLayout layout = (FrameLayout) webView.getView().getParent();
-    FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(webViewWidth - left - right, webViewHeight - top - bottom);
-    params.setMargins(left, top, right, bottom);
-    mapView.setLayoutParams(params);
-
-    layout.addView(mapView);
-
-    return mapView;
-  }
-
   public void listOfflineRegions(final CallbackContext callback) {
     cordova.getActivity().runOnUiThread(new Runnable() {
       @Override
       public void run() {
-        OfflineManager offlineManager = OfflineManager.getInstance(webView.getContext());
-        offlineManager.setAccessToken(accessToken);
         mapboxManager.loadOfflineRegions(new MapboxManager.LoadOfflineRegionsCallback() {
           @Override
           public void onList(JSONArray offlineRegions) {
@@ -440,8 +377,6 @@ public void createOfflineRegion(final JSONObject options, final CallbackContext
     cordova.getActivity().runOnUiThread(new Runnable() {
       @Override
       public void run() {
-        OfflineManager offlineManager = OfflineManager.getInstance(webView.getContext());
-        offlineManager.setAccessToken(accessToken);
         mapboxManager.createOfflineRegion(options, callback, new MapboxManager.OfflineRegionProgressCallback() {
           @Override
           public void onProgress(JSONObject progress) {
@@ -506,35 +441,35 @@ public void onRequestPermissionResult(int commandId, String[] permissions, int[]
 
   @Override
   public void onStart() {
-    for (Map map : Map.maps()) {
+    for (Map map : mapboxManager.maps()) {
       map.getMapView().onStart();
     }
   }
 
   @Override
   public void onResume(boolean multitasking) {
-    for (Map map : Map.maps()) {
+    for (Map map : mapboxManager.maps()) {
       map.getMapView().onResume();
     }
   }
 
   @Override
   public void onPause(boolean multitasking) {
-    for (Map map : Map.maps()) {
+    for (Map map : mapboxManager.maps()) {
       map.getMapView().onPause();
     }
   }
 
   @Override
   public void onStop() {
-    for (Map map : Map.maps()) {
+    for (Map map : mapboxManager.maps()) {
       map.getMapView().onStop();
     }
   }
 
   @Override
   public void onDestroy() {
-    for (Map map : Map.maps()) {
+    for (Map map : mapboxManager.maps()) {
       map.getMapView().onDestroy();
     }
   }
@@ -564,26 +499,6 @@ private String getAccessToken() {
 
     return accessToken;
   }
-
-  public static String getStyle(final String requested) {
-    if ("light".equalsIgnoreCase(requested)) {
-      return Style.LIGHT;
-    } else if ("dark".equalsIgnoreCase(requested)) {
-      return Style.DARK;
-    } else if ("emerald".equalsIgnoreCase(requested)) {
-      return Style.EMERALD;
-    } else if ("satellite".equalsIgnoreCase(requested)) {
-      return Style.SATELLITE;
-      // TODO not currently supported on Android
-      //} else if ("hybrid".equalsIgnoreCase(requested)) {
-      //    return Style.HYBRID;
-    } else if ("streets".equalsIgnoreCase(requested)) {
-      return Style.MAPBOX_STREETS;
-    } else {
-      return requested;
-    }
-  }
-
 }
 
 class Command {
diff --git a/src/android/MapboxManager.java b/src/android/MapboxManager.java
index 0190ee4..835da44 100644
--- a/src/android/MapboxManager.java
+++ b/src/android/MapboxManager.java
@@ -1,7 +1,13 @@
 package com.telerik.plugins.mapbox;
 
+import android.widget.FrameLayout;
+
+import com.mapbox.mapboxsdk.constants.Style;
 import com.mapbox.mapboxsdk.geometry.LatLng;
 import com.mapbox.mapboxsdk.geometry.LatLngBounds;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
 import com.mapbox.mapboxsdk.offline.OfflineManager;
 import com.mapbox.mapboxsdk.offline.OfflineRegionDefinition;
 import com.mapbox.mapboxsdk.offline.OfflineTilePyramidRegionDefinition;
@@ -13,19 +19,46 @@
 import org.json.JSONObject;
 
 import java.io.UnsupportedEncodingException;
+import java.util.Collection;
 import java.util.HashMap;
 
 class MapboxManager {
-
     // JSON encoding/decoding
     public static final String JSON_CHARSET = "UTF-8";
     public static final String JSON_FIELD_ID = "id";
     public static final String JSON_FIELD_REGION_NAME = "name";
 
+    public static String getStyle(final String requested) {
+        if ("light".equalsIgnoreCase(requested)) {
+            return Style.LIGHT;
+        } else if ("dark".equalsIgnoreCase(requested)) {
+            return Style.DARK;
+        } else if ("emerald".equalsIgnoreCase(requested)) {
+            return Style.EMERALD;
+        } else if ("satellite".equalsIgnoreCase(requested)) {
+            return Style.SATELLITE;
+            // TODO not currently supported on Android
+            //} else if ("hybrid".equalsIgnoreCase(requested)) {
+            //    return Style.HYBRID;
+        } else if ("streets".equalsIgnoreCase(requested)) {
+            return Style.MAPBOX_STREETS;
+        } else {
+            return requested;
+        }
+    }
+
+    private int ids = 0;
+
+    private String accessToken;
+
     private Float density;
 
+    private CordovaWebView webView;
+
     private OfflineManager offlineManager;
 
+    private static HashMap<Long, Map> maps = new HashMap<Long, Map>();
+
     private HashMap<Long, OfflineRegion> regions = new HashMap<Long, OfflineRegion>();
 
     public interface OfflineRegionStatusCallback {
@@ -45,11 +78,88 @@ public interface LoadOfflineRegionsCallback {
     }
 
     public MapboxManager(String accessToken, Float screenDensity, CordovaWebView webView) {
+        this.accessToken = accessToken;
         this.density = screenDensity;
+        this.webView = webView;
         this.offlineManager = OfflineManager.getInstance(webView.getContext());
         this.offlineManager.setAccessToken(accessToken);
     }
 
+    public void createMap(final JSONObject options, final CallbackContext callback) {
+        try {
+            final long id = ids++;
+            final MapView mapView = createMapView(this.accessToken, options);
+            final Map map = new Map(id, mapView, options);
+
+            mapView.setStyleUrl(MapboxManager.getStyle(options.getString("style")));
+            JSONObject margins = options.isNull("margins") ? null : options.getJSONObject("margins");
+
+            positionMapView(mapView, margins);
+            mapView.getMapAsync(new OnMapReadyCallback() {
+                @Override
+                public void onMapReady(MapboxMap mMap) {
+                    try {
+                        map.setMapboxMap(mMap, options);
+                        maps.put(id, map);
+
+                        JSONObject resp = new JSONObject();
+                        resp.put("id", id);
+                        callback.success(resp);
+                    } catch (JSONException e) {
+                        removeMap(id);
+                        callback.error("Failed to create map: " + e.getMessage());
+                    }
+                }
+            });
+        } catch (JSONException e) {
+            callback.error("Failed to create map: " + e.getMessage());
+        }
+    }
+
+
+    private MapView createMapView(String accessToken, JSONObject options) throws JSONException {
+        MapView mapView = new MapView(this.webView.getContext());
+        mapView.setAccessToken(accessToken);
+
+        // need to do this to register a receiver which onPause later needs
+        mapView.onResume();
+        mapView.onCreate(null);
+
+        return mapView;
+    }
+
+    private void positionMapView(MapView mapView, JSONObject margins) throws JSONException {
+        PositionInfo positionInfo = new PositionInfo(margins);
+        int top = (int) (density * positionInfo.top);
+        int right = (int) (density * positionInfo.right);
+        int bottom = (int) (density * positionInfo.bottom);
+        int left = (int) (density * positionInfo.left);
+        int webViewWidth = webView.getView().getWidth();
+        int webViewHeight = webView.getView().getHeight();
+        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+                webViewWidth - left - right,
+                webViewHeight - top - bottom
+        );
+
+        params.setMargins(left, top, right, bottom);
+        mapView.setLayoutParams(params);
+
+        final FrameLayout layout = (FrameLayout) webView.getView().getParent();
+        layout.addView(mapView);
+    }
+
+    public Collection<Map> maps() {
+        return maps.values();
+    }
+
+    public Map getMap(long id) {
+        return maps.get(id);
+    }
+
+    public void removeMap(long id) {
+        maps.remove(id);
+    }
+
     public void loadOfflineRegions(final LoadOfflineRegionsCallback callback) {
         this.offlineManager.listOfflineRegions(new OfflineManager.ListOfflineRegionsCallback() {
             @Override
@@ -98,6 +208,7 @@ public void onCreate(com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion) {
                     try {
                         OfflineRegion region = createOfflineRegion(offlineRegion);
                         region.setObserver(offlineRegionStatusCallback);
+
                         JSONObject response = region.getMetadata();
                         response.put(JSON_FIELD_ID, region.getId());
                         callback.success(response);
@@ -105,6 +216,8 @@ public void onCreate(com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion) {
                         this.onError(e.getMessage());
                     } catch (UnsupportedEncodingException e) {
                         this.onError(e.getMessage());
+                    } finally {
+                        removeOfflineRegion(offlineRegion.getID());
                     }
                 }
 
@@ -136,7 +249,7 @@ public void removeOfflineRegion(long id) {
     }
 
     private OfflineRegionDefinition createOfflineRegionDefinition(float retinaFactor, JSONObject options) throws JSONException {
-        String styleURL = Mapbox.getStyle(options.getString("style"));
+        String styleURL = MapboxManager.getStyle(options.getString("style"));
         double minZoom = options.getDouble("minZoom");
         double maxZoom = options.getDouble("maxZoom");
         JSONObject boundsOptions = options.getJSONObject("bounds");
@@ -152,4 +265,18 @@ private OfflineRegionDefinition createOfflineRegionDefinition(float retinaFactor
 
         return new OfflineTilePyramidRegionDefinition(styleURL, bounds, minZoom, maxZoom, retinaFactor);
     }
+
+    private class PositionInfo {
+        int top = 0;
+        int right = 0;
+        int bottom = 0;
+        int left = 0;
+
+        public PositionInfo(JSONObject margins) throws JSONException {
+            this.top = margins == null || margins.isNull("top") ? 0 : margins.getInt("top");
+            this.right = margins == null || margins.isNull("right") ? 0 : margins.getInt("right");
+            this.bottom = margins == null || margins.isNull("bottom") ? 0 : margins.getInt("bottom");
+            this.left = margins == null || margins.isNull("left") ? 0 : margins.getInt("left");
+        }
+    }
 }

From 3b4607fda5d84713698d5c5b1f247b7e11fcd1cb Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Sat, 26 Mar 2016 18:23:47 -0700
Subject: [PATCH 29/37] fixing chicken and egg issue with map and mapview
 creation.

---
 src/android/Map.java           | 88 ++++++++++++++++------------------
 src/android/Mapbox.java        |  6 +--
 src/android/MapboxManager.java | 59 +++++++++++------------
 3 files changed, 70 insertions(+), 83 deletions(-)

diff --git a/src/android/Map.java b/src/android/Map.java
index 4b13579..a50dd1f 100644
--- a/src/android/Map.java
+++ b/src/android/Map.java
@@ -1,25 +1,63 @@
 package com.telerik.plugins.mapbox;
 
+import android.support.annotation.Nullable;
+
 import com.mapbox.mapboxsdk.annotations.MarkerOptions;
 import com.mapbox.mapboxsdk.camera.CameraPosition;
 import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
 import com.mapbox.mapboxsdk.geometry.LatLng;
 import com.mapbox.mapboxsdk.maps.MapView;
 import com.mapbox.mapboxsdk.maps.MapboxMap;
-import com.mapbox.mapboxsdk.maps.UiSettings;
+import com.mapbox.mapboxsdk.maps.MapboxMapOptions;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 public class Map {
+    public static MapboxMapOptions createMapboxMapOptions(JSONObject options) throws JSONException {
+        MapboxMapOptions opts = new MapboxMapOptions();
+        opts.styleUrl(MapboxManager.getStyle(options.getString("style")));
+        opts.attributionEnabled(options.isNull("hideAttribution") || !options.getBoolean("hideAttribution"));
+        opts.logoEnabled(options.isNull("hideLogo") || options.getBoolean("hideLogo"));
+        opts.locationEnabled(!options.isNull("showUserLocation") && options.getBoolean("showUserLocation"));
+        opts.camera(Map.getCameraPostion(options, null));
+        opts.compassEnabled(options.isNull("hideCompass") || !options.getBoolean("hideCompass"));
+        opts.rotateGesturesEnabled(options.isNull("disableRotation") || !options.getBoolean("disableRotation"));
+        opts.scrollGesturesEnabled(options.isNull("disableScroll") || !options.getBoolean("disableScroll"));
+        opts.zoomGesturesEnabled(options.isNull("disableZoom") || !options.getBoolean("disableZoom"));
+        opts.tiltGesturesEnabled(options.isNull("disableTilt") || !options.getBoolean("disableTilt"));
+        return opts;
+    }
+
+    public static CameraPosition getCameraPostion(JSONObject options, @Nullable CameraPosition start) throws JSONException {
+        CameraPosition.Builder builder = new CameraPosition.Builder(start);
+
+        if (!options.isNull("zoom")) {
+            builder.zoom(options.getDouble("zoom"));
+        }
+
+        if (!options.isNull("center")) {
+            JSONArray center = options.getJSONArray("center");
+            double lng = center.getDouble(0);
+            double lat = center.getDouble(1);
+            builder.target(new LatLng(lat, lng));
+        }
+
+        // TODO: Bearing
+
+        // TODO: Pitch
+
+        return builder.build();
+    }
+
     private long id;
 
     private MapView mapView;
 
     private MapboxMap mapboxMap;
 
-    public Map(long id, final MapView mapView, final JSONObject options) throws JSONException {
+    public Map(long id, final MapView mapView) {
         this.id = id;
         this.mapView = mapView;
     }
@@ -32,32 +70,8 @@ public MapView getMapView() {
         return this.mapView;
     }
 
-    public void setMapboxMap(MapboxMap mMap, JSONObject options) throws JSONException {
+    public void setMapboxMap(MapboxMap mMap) {
         this.mapboxMap = mMap;
-        UiSettings uiSettings = mMap.getUiSettings();
-        uiSettings.setCompassEnabled(options.isNull("hideCompass") || !options.getBoolean("hideCompass"));
-        uiSettings.setRotateGesturesEnabled(options.isNull("disableRotation") || !options.getBoolean("disableRotation"));
-        uiSettings.setScrollGesturesEnabled(options.isNull("disableScroll") || !options.getBoolean("disableScroll"));
-        uiSettings.setZoomGesturesEnabled(options.isNull("disableZoom") || !options.getBoolean("disableZoom"));
-        uiSettings.setTiltGesturesEnabled(options.isNull("disableTilt") || !options.getBoolean("disableTilt"));
-
-        if (!options.isNull("hideAttribution") && options.getBoolean("hideAttribution")) {
-            uiSettings.setAttributionMargins(-300, 0, 0, 0);
-        }
-
-        if (!options.isNull("hideLogo") && options.getBoolean("hideLogo")) {
-            uiSettings.setLogoMargins(-300, 0, 0, 0);
-        }
-
-        if (!options.isNull("showUserLocation")) {
-            this.showUserLocation(options.getBoolean("showUserLocation"));
-        }
-
-        if (options.has("markers")) {
-            this.addMarkers(options.getJSONArray("markers"));
-        }
-
-        this.jumpTo(options);
     }
 
     public MapboxMap getMapboxMap() {
@@ -98,25 +112,7 @@ public void setZoom(double zoom) {
     }
 
     public void jumpTo(JSONObject options) throws JSONException {
-        CameraPosition current = mapboxMap.getCameraPosition();
-        CameraPosition.Builder builder = new CameraPosition.Builder(current);
-
-        if (!options.isNull("zoom")) {
-            builder.zoom(options.getDouble("zoom"));
-        }
-
-        if (!options.isNull("center")) {
-            JSONArray center = options.getJSONArray("center");
-            double lng = center.getDouble(0);
-            double lat = center.getDouble(1);
-            builder.target(new LatLng(lat, lng));
-        }
-
-        // TODO: Bearing
-
-        // TODO: Pitch
-
-        CameraPosition position = builder.build();
+        CameraPosition position = Map.getCameraPostion(options, mapboxMap.getCameraPosition());
         mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(position));
     }
 
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 8b2b76d..1e14644 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -8,13 +8,9 @@
 import android.support.v4.app.ActivityCompat;
 import android.util.DisplayMetrics;
 import android.util.Log;
-import android.widget.FrameLayout;
 
 import com.mapbox.mapboxsdk.annotations.Marker;
-import com.mapbox.mapboxsdk.constants.Style;
-import com.mapbox.mapboxsdk.maps.MapView;
 import com.mapbox.mapboxsdk.maps.MapboxMap;
-import com.mapbox.mapboxsdk.offline.OfflineManager;
 
 import org.apache.cordova.CallbackContext;
 import org.apache.cordova.CordovaArgs;
@@ -345,7 +341,7 @@ public void run() {
   }
 
   private void createMap(final JSONObject options, final CallbackContext callback) {
-      cordova.getActivity().runOnUiThread(new Runnable() {
+    cordova.getActivity().runOnUiThread(new Runnable() {
       @Override
       public void run() {
         mapboxManager.createMap(options, callback);
diff --git a/src/android/MapboxManager.java b/src/android/MapboxManager.java
index 835da44..8bae306 100644
--- a/src/android/MapboxManager.java
+++ b/src/android/MapboxManager.java
@@ -7,6 +7,7 @@
 import com.mapbox.mapboxsdk.geometry.LatLngBounds;
 import com.mapbox.mapboxsdk.maps.MapView;
 import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.MapboxMapOptions;
 import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
 import com.mapbox.mapboxsdk.offline.OfflineManager;
 import com.mapbox.mapboxsdk.offline.OfflineRegionDefinition;
@@ -87,26 +88,28 @@ public MapboxManager(String accessToken, Float screenDensity, CordovaWebView web
 
     public void createMap(final JSONObject options, final CallbackContext callback) {
         try {
-            final long id = ids++;
-            final MapView mapView = createMapView(this.accessToken, options);
-            final Map map = new Map(id, mapView, options);
+            PositionInfo position = new PositionInfo(options.isNull("margins") ? null : options.getJSONObject("margins"), density);
+            MapView mapView = createMapView(Map.createMapboxMapOptions(options), position);
 
-            mapView.setStyleUrl(MapboxManager.getStyle(options.getString("style")));
-            JSONObject margins = options.isNull("margins") ? null : options.getJSONObject("margins");
-
-            positionMapView(mapView, margins);
+            long id = ids++;
+            final Map map = new Map(id, mapView);
             mapView.getMapAsync(new OnMapReadyCallback() {
                 @Override
                 public void onMapReady(MapboxMap mMap) {
                     try {
-                        map.setMapboxMap(mMap, options);
-                        maps.put(id, map);
+                        map.setMapboxMap(mMap);
+
+                        if (options.has("markers")) {
+                            map.addMarkers(options.getJSONArray("markers"));
+                        }
+
+                        maps.put(map.getId(), map);
 
                         JSONObject resp = new JSONObject();
-                        resp.put("id", id);
+                        resp.put("id", map.getId());
                         callback.success(resp);
                     } catch (JSONException e) {
-                        removeMap(id);
+                        removeMap(map.getId());
                         callback.error("Failed to create map: " + e.getMessage());
                     }
                 }
@@ -116,36 +119,28 @@ public void onMapReady(MapboxMap mMap) {
         }
     }
 
-
-    private MapView createMapView(String accessToken, JSONObject options) throws JSONException {
-        MapView mapView = new MapView(this.webView.getContext());
-        mapView.setAccessToken(accessToken);
+    private MapView createMapView(MapboxMapOptions options, PositionInfo position) throws JSONException {
+        options.accessToken(accessToken);
+        MapView mapView = new MapView(this.webView.getContext(), options);
 
         // need to do this to register a receiver which onPause later needs
         mapView.onResume();
         mapView.onCreate(null);
 
-        return mapView;
-    }
-
-    private void positionMapView(MapView mapView, JSONObject margins) throws JSONException {
-        PositionInfo positionInfo = new PositionInfo(margins);
-        int top = (int) (density * positionInfo.top);
-        int right = (int) (density * positionInfo.right);
-        int bottom = (int) (density * positionInfo.bottom);
-        int left = (int) (density * positionInfo.left);
         int webViewWidth = webView.getView().getWidth();
         int webViewHeight = webView.getView().getHeight();
         FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
-                webViewWidth - left - right,
-                webViewHeight - top - bottom
+                webViewWidth - position.left - position.right,
+                webViewHeight - position.top - position.bottom
         );
 
-        params.setMargins(left, top, right, bottom);
+        params.setMargins(position.left, position.top, position.right, position.bottom);
         mapView.setLayoutParams(params);
 
         final FrameLayout layout = (FrameLayout) webView.getView().getParent();
         layout.addView(mapView);
+
+        return mapView;
     }
 
     public Collection<Map> maps() {
@@ -272,11 +267,11 @@ private class PositionInfo {
         int bottom = 0;
         int left = 0;
 
-        public PositionInfo(JSONObject margins) throws JSONException {
-            this.top = margins == null || margins.isNull("top") ? 0 : margins.getInt("top");
-            this.right = margins == null || margins.isNull("right") ? 0 : margins.getInt("right");
-            this.bottom = margins == null || margins.isNull("bottom") ? 0 : margins.getInt("bottom");
-            this.left = margins == null || margins.isNull("left") ? 0 : margins.getInt("left");
+        public PositionInfo(JSONObject margins, float density) throws JSONException {
+            this.top = margins == null || margins.isNull("top") ? 0 : (int) (density * margins.getInt("top"));
+            this.right = margins == null || margins.isNull("right") ? 0 : (int) (density * margins.getInt("right"));
+            this.bottom = margins == null || margins.isNull("bottom") ? 0 : (int) (density * margins.getInt("bottom"));
+            this.left = margins == null || margins.isNull("left") ? 0 : (int) (density * margins.getInt("left"));
         }
     }
 }

From d9031e168512fd5d8547b65b33769258c3592062 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Sat, 26 Mar 2016 19:50:27 -0700
Subject: [PATCH 30/37] Added initial geojson support with js api that matches
 mapbox-gl-js.

---
 plugin.xml                      |   2 +
 src/android/FeatureManager.java | 321 ++++++++++++++++++++++++++++++++
 src/android/Map.java            |  63 +++++++
 src/android/Mapbox.java         |  36 +++-
 src/android/MapboxManager.java  |   1 +
 src/android/mapbox.gradle       |   1 +
 www/map-instance.js             | 114 ++++++++++--
 7 files changed, 518 insertions(+), 20 deletions(-)
 create mode 100644 src/android/FeatureManager.java

diff --git a/plugin.xml b/plugin.xml
index 3d0516e..035c608 100755
--- a/plugin.xml
+++ b/plugin.xml
@@ -56,10 +56,12 @@
     </config-file>
 
     <framework src="src/android/mapbox.gradle" custom="true" type="gradleReference"/>
+    <framework src="com.android.support:appcompat-v7:22.+" />
     <source-file src="src/android/Mapbox.java" target-dir="src/com/telerik/plugins/mapbox"/>
     <source-file src="src/android/MapboxManager.java" target-dir="src/com/telerik/plugins/mapbox"/>
     <source-file src="src/android/OfflineRegion.java" target-dir="src/com/telerik/plugins/mapbox"/>
     <source-file src="src/android/Map.java" target-dir="src/com/telerik/plugins/mapbox"/>
+    <source-file src="src/android/FeatureManager.java" target-dir="src/com/telerik/plugins/mapbox"/>
 
     <!-- This leads to trouble in AppBuilder when compiling for Cordova-Android 4 -->
     <!--source-file src="src/android/res/values/mapboxstrings.xml" target-dir="res/values" />
diff --git a/src/android/FeatureManager.java b/src/android/FeatureManager.java
new file mode 100644
index 0000000..e1b9575
--- /dev/null
+++ b/src/android/FeatureManager.java
@@ -0,0 +1,321 @@
+package com.telerik.plugins.mapbox;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.DisplayMetrics;
+import android.util.Log;
+
+import com.cocoahero.android.geojson.Feature;
+import com.cocoahero.android.geojson.FeatureCollection;
+import com.cocoahero.android.geojson.GeoJSON;
+import com.cocoahero.android.geojson.GeoJSONObject;
+import com.cocoahero.android.geojson.Geometry;
+import com.cocoahero.android.geojson.GeometryCollection;
+import com.cocoahero.android.geojson.LineString;
+import com.cocoahero.android.geojson.MultiLineString;
+import com.cocoahero.android.geojson.MultiPoint;
+import com.cocoahero.android.geojson.MultiPolygon;
+import com.cocoahero.android.geojson.Point;
+import com.cocoahero.android.geojson.Polygon;
+import com.cocoahero.android.geojson.Position;
+import com.cocoahero.android.geojson.Ring;
+import com.mapbox.mapboxsdk.annotations.Icon;
+import com.mapbox.mapboxsdk.annotations.IconFactory;
+import com.mapbox.mapboxsdk.annotations.Marker;
+import com.mapbox.mapboxsdk.annotations.MarkerOptions;
+import com.mapbox.mapboxsdk.annotations.PolygonOptions;
+import com.mapbox.mapboxsdk.annotations.PolylineOptions;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+interface DataSource {
+    List<Feature> getFills();
+    List<Feature> getLines();
+    List<Feature> getSymbols();
+}
+
+class FeatureManager {
+    private String TAG = "FeatureManager";
+
+    protected Context ctx;
+
+    protected IconFactory iconFactory;
+
+    protected MapboxMap mapboxMap;
+
+    protected HashMap<String, DataSource> sources = new HashMap<String, DataSource>();
+
+    protected HashMap<Long, Feature> markerIndex = new HashMap<Long, Feature>();
+
+    public FeatureManager(Context ctx, MapboxMap mapboxMap) {
+        this.ctx = ctx;
+        this.mapboxMap = mapboxMap;
+        this.iconFactory = IconFactory.getInstance(ctx);
+    }
+
+    public boolean hasSource(String name) {
+        return this.sources.containsKey(name);
+    }
+
+    public boolean hasMarkerFeature(Long id) {
+        return this.markerIndex.containsKey(id);
+    }
+
+    public Feature getMarkerFeature(Long id) {
+        if (this.hasMarkerFeature(id)) {
+            return this.markerIndex.get(id);
+        } else {
+            return null;
+        }
+    }
+
+    public Feature getMarkerFeature(Marker marker) {
+        return this.getMarkerFeature(marker.getId());
+    }
+
+    public void addGeoJSONSource(String name, String json) throws JSONException {
+        this.sources.put(name, new GeoJSONSource(name).addGeoJSON(json));
+    }
+
+    public void addGeoJSONSource(String name, JSONObject json) throws JSONException {
+        this.sources.put(name, new GeoJSONSource(name).addGeoJSON(json));
+    }
+
+    public void addFillLayer(String id, String source, JSONObject layer) {
+        for (Feature feature : this.sources.get(source).getFills()) {
+            ArrayList<LatLng> latLngs = new ArrayList<LatLng>();
+            for (Ring ring : ((Polygon) feature.getGeometry()).getRings()) {
+                for (Position position : ring.getPositions()) {
+                    latLngs.add(new LatLng(position.getLatitude(), position.getLongitude()));
+                }
+            }
+            LatLng[] points = latLngs.toArray(new LatLng[latLngs.size()]);
+
+            PolygonOptions polygon = new PolygonOptions()
+                    // TODO: Need to use values in layer to set options.
+                    .add(points);
+
+            this.mapboxMap.addPolygon(polygon);
+        }
+    }
+
+    public void addLineLayer(String id, String source, JSONObject layer) {
+        for (Feature feature : this.sources.get(source).getLines()) {
+            ArrayList<LatLng> latLngs = new ArrayList<LatLng>();
+            for (Position position : ((LineString) feature.getGeometry()).getPositions()) {
+                latLngs.add(new LatLng(position.getLatitude(), position.getLongitude()));
+            }
+            LatLng[] points = latLngs.toArray(new LatLng[latLngs.size()]);
+
+            PolylineOptions line = new PolylineOptions()
+                    // TODO: Need to use values in layer to set options.
+                    .add(points);
+
+            this.mapboxMap.addPolyline(line);
+        }
+    }
+
+    public void addMarkerLayer(String id, String source, JSONObject layer) {
+        List<Feature> features = this.sources.get(source).getSymbols();
+
+        for (Feature feature : features) {
+            MarkerOptions options = this.createMarker(feature, layer);
+            Marker marker = this.mapboxMap.addMarker(options);
+            this.markerIndex.put(marker.getId(), feature);
+        }
+    }
+
+    protected MarkerOptions createMarker(Feature feature, JSONObject style) {
+        final JSONObject properties = feature.getProperties();
+        final Position p = ((Point) feature.getGeometry()).getPosition();
+        final MarkerOptions marker = new MarkerOptions()
+            .position(new LatLng(p.getLatitude(), p.getLongitude()));
+
+        try {
+            final String textField = style.getJSONObject("layout").getString("text-field");
+            marker.title(textField.replace("{title}", properties.getString("title")));
+        } catch (JSONException e) {
+            Log.w(TAG, "Error parsing Style JSON properties: " + e.getMessage());
+        }
+
+        try {
+            marker.snippet(properties.getString("description"));
+        } catch (JSONException e) {
+            Log.w(TAG, "Error parsing Style JSON properties: " + e.getMessage());
+        }
+
+        try {
+            final String iconImage = style.getJSONObject("layout").getString("icon-image");
+            final String markerSymbol = properties.getString("marker-symbol");
+            final URI uri = new URI(iconImage.replace("{marker-symbol}", markerSymbol));
+            final Icon icon = this.loadIcon(uri);
+            if (icon != null) {
+                marker.icon(icon);
+            }
+        } catch (JSONException e) {
+            Log.w(TAG, "Error parsing Style JSON properties: " + e.getMessage());
+        } catch (URISyntaxException e) {
+            Log.w(TAG, "Invalid icon-image URI: " + e.getMessage());
+        } catch (IOException e) {
+            Log.w(TAG, "Error loading file: " + e.getMessage());
+        }
+
+        return marker;
+    }
+
+    protected Icon loadIcon(URI uri) throws IOException {
+        Icon icon;
+
+        if (uri.getScheme().equals("asset")) {
+            // Stripping leading '/'.
+            String path = uri.getPath().substring(1);
+            icon = iconFactory.fromBitmap(this.loadScaledBitmap(path));
+        }
+        else {
+            icon = iconFactory.fromPath(uri.getPath());
+        }
+
+        return icon;
+    }
+
+    protected Bitmap loadScaledBitmap(String path) throws IOException {
+        AssetManager am = ctx.getAssets();
+        InputStream image = am.open(path);
+        Bitmap bmp = BitmapFactory.decodeStream(image);
+        DisplayMetrics dm = ctx.getResources().getDisplayMetrics();
+        bmp.setDensity(dm.densityDpi);
+        return bmp;
+    }
+
+    private class GeoJSONSource implements DataSource {
+        private String TAG = "GeoJSONSource";
+
+        protected String name;
+
+        protected ArrayList<Feature> polygons = new ArrayList<Feature>();
+        protected ArrayList<Feature> polylines = new ArrayList<Feature>();
+        protected ArrayList<Feature> markers = new ArrayList<Feature>();
+
+        public GeoJSONSource(String name) {
+            this.name = name;
+        }
+
+        @Override
+        public List<Feature> getFills() {
+            return polygons;
+        }
+
+        @Override
+        public List<Feature> getLines() {
+            return polylines;
+        }
+
+        @Override
+        public List<Feature> getSymbols() {
+            return markers;
+        }
+
+        public GeoJSONSource addGeoJSON(String json) throws JSONException {
+            return this.addGeoJSON(GeoJSON.parse(json));
+        }
+
+        public GeoJSONSource addGeoJSON(JSONObject json) throws JSONException {
+            return this.addGeoJSON(GeoJSON.parse(json));
+        }
+
+        public GeoJSONSource addGeoJSON(GeoJSONObject geojson) {
+            if (geojson instanceof FeatureCollection) {
+                FeatureCollection fc = (FeatureCollection) geojson;
+                for (Feature f : fc.getFeatures()) {
+                    this.addGeoJSON(f);
+                }
+            } else if (geojson instanceof Feature) {
+                Feature feature = (Feature) geojson;
+                this.addFeature(feature);
+            } else {
+                Log.e(TAG, "GeoJSON must be FeatureCollection or Feature.");
+            }
+
+            return this;
+        }
+
+        /**
+         * TODO: Handling of Complex geometries (GeometryCollection & Multi*) needs improvement when Mapbox SDK supports them better.
+         * TODO: Recursive processing of GeoJSON could probably be optimized.
+         *
+         * @param feature
+         */
+        public void addFeature(Feature feature) {
+            Geometry geom = feature.getGeometry();
+
+            if (geom instanceof GeometryCollection) {
+                GeometryCollection gc = (GeometryCollection) geom;
+                for (Geometry g : gc.getGeometries()) {
+                    this.addFeature(feature, g);
+                }
+            }
+            else if (geom instanceof MultiPolygon) {
+                MultiPolygon multiPoly = (MultiPolygon) geom;
+                for (Polygon poly : multiPoly.getPolygons()) {
+                    this.addFeature(feature, poly);
+                }
+            }
+            else if (geom instanceof MultiLineString) {
+                MultiLineString multiLine = (MultiLineString) geom;
+                for (LineString ls : multiLine.getLineStrings()) {
+                    this.addFeature(feature, ls);
+                }
+            }
+            else if (geom instanceof MultiPoint) {
+                MultiPoint multiPoint = (MultiPoint) geom;
+                for (Position p : multiPoint.getPositions()) {
+                    this.addFeature(feature, new Point(p));
+                }
+            }
+            else {
+                this.addFeature(feature, geom);
+            }
+        }
+
+        protected void addFeature(Feature feature, Geometry geom) {
+            if (geom instanceof MultiPolygon || geom instanceof MultiLineString || geom instanceof MultiPoint) {
+                feature.setGeometry(geom);
+                Feature f = new Feature(geom);
+                f.setProperties(feature.getProperties());
+                this.addFeature(f);
+            }
+            else if (geom instanceof Polygon) {
+                this.polygons.add(this.createFeature(feature.getProperties(), geom));
+            }
+            else if (geom instanceof LineString) {
+                this.polylines.add(this.createFeature(feature.getProperties(), geom));
+            }
+            else if (geom instanceof Point) {
+                this.markers.add(this.createFeature(feature.getProperties(), geom));
+            } else {
+                // Unsupported geometry type.
+                Log.e(TAG, String.format("Unsupported GeoJSON geometry type: %s.", geom.getType()));
+            }
+        }
+
+        protected Feature createFeature(JSONObject properties, Geometry geom) {
+            Feature f = new Feature();
+            f.setGeometry(geom);
+            f.setProperties(properties);
+            return f;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/android/Map.java b/src/android/Map.java
index a50dd1f..a1d8434 100644
--- a/src/android/Map.java
+++ b/src/android/Map.java
@@ -57,6 +57,8 @@ public static CameraPosition getCameraPostion(JSONObject options, @Nullable Came
 
     private MapboxMap mapboxMap;
 
+    private FeatureManager features;
+
     public Map(long id, final MapView mapView) {
         this.id = id;
         this.mapView = mapView;
@@ -74,6 +76,10 @@ public void setMapboxMap(MapboxMap mMap) {
         this.mapboxMap = mMap;
     }
 
+    public void setFeatureManager(FeatureManager featureManager) {
+        this.features = featureManager;
+    }
+
     public MapboxMap getMapboxMap() {
         return this.mapboxMap;
     }
@@ -136,4 +142,61 @@ public void addMarkerListener(MapboxMap.OnInfoWindowClickListener listener) {
     public void showUserLocation(boolean enabled) {
         mapboxMap.setMyLocationEnabled(enabled);
     }
+
+    public void addSource(String name, JSONObject source) throws UnsupportedTypeException, JSONException {
+        final String sourceType = source.getString("type");
+
+        if (sourceType.equals("geojson") && source.has("data")) {
+            final JSONObject data = source.getJSONObject("data");
+            features.addGeoJSONSource(name, data);
+        } else {
+            throw new UnsupportedTypeException("source:" + sourceType);
+        }
+    }
+
+    public void addLayer(JSONObject layer) throws UnknownSourceException, UnsupportedTypeException, JSONException {
+        final String layerType = layer.getString("type");
+        final String source = layer.getString("source");
+        final String id = layer.getString("id");
+
+        if (features.hasSource(source)) {
+            if (layerType.equals("fill")) {
+                features.addFillLayer(id, source, layer);
+            } else if (layerType.equals("line")) {
+                features.addLineLayer(id, source, layer);
+            } else if (layerType.equals("symbol")) {
+                features.addMarkerLayer(id, source, layer);
+            } else {
+                throw new UnsupportedTypeException("layer:" + layerType);
+            }
+        } else {
+            throw new UnknownSourceException(source);
+        }
+    }
 }
+
+class UnsupportedTypeException extends Exception {
+    String type;
+
+    public UnsupportedTypeException(String type) {
+        this.type = type;
+    }
+
+    @Override
+    public String getMessage() {
+        return "Unsupported type: " + this.type;
+    }
+}
+
+class UnknownSourceException extends Exception {
+    String source;
+
+    public UnknownSourceException(String source) {
+        this.source = source;
+    }
+
+    @Override
+    public String getMessage() {
+        return "Unknown source: " + this.source;
+    }
+}
\ No newline at end of file
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 1e14644..75460c8 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -51,6 +51,8 @@ public class Mapbox extends CordovaPlugin {
   private static final String ACTION_ADD_MARKER_CALLBACK = "addMarkerCallback";
   private static final String ACTION_ADD_POLYGON = "addPolygon";
   private static final String ACTION_ADD_GEOJSON = "addGeoJSON";
+  private static final String ACTION_ADD_SOURCE = "addSource";
+  private static final String ACTION_ADD_LAYER = "addLayer";
   private static final String ACTION_GET_ZOOMLEVEL = "getZoomLevel";
   private static final String ACTION_SET_ZOOMLEVEL = "setZoomLevel";
   private static final String ACTION_GET_CENTER = "getCenter";
@@ -69,7 +71,7 @@ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
 
   @Override
   public boolean execute(final String action, final CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
-    Command command = Command.create(action, args, callbackContext);
+  Command command = Command.create(action, args, callbackContext);
     return execute(command);
   }
 
@@ -95,6 +97,7 @@ else if (ACTION_SHOW_USER_LOCATION.equals(action)) {
           @Override
           public void run() {
             map.showUserLocation(enabled);
+            callbackContext.success();
           }
         });
       }
@@ -191,6 +194,36 @@ public boolean onInfoWindowClick(Marker marker) {
       );
     }
 
+    else if (ACTION_ADD_SOURCE.equals(action)) {
+      final long mapId = args.getLong(0);
+      final String name = args.getString(1);
+      final JSONObject source = args.getJSONObject(2);
+      final Map map = mapboxManager.getMap(mapId);
+      try {
+        map.addSource(name, source);
+        callbackContext.success();
+      } catch (Exception e) {
+        callbackContext.error("Unable to add data source to map: " + e.getMessage());
+      }
+    }
+
+    else if (ACTION_ADD_LAYER.equals(action)) {
+      final long mapId = args.getLong(0);
+      final JSONObject layer = args.getJSONObject(1);
+      final Map map = mapboxManager.getMap(mapId);
+      cordova.getActivity().runOnUiThread(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            map.addLayer(layer);
+            callbackContext.success();
+          } catch (Exception e) {
+            callbackContext.error("Unable to add layer to map: " + e.getMessage());
+          }
+        }
+      });
+    }
+
     else if (ACTION_LIST_OFFLINE_REGIONS.equals(action)) {
       this.listOfflineRegions(callbackContext);
     }
@@ -442,7 +475,6 @@ public void onStart() {
     }
   }
 
-  @Override
   public void onResume(boolean multitasking) {
     for (Map map : mapboxManager.maps()) {
       map.getMapView().onResume();
diff --git a/src/android/MapboxManager.java b/src/android/MapboxManager.java
index 8bae306..0d4d187 100644
--- a/src/android/MapboxManager.java
+++ b/src/android/MapboxManager.java
@@ -98,6 +98,7 @@ public void createMap(final JSONObject options, final CallbackContext callback)
                 public void onMapReady(MapboxMap mMap) {
                     try {
                         map.setMapboxMap(mMap);
+                        map.setFeatureManager(new FeatureManager(webView.getContext(), mMap));
 
                         if (options.has("markers")) {
                             map.addMarkers(options.getJSONArray("markers"));
diff --git a/src/android/mapbox.gradle b/src/android/mapbox.gradle
index 4580af0..f67c263 100644
--- a/src/android/mapbox.gradle
+++ b/src/android/mapbox.gradle
@@ -5,6 +5,7 @@ repositories {
 }
 
 dependencies {
+    compile 'com.cocoahero.android:geojson:1.0.1@jar'
     compile 'com.android.support:appcompat-v7:23.0.1'
     compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:4.0.0-rc.1@aar'){
         transitive=true
diff --git a/www/map-instance.js b/www/map-instance.js
index 75c2074..95daa93 100644
--- a/www/map-instance.js
+++ b/www/map-instance.js
@@ -40,44 +40,122 @@ MapInstance.prototype._execAfterLoad = function () {
     }.bind(this));
 };
 
-MapInstance.prototype.jumpTo = function (options, successCallback, errorCallback) {
-    this._execAfterLoad(successCallback, errorCallback, "jumpTo", [options]);
+MapInstance.prototype.jumpTo = function (options, callback) {
+    var result = wrapCallback(callback);
+    this._execAfterLoad(
+        result.success,
+        result.error,
+        "jumpTo",
+        [options]
+    );
 };
 
-MapInstance.prototype.setCenter = function (options, successCallback, errorCallback) {
-    this._execAfterLoad(successCallback, errorCallback, "setCenter", [options]);
+MapInstance.prototype.setCenter = function (options, callback) {
+    var result = wrapCallback(callback);
+    this._execAfterLoad(
+        result.success,
+        result.error,
+        "setCenter",
+        [options]
+    );
 };
 
-MapInstance.prototype.getCenter = function (successCallback, errorCallback) {
-    this._execAfterLoad(successCallback, errorCallback, "getCenter");
+MapInstance.prototype.getCenter = function (callback) {
+    var result = wrapCallback(callback);
+    this._execAfterLoad(
+        result.success,
+        result.error,
+        "getCenter"
+    );
 };
 
-MapInstance.prototype.addMarkers = function (options, successCallback, errorCallback) {
-    this._execAfterLoad(successCallback, errorCallback, "addMarkers", [options]);
+MapInstance.prototype.addMarkers = function (options, callback) {
+    var result = wrapCallback(callback);
+    this._execAfterLoad(
+        result.success,
+        result.error,
+        "addMarkers",
+        [options]
+    );
 };
 
 MapInstance.prototype.addMarkerCallback = function (callback) {
     this._execAfterLoad(callback, null, "addMarkerCallback");
 };
 
-MapInstance.prototype.setCenter = function (center, successCallback, errorCallback) {
-    this._execAfterLoad(successCallback, errorCallback, "setCenter", [center]);
+MapInstance.prototype.setCenter = function (center, callback) {
+    var result = wrapCallback(callback);
+    this._execAfterLoad(
+        result.success,
+        result.error,
+        "setCenter",
+        [center]
+    );
 };
 
-MapInstance.prototype.getCenter = function (successCallback, errorCallback) {
-    this._execAfterLoad(successCallback, errorCallback, "getCenter");
+MapInstance.prototype.getCenter = function (callback) {
+    var result = wrapCallback(callback);
+    this._execAfterLoad(
+        result.success,
+        result.error,
+        "getCenter"
+    );
 };
 
-MapInstance.prototype.getZoomLevel = function (successCallback, errorCallback) {
-    this._execAfterLoad(successCallback, errorCallback, "getZoomLevel");
+MapInstance.prototype.getZoomLevel = function (callback) {
+    var result = wrapCallback(callback);
+    this._execAfterLoad(
+        result.success,
+        result.error,
+        "getZoomLevel"
+    );
 };
 
-MapInstance.prototype.setZoomLevel = function (zoom, successCallback, errorCallback) {
-    this._execAfterLoad(successCallback, errorCallback, "setZoomLevel", [zoom]);
+MapInstance.prototype.setZoomLevel = function (zoom, callback) {
+    var result = wrapCallback(callback);
+    this._execAfterLoad(
+        result.success,
+        result.error,
+        "setZoomLevel",
+        [zoom]
+    );
 };
 
-MapInstance.prototype.showUserLocation = function (enabled, successCallback, errorCallback) {
-    this._execAfterLoad(successCallback, errorCallback, "showUserLocation", [enabled]);
+MapInstance.prototype.showUserLocation = function (enabled, callback) {
+    var result = wrapCallback(callback);
+    this._execAfterLoad(
+        result.success,
+        result.error,
+        "showUserLocation",
+        [enabled]
+    );
 };
 
+MapInstance.prototype.addSource = function (name, source, callback) {
+    var result = wrapCallback(callback);
+    this._execAfterLoad(
+        result.success,
+        result.error,
+        "addSource",
+        [name, source]
+    );
+};
+
+MapInstance.prototype.addLayer = function (layer, callback) {
+    var result = wrapCallback(callback);
+    this._execAfterLoad(
+        result.success,
+        result.error,
+        "addLayer",
+        [layer]
+    );
+};
+
+function wrapCallback(callback) {
+    return {
+        success: function (response) { callback(null, response); },
+        error: function (err) { callback(err); }
+    };
+}
+
 module.exports = MapInstance;

From b8561db9d74a693257cd59900a7d59d6fd82b9a4 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Sun, 27 Mar 2016 01:08:05 -0700
Subject: [PATCH 31/37] Added promise support to API.

---
 plugin.xml                     |   1 +
 src/android/Mapbox.java        |  19 +++--
 www/events-mixin.js            |  96 +++++++++++------------
 www/map-instance.js            | 137 ++++++--------------------------
 www/mapbox-plugin-api-mixin.js |  57 ++++++++++++++
 www/offline-region.js          | 138 +++++++++++++--------------------
 6 files changed, 195 insertions(+), 253 deletions(-)
 create mode 100644 www/mapbox-plugin-api-mixin.js

diff --git a/plugin.xml b/plugin.xml
index 035c608..c132f98 100755
--- a/plugin.xml
+++ b/plugin.xml
@@ -25,6 +25,7 @@
   </engines>
 
   <js-module src="www/mixin.js" name="mixin" />
+  <js-module src="www/mapbox-plugin-api-mixin.js" name="mapbox-plugin-api-mixin" />
   <js-module src="www/events-mixin.js" name="events-mixin" />
   <js-module src="www/map-instance.js" name="map-instance" />
   <js-module src="www/offline-region.js" name="offline-region" />
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 75460c8..212f4c6 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -170,28 +170,31 @@ else if (ACTION_ADD_MARKERS.equals(action)) {
 
     else if (ACTION_ADD_MARKER_CALLBACK.equals(action)) {
       final long mapId = args.getLong(0);
+      final CallbackContext markerCallback = new CallbackContext(args.getString(1), this.webView);
       final Map map = mapboxManager.getMap(mapId);
       map.addMarkerListener(
         new MapboxMap.OnInfoWindowClickListener() {
           @Override
           public boolean onInfoWindowClick(Marker marker) {
             try {
-              callbackContext.success(
-                new JSONObject()
-                        .put("title", marker.getTitle())
-                        .put("subtitle", marker.getSnippet())
-                        .put("lat", marker.getPosition().getLatitude())
-                        .put("lng", marker.getPosition().getLongitude())
-              );
+              JSONObject markerInfo = new JSONObject()
+                      .put("title", marker.getTitle())
+                      .put("subtitle", marker.getSnippet())
+                      .put("lat", marker.getPosition().getLatitude())
+                      .put("lng", marker.getPosition().getLongitude());
+              PluginResult result = new PluginResult(PluginResult.Status.OK, markerInfo);
+              result.setKeepCallback(true);
+              markerCallback.sendPluginResult(result);
               return true;
             } catch (JSONException e) {
               String message = "Error in callback of " + ACTION_ADD_MARKER_CALLBACK + ": " + e.getMessage();
-              callbackContext.error(message);
+              markerCallback.error(message);
               return false;
             }
           }
         }
       );
+      callbackContext.success();
     }
 
     else if (ACTION_ADD_SOURCE.equals(action)) {
diff --git a/www/events-mixin.js b/www/events-mixin.js
index 09f0e32..585a9c2 100644
--- a/www/events-mixin.js
+++ b/www/events-mixin.js
@@ -2,55 +2,53 @@ var Mixin = require('./mixin'),
     channel = require("cordova/channel"),
     channelIds = 0;
 
-module.exports = Mixin({
-    initEvents: function (prefix) {
-        if (!this._channelPrefix) {
-            this._channelPrefix = prefix + "." + (channelIds++);
-        }
-    },
+module.exports = function (prefix, target) {
+    var _channelPrefix = prefix + "." + (channelIds++);
 
-    _prefix: function (type) {
-        return this._channelPrefix + "." + type;
-    },
+    function _prefix(type) {
+        return _channelPrefix + "." + type;
+    }
 
-    _channel: function (type, sticky) {
-        var t = this._prefix(type);
-        if (!this._channels) {
-            this._channels = {};
+    return Mixin({
+        _channel: function (type, sticky) {
+            var t = _prefix(type);
+            if (!this._channels) {
+                this._channels = {};
+            }
+            if (sticky !== undefined) {
+                this._channels[t] = sticky ?
+                    channel.createSticky(t) :
+                    channel.create(t);
+            }
+            return this._channels[t];
+        },
+
+        createChannel: function (type) {
+            this._channel(type, false);
+        },
+
+        createStickyChannel: function (type) {
+            this._channel(type, true);
+        },
+
+        once: function (type, listener) {
+            var onEvent = function (e) {
+                    listener(e);
+                    this.off(type, onEvent);
+                };
+            this.on(type, onEvent.bind(this));
+        },
+
+        on: function (type, listener) {
+            this._channel(type).subscribe(listener);
+        },
+
+        off: function (type, listener) {
+            this._channel(type).unsubscribe(listener);
+        },
+
+        fire: function (type, e) {
+            this._channel(type).fire(e);
         }
-        if (sticky !== undefined) {
-            this._channels[t] = sticky ?
-                channel.createSticky(t) :
-                channel.create(t);
-        }
-        return this._channels[t];
-    },
-
-    createChannel: function (type) {
-        this._channel(type, false);
-    },
-
-    createStickyChannel: function (type) {
-        this._channel(type, true);
-    },
-
-    once: function (type, listener) {
-        var onEvent = function (e) {
-                listener(e);
-                this.off(type, onEvent);
-            };
-        this.on(type, onEvent.bind(this));
-    },
-
-    on: function (type, listener) {
-        this._channel(type).subscribe(listener);
-    },
-
-    off: function (type, listener) {
-        this._channel(type).unsubscribe(listener);
-    },
-
-    fire: function (type, e) {
-        this._channel(type).fire(e);
-    }
-});
+    })(target);
+};
diff --git a/www/map-instance.js b/www/map-instance.js
index 95daa93..f2aab1a 100644
--- a/www/map-instance.js
+++ b/www/map-instance.js
@@ -1,161 +1,72 @@
 var exec = require("cordova/exec"),
+    MapboxPluginAPI = require("./mapbox-plugin-api-mixin"),
     EventsMixin = require("./events-mixin");
 
 function MapInstance(options) {
-    var onLoad = _onLoad.bind(this),
+    var onLoad = this._onLoad.bind(this),
         onError = this._error.bind(this);
 
-    this._error = onError;
-
-    this.initEvents("Mapbox.MapInstance");
     this.createStickyChannel("load");
 
-    exec(onLoad, this._error, "Mapbox", "createMap", [options]);
-
-    function _onLoad(resp) {
-        this._id = resp.id;
-        this.loaded = true;
-
-        this.fire("load", {map: this});
-    }
+    exec(onLoad, onError, "Mapbox", "createMap", [options]);
 }
 
-EventsMixin(MapInstance.prototype);
-
-MapInstance.prototype._error = function (err) {
-    var error = new Error("Map error (ID: " + this._id + "): " + err);
-    console.warn("throwing MapError: ", error);
-    throw error;
-};
-
-MapInstance.prototype._exec = function (successCallback, errorCallback, method, args) {
-    args = [this._id].concat(args || []);
-    exec(successCallback, errorCallback, "Mapbox", method, args);
-};
-
-MapInstance.prototype._execAfterLoad = function () {
-    var args = arguments;
-    this.once('load', function (map) {
-        this._exec.apply(this, args);
-    }.bind(this));
-};
+MapboxPluginAPI('MapInstance', MapInstance.prototype);
+EventsMixin('MapInstance', MapInstance.prototype);
 
 MapInstance.prototype.jumpTo = function (options, callback) {
-    var result = wrapCallback(callback);
-    this._execAfterLoad(
-        result.success,
-        result.error,
-        "jumpTo",
-        [options]
-    );
+    return this._execAfterLoad(callback, "jumpTo", [options]);
 };
 
 MapInstance.prototype.setCenter = function (options, callback) {
-    var result = wrapCallback(callback);
-    this._execAfterLoad(
-        result.success,
-        result.error,
-        "setCenter",
-        [options]
-    );
+    return this._execAfterLoad(callback, "setCenter", [options]);
 };
 
 MapInstance.prototype.getCenter = function (callback) {
-    var result = wrapCallback(callback);
-    this._execAfterLoad(
-        result.success,
-        result.error,
-        "getCenter"
-    );
+    return this._execAfterLoad(callback, "getCenter");
 };
 
 MapInstance.prototype.addMarkers = function (options, callback) {
-    var result = wrapCallback(callback);
-    this._execAfterLoad(
-        result.success,
-        result.error,
-        "addMarkers",
-        [options]
-    );
+    return this._execAfterLoad(callback, "addMarkers", [options]);
 };
 
-MapInstance.prototype.addMarkerCallback = function (callback) {
-    this._execAfterLoad(callback, null, "addMarkerCallback");
+MapInstance.prototype.addMarkerCallback = function (markerCallback, callback) {
+    return this._execAfterLoad(callback, "addMarkerCallback", [markerCallback]);
 };
 
 MapInstance.prototype.setCenter = function (center, callback) {
-    var result = wrapCallback(callback);
-    this._execAfterLoad(
-        result.success,
-        result.error,
-        "setCenter",
-        [center]
-    );
+    return this._execAfterLoad(callback, "setCenter", [center]);
 };
 
 MapInstance.prototype.getCenter = function (callback) {
-    var result = wrapCallback(callback);
-    this._execAfterLoad(
-        result.success,
-        result.error,
-        "getCenter"
-    );
+    return this._execAfterLoad(callback, "getCenter");
 };
 
 MapInstance.prototype.getZoomLevel = function (callback) {
-    var result = wrapCallback(callback);
-    this._execAfterLoad(
-        result.success,
-        result.error,
-        "getZoomLevel"
-    );
+    return this._execAfterLoad(callback, "getZoomLevel");
 };
 
 MapInstance.prototype.setZoomLevel = function (zoom, callback) {
-    var result = wrapCallback(callback);
-    this._execAfterLoad(
-        result.success,
-        result.error,
-        "setZoomLevel",
-        [zoom]
-    );
+    return this._execAfterLoad(callback, "setZoomLevel", [zoom]);
 };
 
 MapInstance.prototype.showUserLocation = function (enabled, callback) {
-    var result = wrapCallback(callback);
-    this._execAfterLoad(
-        result.success,
-        result.error,
-        "showUserLocation",
-        [enabled]
-    );
+    return this._execAfterLoad(callback, "showUserLocation", [enabled]);
 };
 
 MapInstance.prototype.addSource = function (name, source, callback) {
-    var result = wrapCallback(callback);
-    this._execAfterLoad(
-        result.success,
-        result.error,
-        "addSource",
-        [name, source]
-    );
+    return this._execAfterLoad(callback, "addSource", [name, source]);
 };
 
 MapInstance.prototype.addLayer = function (layer, callback) {
-    var result = wrapCallback(callback);
-    this._execAfterLoad(
-        result.success,
-        result.error,
-        "addLayer",
-        [layer]
-    );
+    return this._execAfterLoad(callback, "addLayer", [layer]);
 };
 
-function wrapCallback(callback) {
-    return {
-        success: function (response) { callback(null, response); },
-        error: function (err) { callback(err); }
-    };
-}
+MapInstance.prototype._onLoad = function (resp) {
+    this._id = resp.id;
+    this.loaded = true;
+
+    this.fire("load", {map: this});
+};
 
 module.exports = MapInstance;
diff --git a/www/mapbox-plugin-api-mixin.js b/www/mapbox-plugin-api-mixin.js
new file mode 100644
index 0000000..46b38fd
--- /dev/null
+++ b/www/mapbox-plugin-api-mixin.js
@@ -0,0 +1,57 @@
+var cordova = require("cordova"),
+    exec = require("cordova/exec"),
+    Mixin = require('./mixin');
+
+module.exports = function (type, target) {
+    var _mapboxType = type;
+
+    return Mixin({
+        _registerCallback: function (name, success, fail) {
+            var callbackId = ["Mapbox", type, name, cordova.callbackId++].join('.');
+
+            success = success ||  function () { console.log(callbackId + "() success!", arguments); };
+            fail = fail ||  function () { console.log(callbackId + "() fail :(", arguments); };
+
+            cordova.callbacks[callbackId] = {success: success, fail: fail};
+            return callbackId;
+        },
+
+        _error: function (err) {
+            var error = new Error("MapboxPlugin error (" + _mapboxType + ":" + this._id + "): " + err);
+            console.warn("throwing MapboxPluginError: ", error);
+            throw error;
+        },
+
+        _execAfterLoad: function () {
+            var args = Array.prototype.slice.call(arguments),
+                once = this.once.bind(this),
+                onLoad = function () {
+                    return this._exec.apply(this, args);
+                }.bind(this);
+
+            return new Promise(function (resolve, reject) {
+                once('load', function (obj) {
+                    onLoad().then(resolve, reject);
+                });
+            });
+        },
+
+        _exec: function (callback, method, args) {
+            args = [this._id].concat(args || []);
+            callback = callback || function (err, response) {};
+            return new Promise(function (resolve, reject) {
+                exec(
+                    function onSuccess(response) {
+                        callback(null, response);
+                        resolve(response);
+                    },
+                    function onError(error) {
+                        callback(error);
+                        reject(error);
+                    },
+                    "Mapbox", method, args
+                );
+            });
+        }
+    })(target);
+};
diff --git a/www/offline-region.js b/www/offline-region.js
index b6601d1..c6a8046 100644
--- a/www/offline-region.js
+++ b/www/offline-region.js
@@ -1,5 +1,5 @@
-var cordova = require("cordova"),
-    exec = require("cordova/exec"),
+var exec = require("cordova/exec"),
+    MapboxPluginAPI = require("./mapbox-plugin-api-mixin"),
     EventsMixin = require("./events-mixin");
 
 var OFFLINE_REGIONS = {};
@@ -18,7 +18,6 @@ function OfflineRegion() {
 
     this._downloading = false;
 
-    this.initEvents("Mapbox.MapInstance");
     this.createStickyChannel("load");
     this.createChannel("progress");
     this.createStickyChannel("complete");
@@ -42,7 +41,20 @@ function OfflineRegion() {
     }
 }
 
-EventsMixin(OfflineRegion.prototype);
+MapboxPluginAPI('OfflineRegion', OfflineRegion.prototype);
+EventsMixin('OfflineRegion', OfflineRegion.prototype);
+
+Object.defineProperty(OfflineRegion.prototype, "downloading", {
+    get: function () {
+        return this._downloading;
+    }
+});
+
+Object.defineProperty(OfflineRegion.prototype, "name", {
+    get: function () {
+        return this._name;
+    }
+});
 
 OfflineRegion.prototype._create = function (options) {
     var args = [options, this._onProgressId, this._onCompleteId];
@@ -57,73 +69,28 @@ OfflineRegion.prototype._instance = function (response) {
     OFFLINE_REGIONS[this._id] = this;
 };
 
-OfflineRegion.prototype._error = function (err) {
-    var error = new Error("OfflineRegion error (ID: " + this._id + "): " + err);
-    this._downloading = false;
-    console.warn("throwing OfflineRegionError: ", error);
-    throw error;
-};
-
-OfflineRegion.prototype._exec = function (successCallback, errorCallback, method, args) {
-    args = [this._id].concat(args || []);
-    exec(successCallback, errorCallback, "Mapbox", method, args);
-};
-
-OfflineRegion.prototype._execAfterLoad = function () {
-    var args = arguments;
-    this.once('load', function (map) {
-        this._exec.apply(this, args);
-    }.bind(this));
-};
-
-OfflineRegion.prototype._registerCallback = function (name, success, fail) {
-    var callbackId = "MapboxOfflineRegion" + name + cordova.callbackId++;
-
-    success = success ||  function () { console.log(callbackId + "() success!", arguments); };
-    fail = fail ||  function () { console.log(callbackId + "() fail :(", arguments); };
-
-    cordova.callbacks[callbackId] = {success: success, fail: fail};
-    return callbackId;
-};
-
-OfflineRegion.prototype.download = function () {
+OfflineRegion.prototype.download = function (callback) {
     this._downloading = true;
-    this._execAfterLoad(onSuccess, this._error, "downloadOfflineRegion");
-    function onSuccess() {
+    return this._execAfterLoad(onSuccess, "downloadOfflineRegion");
+    function onSuccess(err) {
+        if (err) return (callback || this._err)(err);
         console.log("Mapbox OfflineRegion download started.");
     }
 };
 
-OfflineRegion.prototype.pause = function () {
+OfflineRegion.prototype.pause = function (callback) {
     this._downloading = false;
-    this._execAfterLoad(onSuccess, this._error, "pauseOfflineRegion");
+    return this._execAfterLoad(onSuccess, "pauseOfflineRegion");
     function onSuccess() {
+        if (err) return (callback || this._err)(err);
         console.log("Mapbox OfflineRegion download paused.");
     }
 };
 
 OfflineRegion.prototype.getStatus = function (callback) {
-    this._execAfterLoad(onSuccess, onError, "offlineRegionStatus");
-    function onSuccess(status) {
-        callback(null, status);
-    }
-    function onError(error) {
-        callback(error);
-    }
+    return this._execAfterLoad(callback, "offlineRegionStatus");
 };
 
-Object.defineProperty(OfflineRegion.prototype, "downloading", {
-    get: function () {
-        return this._downloading;
-    }
-});
-
-Object.defineProperty(OfflineRegion.prototype, "name", {
-    get: function () {
-        return this._name;
-    }
-});
-
 module.exports = {
     createOfflineRegion: function (options) {
         var region = new OfflineRegion();
@@ -132,31 +99,36 @@ module.exports = {
     },
 
     listOfflineRegions: function (callback) {
-        exec(
-            function (responses) {
-                console.log("Offline regions: ", responses);
-                var regions = responses.map(function (response) {
-                        var region = OFFLINE_REGIONS[response.id];
-                        if (!region) {
-                            region = new OfflineRegion();
-                            region._instance(response);
-                        }
-                        return region;
-                    }),
-                    byName = regions.reduce(function (regionsByName, region) {
-                        regionsByName[region.name] = region;
-                        return regionsByName;
-                    }, {});
-                callback(null, byName);
-            },
-            function (errorMessage) {
-                var error = "Error getting offline regions: " + errorMessage;
-                console.error(error);
-                callback(error);
-            },
-            "Mapbox",
-            "listOfflineRegions",
-            []
-        );
+        callback = callback || function (err, response) {};
+        return new Promise(function (resolve, reject) {
+            exec(
+                function (responses) {
+                    console.log("Offline regions: ", responses);
+                    var regions = responses.map(function (response) {
+                            var region = OFFLINE_REGIONS[response.id];
+                            if (!region) {
+                                region = new OfflineRegion();
+                                region._instance(response);
+                            }
+                            return region;
+                        }),
+                        byName = regions.reduce(function (regionsByName, region) {
+                            regionsByName[region.name] = region;
+                            return regionsByName;
+                        }, {});
+                    callback(null, byName);
+                    resolve(byName);
+                },
+                function (errorMessage) {
+                    var error = "Error getting offline regions: " + errorMessage;
+                    console.error(error);
+                    callback(error);
+                    reject(error);
+                },
+                "Mapbox",
+                "listOfflineRegions",
+                []
+            );
+        });
     }
 };

From 563e7150e02514c060e4c967b253dfd211e6bd56 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Mon, 28 Mar 2016 00:58:06 -0700
Subject: [PATCH 32/37] Fixes bunch of issues with offline layer loading.

---
 src/android/Mapbox.java        | 62 ++++++++++++++----------
 src/android/MapboxManager.java |  8 ++--
 src/android/OfflineRegion.java |  2 +
 src/android/mapbox.gradle      |  3 +-
 www/offline-region.js          | 87 +++++++++++++++++++---------------
 5 files changed, 94 insertions(+), 68 deletions(-)

diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 212f4c6..ae410be 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -44,6 +44,7 @@ public class Mapbox extends CordovaPlugin {
   private static final String ACTION_SHOW_USER_LOCATION = "showUserLocation";
   private static final String ACTION_LIST_OFFLINE_REGIONS = "listOfflineRegions";
   private static final String ACTION_CREATE_OFFLINE_REGION = "createOfflineRegion";
+  private static final String ACTION_BIND_OFFLINE_REGION_CALLBACKS = "bindOfflineRegionCallbacks";
   private static final String ACTION_DOWNLOAD_OFFLINE_REGION = "downloadOfflineRegion";
   private static final String ACTION_PAUSE_OFFLINE_REGION = "pauseOfflineRegion";
   private static final String ACTION_OFFLINE_REGION_STATUS = "offlineRegionStatus";
@@ -233,9 +234,41 @@ else if (ACTION_LIST_OFFLINE_REGIONS.equals(action)) {
 
     else if (ACTION_CREATE_OFFLINE_REGION.equals(action)) {
       final JSONObject options = args.getJSONObject(0);
+      this.createOfflineRegion(options, callbackContext);
+    }
+
+    else if (ACTION_BIND_OFFLINE_REGION_CALLBACKS.equals(action)) {
+      final long offlineRegionId = args.getLong(0);
       final CallbackContext onProgress = new CallbackContext(args.getString(1), this.webView);
       final CallbackContext onComplete = new CallbackContext(args.getString(2), this.webView);
-      this.createOfflineRegion(options, callbackContext, onProgress, onComplete);
+      final OfflineRegion region = this.mapboxManager.getOfflineRegion(offlineRegionId);
+
+      region.setObserver(new MapboxManager.OfflineRegionProgressCallback() {
+        @Override
+        public void onProgress(JSONObject progress) {
+          PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
+          result.setKeepCallback(true);
+          onProgress.sendPluginResult(result);
+        }
+
+        @Override
+        public void onComplete(JSONObject progress) {
+          Log.d(LOG_TAG, "OfflineRegion (" + region.getId() + ") download complete.");
+          PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
+          onComplete.sendPluginResult(result);
+        }
+
+        @Override
+        public void onError(String error) {
+          String message = "Failed to create OfflineRegion (" + region.getId() + "): " + error;
+          Log.e(LOG_TAG, message);
+          PluginResult result = new PluginResult(PluginResult.Status.ERROR, message);
+          result.setKeepCallback(true);
+          onComplete.error(message);
+        }
+      });
+
+      callbackContext.success();
     }
 
     else if (ACTION_DOWNLOAD_OFFLINE_REGION.equals(action)) {
@@ -405,34 +438,11 @@ public void onError(String error) {
     });
   }
 
-  public void createOfflineRegion(final JSONObject options, final CallbackContext callback, final CallbackContext onProgress, final CallbackContext onComplete) throws JSONException {
+  public void createOfflineRegion(final JSONObject options, final CallbackContext callback) throws JSONException {
     cordova.getActivity().runOnUiThread(new Runnable() {
       @Override
       public void run() {
-        mapboxManager.createOfflineRegion(options, callback, new MapboxManager.OfflineRegionProgressCallback() {
-          @Override
-          public void onProgress(JSONObject progress) {
-            PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
-            result.setKeepCallback(true);
-            onProgress.sendPluginResult(result);
-          }
-
-          @Override
-          public void onComplete(JSONObject progress) {
-            Log.d(LOG_TAG, "complete");
-            PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
-            onComplete.sendPluginResult(result);
-          }
-
-          @Override
-          public void onError(String error) {
-            String message = "Failed to create offline region: " + error;
-            Log.e(LOG_TAG, message);
-            PluginResult result = new PluginResult(PluginResult.Status.ERROR, message);
-            result.setKeepCallback(true);
-            onComplete.error(message);
-          }
-        });
+        mapboxManager.createOfflineRegion(options, callback);
       }
     });
   }
diff --git a/src/android/MapboxManager.java b/src/android/MapboxManager.java
index 0d4d187..098cfd3 100644
--- a/src/android/MapboxManager.java
+++ b/src/android/MapboxManager.java
@@ -1,5 +1,6 @@
 package com.telerik.plugins.mapbox;
 
+import android.util.Log;
 import android.widget.FrameLayout;
 
 import com.mapbox.mapboxsdk.constants.Style;
@@ -15,6 +16,7 @@
 
 import org.apache.cordova.CallbackContext;
 import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.PluginResult;
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -189,7 +191,7 @@ public void onError(String error) {
         });
     }
 
-    public void createOfflineRegion(final JSONObject options, final CallbackContext callback, final OfflineRegionProgressCallback offlineRegionStatusCallback) {
+    public void createOfflineRegion(final JSONObject options, final CallbackContext callback) {
         try {
             final String regionName = options.getString("name");
 
@@ -203,16 +205,14 @@ public void createOfflineRegion(final JSONObject options, final CallbackContext
                 public void onCreate(com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion) {
                     try {
                         OfflineRegion region = createOfflineRegion(offlineRegion);
-                        region.setObserver(offlineRegionStatusCallback);
-
                         JSONObject response = region.getMetadata();
                         response.put(JSON_FIELD_ID, region.getId());
                         callback.success(response);
                     } catch (JSONException e) {
                         this.onError(e.getMessage());
+                        removeOfflineRegion(offlineRegion.getID());
                     } catch (UnsupportedEncodingException e) {
                         this.onError(e.getMessage());
-                    } finally {
                         removeOfflineRegion(offlineRegion.getID());
                     }
                 }
diff --git a/src/android/OfflineRegion.java b/src/android/OfflineRegion.java
index 464a36b..dcb5994 100644
--- a/src/android/OfflineRegion.java
+++ b/src/android/OfflineRegion.java
@@ -65,6 +65,8 @@ private JSONObject statusToJSON(OfflineRegionStatus status) throws JSONException
         long requiredCount = status.getRequiredResourceCount();
         double percentage = requiredCount >= 0 ? (100.0 * completedCount / requiredCount) : 0.0;
         JSONObject jsonStatus = new JSONObject()
+            .put("completed", status.isComplete())
+            .put("downloading", status.getDownloadState() == com.mapbox.mapboxsdk.offline.OfflineRegion.STATE_ACTIVE)
             .put("completedCount", completedCount)
             .put("completedSize", status.getCompletedResourceSize())
             .put("requiredCount", requiredCount)
diff --git a/src/android/mapbox.gradle b/src/android/mapbox.gradle
index f67c263..1ce0310 100644
--- a/src/android/mapbox.gradle
+++ b/src/android/mapbox.gradle
@@ -2,12 +2,13 @@ ext.cdvMinSdkVersion = 15
 
 repositories {
     mavenCentral()
+    maven { url "http://oss.sonatype.org/content/repositories/snapshots/" }
 }
 
 dependencies {
     compile 'com.cocoahero.android:geojson:1.0.1@jar'
     compile 'com.android.support:appcompat-v7:23.0.1'
-    compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:4.0.0-rc.1@aar'){
+    compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:4.0.0-SNAPSHOT@aar'){
         transitive=true
     }
 }
diff --git a/www/offline-region.js b/www/offline-region.js
index c6a8046..6ccfa0d 100644
--- a/www/offline-region.js
+++ b/www/offline-region.js
@@ -57,23 +57,31 @@ Object.defineProperty(OfflineRegion.prototype, "name", {
 });
 
 OfflineRegion.prototype._create = function (options) {
-    var args = [options, this._onProgressId, this._onCompleteId];
+    var args = [options];
     exec(this._instance, this._error, "Mapbox", "createOfflineRegion", args);
 };
 
 OfflineRegion.prototype._instance = function (response) {
     this._id = response.id;
     this._name = response.name;
-    this.loaded = true;
-    this.fire("load", {offlineRegion: this});
-    OFFLINE_REGIONS[this._id] = this;
+    return this._exec(
+        onSuccess.bind(this),
+        'bindOfflineRegionCallbacks',
+        [this._onProgressId, this._onCompleteId]
+    );
+    function onSuccess(err) {
+        if (err) return this._error(err);
+        OFFLINE_REGIONS[this._id] = this;
+        this.loaded = true;
+        this.fire("load", {offlineRegion: this});
+    }
 };
 
 OfflineRegion.prototype.download = function (callback) {
     this._downloading = true;
     return this._execAfterLoad(onSuccess, "downloadOfflineRegion");
     function onSuccess(err) {
-        if (err) return (callback || this._err)(err);
+        if (err) return (callback || this._error)(err);
         console.log("Mapbox OfflineRegion download started.");
     }
 };
@@ -81,8 +89,8 @@ OfflineRegion.prototype.download = function (callback) {
 OfflineRegion.prototype.pause = function (callback) {
     this._downloading = false;
     return this._execAfterLoad(onSuccess, "pauseOfflineRegion");
-    function onSuccess() {
-        if (err) return (callback || this._err)(err);
+    function onSuccess(err) {
+        if (err) return (callback || this._error)(err);
         console.log("Mapbox OfflineRegion download paused.");
     }
 };
@@ -91,6 +99,21 @@ OfflineRegion.prototype.getStatus = function (callback) {
     return this._execAfterLoad(callback, "offlineRegionStatus");
 };
 
+function listOfflineRegions() {
+    return new Promise(function (resolve, reject) {
+        exec(resolve, reject, "Mapbox", "listOfflineRegions", []);
+    });
+}
+
+function createRegionFromResponse(response) {
+    var region = OFFLINE_REGIONS[response.id];
+    if (!region) {
+        region = new OfflineRegion();
+        region._instance(response);
+    }
+    return region;
+}
+
 module.exports = {
     createOfflineRegion: function (options) {
         var region = new OfflineRegion();
@@ -100,35 +123,25 @@ module.exports = {
 
     listOfflineRegions: function (callback) {
         callback = callback || function (err, response) {};
-        return new Promise(function (resolve, reject) {
-            exec(
-                function (responses) {
-                    console.log("Offline regions: ", responses);
-                    var regions = responses.map(function (response) {
-                            var region = OFFLINE_REGIONS[response.id];
-                            if (!region) {
-                                region = new OfflineRegion();
-                                region._instance(response);
-                            }
-                            return region;
-                        }),
-                        byName = regions.reduce(function (regionsByName, region) {
-                            regionsByName[region.name] = region;
-                            return regionsByName;
-                        }, {});
-                    callback(null, byName);
-                    resolve(byName);
-                },
-                function (errorMessage) {
-                    var error = "Error getting offline regions: " + errorMessage;
-                    console.error(error);
-                    callback(error);
-                    reject(error);
-                },
-                "Mapbox",
-                "listOfflineRegions",
-                []
-            );
-        });
+
+        return listOfflineRegions()
+            .then(function (responses) {
+                return Promise.all(responses.map(createRegionFromResponse));
+            })
+            .then(function (regions) {
+                return regions.reduce(function (regionsByName, region) {
+                    regionsByName[region.name] = region;
+                    return regionsByName;
+                }, {});
+            })
+            .then(function (regionsByName) {
+                callback(null, regionsByName);
+                return regionsByName;
+            })
+            .catch(function (errorMessage) {
+                var error = "Error getting offline regions: " + errorMessage;
+                console.error(error);
+                callback(error);
+            });
     }
 };

From 857674a5374b0f6028f300fb4e764d783b3878e0 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Tue, 29 Mar 2016 00:28:20 -0700
Subject: [PATCH 33/37] switched downloading log statements to debug

---
 www/offline-region.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/www/offline-region.js b/www/offline-region.js
index 6ccfa0d..3c89fdc 100644
--- a/www/offline-region.js
+++ b/www/offline-region.js
@@ -82,7 +82,7 @@ OfflineRegion.prototype.download = function (callback) {
     return this._execAfterLoad(onSuccess, "downloadOfflineRegion");
     function onSuccess(err) {
         if (err) return (callback || this._error)(err);
-        console.log("Mapbox OfflineRegion download started.");
+        console.debug("Mapbox OfflineRegion download started.");
     }
 };
 
@@ -91,7 +91,7 @@ OfflineRegion.prototype.pause = function (callback) {
     return this._execAfterLoad(onSuccess, "pauseOfflineRegion");
     function onSuccess(err) {
         if (err) return (callback || this._error)(err);
-        console.log("Mapbox OfflineRegion download paused.");
+        console.debug("Mapbox OfflineRegion download paused.");
     }
 };
 

From d0f5b03cab642cad8b18042a17ce93d09fdf62bc Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Tue, 29 Mar 2016 15:59:52 -0700
Subject: [PATCH 34/37] Removed calls to MapView's onStart() and onStop() as
 they have been removed upstream.

---
 src/android/Mapbox.java | 14 --------------
 1 file changed, 14 deletions(-)

diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index ae410be..56e32b0 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -481,13 +481,6 @@ public void onRequestPermissionResult(int commandId, String[] permissions, int[]
     Command.execute(this, commandId);
   }
 
-  @Override
-  public void onStart() {
-    for (Map map : mapboxManager.maps()) {
-      map.getMapView().onStart();
-    }
-  }
-
   public void onResume(boolean multitasking) {
     for (Map map : mapboxManager.maps()) {
       map.getMapView().onResume();
@@ -501,13 +494,6 @@ public void onPause(boolean multitasking) {
     }
   }
 
-  @Override
-  public void onStop() {
-    for (Map map : mapboxManager.maps()) {
-      map.getMapView().onStop();
-    }
-  }
-
   @Override
   public void onDestroy() {
     for (Map map : mapboxManager.maps()) {

From db2154ae3245d8e4a55d97766f0b2824ad79dc7e Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Wed, 30 Mar 2016 11:08:29 -0700
Subject: [PATCH 35/37] Cleaned up OfflineRegion implementation. Removed extra
 call to Android to bind offline progress callbacks upon creation.

---
 src/android/Mapbox.java        |  71 ++++++++---------------
 src/android/MapboxManager.java |  80 ++++++++++++--------------
 src/android/OfflineRegion.java |  85 +++++++++++++++------------
 www/offline-region.js          | 102 +++++++++++++++------------------
 4 files changed, 154 insertions(+), 184 deletions(-)

diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 56e32b0..ce726c6 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -7,7 +7,6 @@
 import android.os.Build;
 import android.support.v4.app.ActivityCompat;
 import android.util.DisplayMetrics;
-import android.util.Log;
 
 import com.mapbox.mapboxsdk.annotations.Marker;
 import com.mapbox.mapboxsdk.maps.MapboxMap;
@@ -29,9 +28,6 @@
 // TODO fox Xwalk compat, see nativepagetransitions plugin
 // TODO look at demo app: https://github.com/mapbox/mapbox-gl-native/blob/master/android/java/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxgl/testapp/MainActivity.java
 public class Mapbox extends CordovaPlugin {
-
-  private static final String LOG_TAG = "MapboxCordovaPlugin";
-
   public static final String FINE_LOCATION = Manifest.permission.ACCESS_FINE_LOCATION;
   public static final String COARSE_LOCATION = Manifest.permission.ACCESS_COARSE_LOCATION;
 
@@ -44,7 +40,6 @@ public class Mapbox extends CordovaPlugin {
   private static final String ACTION_SHOW_USER_LOCATION = "showUserLocation";
   private static final String ACTION_LIST_OFFLINE_REGIONS = "listOfflineRegions";
   private static final String ACTION_CREATE_OFFLINE_REGION = "createOfflineRegion";
-  private static final String ACTION_BIND_OFFLINE_REGION_CALLBACKS = "bindOfflineRegionCallbacks";
   private static final String ACTION_DOWNLOAD_OFFLINE_REGION = "downloadOfflineRegion";
   private static final String ACTION_PAUSE_OFFLINE_REGION = "pauseOfflineRegion";
   private static final String ACTION_OFFLINE_REGION_STATUS = "offlineRegionStatus";
@@ -234,41 +229,10 @@ else if (ACTION_LIST_OFFLINE_REGIONS.equals(action)) {
 
     else if (ACTION_CREATE_OFFLINE_REGION.equals(action)) {
       final JSONObject options = args.getJSONObject(0);
-      this.createOfflineRegion(options, callbackContext);
-    }
-
-    else if (ACTION_BIND_OFFLINE_REGION_CALLBACKS.equals(action)) {
-      final long offlineRegionId = args.getLong(0);
       final CallbackContext onProgress = new CallbackContext(args.getString(1), this.webView);
       final CallbackContext onComplete = new CallbackContext(args.getString(2), this.webView);
-      final OfflineRegion region = this.mapboxManager.getOfflineRegion(offlineRegionId);
-
-      region.setObserver(new MapboxManager.OfflineRegionProgressCallback() {
-        @Override
-        public void onProgress(JSONObject progress) {
-          PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
-          result.setKeepCallback(true);
-          onProgress.sendPluginResult(result);
-        }
-
-        @Override
-        public void onComplete(JSONObject progress) {
-          Log.d(LOG_TAG, "OfflineRegion (" + region.getId() + ") download complete.");
-          PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
-          onComplete.sendPluginResult(result);
-        }
-
-        @Override
-        public void onError(String error) {
-          String message = "Failed to create OfflineRegion (" + region.getId() + "): " + error;
-          Log.e(LOG_TAG, message);
-          PluginResult result = new PluginResult(PluginResult.Status.ERROR, message);
-          result.setKeepCallback(true);
-          onComplete.error(message);
-        }
-      });
 
-      callbackContext.success();
+      this.createOfflineRegion(options, onProgress, onComplete, callbackContext);
     }
 
     else if (ACTION_DOWNLOAD_OFFLINE_REGION.equals(action)) {
@@ -423,26 +387,37 @@ public void listOfflineRegions(final CallbackContext callback) {
       @Override
       public void run() {
         mapboxManager.loadOfflineRegions(new MapboxManager.LoadOfflineRegionsCallback() {
-          @Override
-          public void onList(JSONArray offlineRegions) {
-            callback.success(offlineRegions);
-          }
+            @Override
+            public void onList(JSONArray offlineRegions) {
+                callback.success(offlineRegions);
+            }
 
-          @Override
-          public void onError(String error) {
-            String message = "Error loading offline regions: " + error;
-            callback.error(message);
-          }
+            @Override
+            public void onError(String error) {
+                String message = "Error loading offline regions: " + error;
+                callback.error(message);
+            }
         });
       }
     });
   }
 
-  public void createOfflineRegion(final JSONObject options, final CallbackContext callback) throws JSONException {
+  public void createOfflineRegion(final JSONObject options, final CallbackContext onProgress, final CallbackContext onComplete, final CallbackContext callback) throws JSONException {
     cordova.getActivity().runOnUiThread(new Runnable() {
       @Override
       public void run() {
-        mapboxManager.createOfflineRegion(options, callback);
+        mapboxManager.getOrCreateOfflineRegion(options, new MapboxManager.OfflineRegionLoadCallback() {
+          @Override
+          public void onLoad(OfflineRegion region) {
+            region.bindStatusCallbacks(onProgress, onComplete);
+            callback.success(region.getMetadata());
+          }
+
+          @Override
+          public void onError(String error) {
+            callback.error(error);
+          }
+        });
       }
     });
   }
diff --git a/src/android/MapboxManager.java b/src/android/MapboxManager.java
index 098cfd3..c36cf56 100644
--- a/src/android/MapboxManager.java
+++ b/src/android/MapboxManager.java
@@ -1,6 +1,5 @@
 package com.telerik.plugins.mapbox;
 
-import android.util.Log;
 import android.widget.FrameLayout;
 
 import com.mapbox.mapboxsdk.constants.Style;
@@ -16,7 +15,7 @@
 
 import org.apache.cordova.CallbackContext;
 import org.apache.cordova.CordovaWebView;
-import org.apache.cordova.PluginResult;
+
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -26,11 +25,6 @@
 import java.util.HashMap;
 
 class MapboxManager {
-    // JSON encoding/decoding
-    public static final String JSON_CHARSET = "UTF-8";
-    public static final String JSON_FIELD_ID = "id";
-    public static final String JSON_FIELD_REGION_NAME = "name";
-
     public static String getStyle(final String requested) {
         if ("light".equalsIgnoreCase(requested)) {
             return Style.LIGHT;
@@ -69,9 +63,8 @@ public interface OfflineRegionStatusCallback {
         void onError(String error);
     }
 
-    public interface OfflineRegionProgressCallback {
-        void onComplete(JSONObject progress);
-        void onProgress(JSONObject progress);
+    public interface OfflineRegionLoadCallback {
+        void onLoad(OfflineRegion region);
         void onError(String error);
     }
 
@@ -173,7 +166,6 @@ public void onList(com.mapbox.mapboxsdk.offline.OfflineRegion[] offlineRegions)
                             region = createOfflineRegion(offlineRegion);
                         }
                         response = region.getMetadata();
-                        response.put(JSON_FIELD_ID, region.getId());
                         responses.put(response);
                     }
                     callback.onList(responses);
@@ -191,42 +183,44 @@ public void onError(String error) {
         });
     }
 
-    public void createOfflineRegion(final JSONObject options, final CallbackContext callback) {
+    public void getOrCreateOfflineRegion(final JSONObject options, final OfflineRegionLoadCallback callback) {
         try {
-            final String regionName = options.getString("name");
-
-            JSONObject metadata = new JSONObject();
-            metadata.put(JSON_FIELD_REGION_NAME, regionName);
-            byte[] encodedMetadata =  metadata.toString().getBytes(JSON_CHARSET);
-            OfflineRegionDefinition definition = this.createOfflineRegionDefinition(density, options);
-
-            offlineManager.createOfflineRegion(definition, encodedMetadata, new OfflineManager.CreateOfflineRegionCallback() {
-                @Override
-                public void onCreate(com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion) {
-                    try {
-                        OfflineRegion region = createOfflineRegion(offlineRegion);
-                        JSONObject response = region.getMetadata();
-                        response.put(JSON_FIELD_ID, region.getId());
-                        callback.success(response);
-                    } catch (JSONException e) {
-                        this.onError(e.getMessage());
-                        removeOfflineRegion(offlineRegion.getID());
-                    } catch (UnsupportedEncodingException e) {
-                        this.onError(e.getMessage());
-                        removeOfflineRegion(offlineRegion.getID());
+            if (options.has("id")) {
+                OfflineRegion region = getOfflineRegion(options.getLong("id"));
+                callback.onLoad(region);
+            } else {
+                JSONObject properties = options.getJSONObject(OfflineRegion.JSON_FIELD_PROPERTIES);
+                byte[] encodedMetadata = new JSONObject()
+                        .put(OfflineRegion.JSON_FIELD_PROPERTIES, properties)
+                        .toString()
+                        .getBytes(OfflineRegion.JSON_CHARSET);
+                OfflineRegionDefinition definition = this.createOfflineRegionDefinition(density, options);
+
+                offlineManager.createOfflineRegion(definition, encodedMetadata, new OfflineManager.CreateOfflineRegionCallback() {
+                    @Override
+                    public void onCreate(com.mapbox.mapboxsdk.offline.OfflineRegion offlineRegion) {
+                        try {
+                            OfflineRegion region = createOfflineRegion(offlineRegion);
+                            callback.onLoad(region);
+                        } catch (JSONException e) {
+                            this.onError(e.getMessage());
+                            removeOfflineRegion(offlineRegion.getID());
+                        } catch (UnsupportedEncodingException e) {
+                            this.onError(e.getMessage());
+                            removeOfflineRegion(offlineRegion.getID());
+                        }
                     }
-                }
 
-                @Override
-                public void onError(String error) {
-                    String message = "Failed to create offline region: " + error;
-                    callback.error(message);
-                }
-            });
-        } catch (JSONException e) {
-            callback.error(e.getMessage());
+                    @Override
+                    public void onError(String error) {
+                        callback.onError(error);
+                    }
+                });
+            }
+        } catch(JSONException e) {
+            callback.onError(e.getMessage());
         } catch (UnsupportedEncodingException e) {
-            callback.error(e.getMessage());
+            callback.onError(e.getMessage());
         }
     }
 
diff --git a/src/android/OfflineRegion.java b/src/android/OfflineRegion.java
index dcb5994..a486d4d 100644
--- a/src/android/OfflineRegion.java
+++ b/src/android/OfflineRegion.java
@@ -1,8 +1,12 @@
 package com.telerik.plugins.mapbox;
 
+import android.util.Log;
+
 import com.mapbox.mapboxsdk.offline.OfflineRegionError;
 import com.mapbox.mapboxsdk.offline.OfflineRegionStatus;
 
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.PluginResult;
 import org.json.JSONException;
 import org.json.JSONObject;
 
@@ -10,7 +14,11 @@
 
 public class OfflineRegion {
 
+    public static final String LOG_TAG = "OfflineRegion";
+
     public static final String JSON_CHARSET = "UTF-8";
+    public static final String JSON_FIELD_ID = "id";
+    public static final String JSON_FIELD_PROPERTIES = "properties";
 
     private JSONObject metadata;
 
@@ -19,7 +27,9 @@ public class OfflineRegion {
     protected OfflineRegion(com.mapbox.mapboxsdk.offline.OfflineRegion region) throws JSONException, UnsupportedEncodingException {
         this.region = region;
         byte[] encodedMetadata = region.getMetadata();
-        this.metadata = new JSONObject(new String(encodedMetadata, JSON_CHARSET));
+        JSONObject metadata = new JSONObject(new String(encodedMetadata, JSON_CHARSET))
+                .put(JSON_FIELD_ID, this.getId());
+        this.metadata = metadata;
     }
 
     public Long getId() {
@@ -56,8 +66,43 @@ public void pause() {
         this.region.setDownloadState(com.mapbox.mapboxsdk.offline.OfflineRegion.STATE_INACTIVE);
     }
 
-    public void setObserver(MapboxManager.OfflineRegionProgressCallback statusCallback) {
-        this.region.setObserver(new OfflineRegionObserver(statusCallback));
+    public void bindStatusCallbacks(final CallbackContext onProgress, final CallbackContext onComplete) {
+        this.region.setObserver(new com.mapbox.mapboxsdk.offline.OfflineRegion.OfflineRegionObserver() {
+            @Override
+            public void onStatusChanged(OfflineRegionStatus status) {
+                try {
+                    JSONObject progress = statusToJSON(status);
+                    PluginResult result = new PluginResult(PluginResult.Status.OK, progress);
+                    if (status.isComplete()) {
+                        onComplete.sendPluginResult(result);
+                    } else {
+                        result.setKeepCallback(true);
+                        onProgress.sendPluginResult(result);
+                    }
+                } catch (JSONException e) {
+                    this.onError(e.getMessage());
+                }
+            }
+
+            @Override
+            public void onError(OfflineRegionError error) {
+                String message = "OfflineRegionError: [" + error.getReason() + "] " + error.getMessage();
+                this.onError(message);
+            }
+
+            @Override
+            public void mapboxTileCountLimitExceeded(long limit) {
+                this.onError("Tile limit exceeded (limit: " + limit + ")");
+            }
+
+            private void onError(String error) {
+                Log.e(LOG_TAG, error);
+                PluginResult result = new PluginResult(PluginResult.Status.ERROR, error);
+                result.setKeepCallback(true);
+                onComplete.error(error);
+                pause();
+            }
+        });
     }
 
     private JSONObject statusToJSON(OfflineRegionStatus status) throws JSONException {
@@ -74,38 +119,4 @@ private JSONObject statusToJSON(OfflineRegionStatus status) throws JSONException
 
         return jsonStatus;
     }
-
-    private class OfflineRegionObserver implements com.mapbox.mapboxsdk.offline.OfflineRegion.OfflineRegionObserver {
-        private MapboxManager.OfflineRegionProgressCallback progressCallback;
-
-        OfflineRegionObserver(MapboxManager.OfflineRegionProgressCallback callback) {
-            this.progressCallback = callback;
-        }
-
-        @Override
-        public void onStatusChanged(OfflineRegionStatus status) {
-            try {
-                JSONObject progress = statusToJSON(status);
-                if (!status.isComplete()) {
-                    progressCallback.onProgress(progress);
-                } else {
-                    progressCallback.onComplete(progress);
-                }
-            } catch (JSONException e) {
-                progressCallback.onError(e.getMessage());
-            }
-        }
-
-        @Override
-        public void onError(OfflineRegionError error) {
-            String message = "OfflineRegionError: [" + error.getReason() + "] " + error.getMessage();
-            progressCallback.onError(message);
-            pause();
-        }
-
-        @Override
-        public void mapboxTileCountLimitExceeded(long limit) {
-            progressCallback.onError("Tile limit exceeded (limit: " + limit + ")");
-        }
-    }
 }
diff --git a/www/offline-region.js b/www/offline-region.js
index 3c89fdc..10871f3 100644
--- a/www/offline-region.js
+++ b/www/offline-region.js
@@ -4,17 +4,44 @@ var exec = require("cordova/exec"),
 
 var OFFLINE_REGIONS = {};
 
-function OfflineRegion() {
+function OfflineRegion(options) {
+    this._init();
+    this._properties = options.properties || {};
+
+    var args = [options, this._onProgressId, this._onCompleteId];
+    exec(this._onCreate, this._error, "Mapbox", "createOfflineRegion", args);
+}
+
+MapboxPluginAPI('OfflineRegion', OfflineRegion.prototype);
+EventsMixin('OfflineRegion', OfflineRegion.prototype);
+
+Object.defineProperty(OfflineRegion.prototype, "downloading", {
+    get: function () {
+        return this._downloading;
+    }
+});
+
+Object.defineProperty(OfflineRegion.prototype, "loaded", {
+    get: function () {
+        return this._loaded;
+    }
+});
+
+Object.defineProperty(OfflineRegion.prototype, "properties", {
+    get: function () {
+        return this._properties;
+    }
+});
+
+OfflineRegion.prototype._init = function () {
     var onProgress = _onProgress.bind(this),
         onComplete = _onComplete.bind(this),
         onError = _onError.bind(this);
 
     this._onProgressId = this._registerCallback('onProgress', onProgress);
     this._onCompleteId = this._registerCallback('onComplete', onComplete, onError);
-
-    this._error = this._error.bind(this);
-    this._create = this._create.bind(this);
-    this._instance = this._instance.bind(this);
+    this._error = onError;
+    this._onCreate = this._onCreate.bind(this);
 
     this._downloading = false;
 
@@ -34,47 +61,20 @@ function OfflineRegion() {
 
     function _onError(error) {
         try {
-            this._error(error);
+            this.prototype._error.call(this, error);
         } catch (e) {
             this.fire("error", e);
         }
     }
-}
-
-MapboxPluginAPI('OfflineRegion', OfflineRegion.prototype);
-EventsMixin('OfflineRegion', OfflineRegion.prototype);
-
-Object.defineProperty(OfflineRegion.prototype, "downloading", {
-    get: function () {
-        return this._downloading;
-    }
-});
-
-Object.defineProperty(OfflineRegion.prototype, "name", {
-    get: function () {
-        return this._name;
-    }
-});
-
-OfflineRegion.prototype._create = function (options) {
-    var args = [options];
-    exec(this._instance, this._error, "Mapbox", "createOfflineRegion", args);
 };
 
-OfflineRegion.prototype._instance = function (response) {
+OfflineRegion.prototype._onCreate = function onCreate(response) {
     this._id = response.id;
-    this._name = response.name;
-    return this._exec(
-        onSuccess.bind(this),
-        'bindOfflineRegionCallbacks',
-        [this._onProgressId, this._onCompleteId]
-    );
-    function onSuccess(err) {
-        if (err) return this._error(err);
-        OFFLINE_REGIONS[this._id] = this;
-        this.loaded = true;
-        this.fire("load", {offlineRegion: this});
-    }
+    this._properties = response.properties;
+    this._loaded = true;
+    this.fire("load", this);
+
+    OFFLINE_REGIONS[this._id] = this;
 };
 
 OfflineRegion.prototype.download = function (callback) {
@@ -106,19 +106,15 @@ function listOfflineRegions() {
 }
 
 function createRegionFromResponse(response) {
-    var region = OFFLINE_REGIONS[response.id];
-    if (!region) {
-        region = new OfflineRegion();
-        region._instance(response);
-    }
-    return region;
+    return new Promise(function (resolve, reject) {
+        var region = OFFLINE_REGIONS[response.id] || new OfflineRegion(response);
+        region.once('load', resolve);
+    });
 }
 
 module.exports = {
     createOfflineRegion: function (options) {
-        var region = new OfflineRegion();
-        region._create(options);
-        return region;
+        return new OfflineRegion(options);
     },
 
     listOfflineRegions: function (callback) {
@@ -129,14 +125,8 @@ module.exports = {
                 return Promise.all(responses.map(createRegionFromResponse));
             })
             .then(function (regions) {
-                return regions.reduce(function (regionsByName, region) {
-                    regionsByName[region.name] = region;
-                    return regionsByName;
-                }, {});
-            })
-            .then(function (regionsByName) {
-                callback(null, regionsByName);
-                return regionsByName;
+                callback(null, regions);
+                return regions;
             })
             .catch(function (errorMessage) {
                 var error = "Error getting offline regions: " + errorMessage;

From efd96e60145f7097288c3d7977a6c2bda2386913 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Wed, 30 Mar 2016 14:51:46 -0700
Subject: [PATCH 36/37] Initial attempt at supporting ALL map events.

---
 src/android/Map.java           | 98 +++++++++++++++++++++++++++++++++-
 src/android/Mapbox.java        | 47 +++++++++++-----
 src/android/MapboxManager.java |  4 +-
 www/events-mixin.js            |  3 ++
 www/map-instance.js            | 21 ++++++--
 5 files changed, 155 insertions(+), 18 deletions(-)

diff --git a/src/android/Map.java b/src/android/Map.java
index a1d8434..76ef884 100644
--- a/src/android/Map.java
+++ b/src/android/Map.java
@@ -1,7 +1,9 @@
 package com.telerik.plugins.mapbox;
 
+import android.location.Location;
 import android.support.annotation.Nullable;
 
+import com.mapbox.mapboxsdk.annotations.Marker;
 import com.mapbox.mapboxsdk.annotations.MarkerOptions;
 import com.mapbox.mapboxsdk.camera.CameraPosition;
 import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
@@ -15,6 +17,11 @@
 import org.json.JSONObject;
 
 public class Map {
+
+    public interface MapEventListener {
+        void onEvent(String name, JSONObject event);
+    }
+
     public static MapboxMapOptions createMapboxMapOptions(JSONObject options) throws JSONException {
         MapboxMapOptions opts = new MapboxMapOptions();
         opts.styleUrl(MapboxManager.getStyle(options.getString("style")));
@@ -57,11 +64,14 @@ public static CameraPosition getCameraPostion(JSONObject options, @Nullable Came
 
     private MapboxMap mapboxMap;
 
+    private MapEventListener eventListener;
+
     private FeatureManager features;
 
-    public Map(long id, final MapView mapView) {
+    public Map(long id, MapEventListener eventListener, final MapView mapView) {
         this.id = id;
         this.mapView = mapView;
+        this.eventListener = eventListener;
     }
 
     public long getId() {
@@ -74,6 +84,92 @@ public MapView getMapView() {
 
     public void setMapboxMap(MapboxMap mMap) {
         this.mapboxMap = mMap;
+
+        this.mapboxMap.setOnCameraChangeListener(new MapboxMap.OnCameraChangeListener() {
+            @Override
+            public void onCameraChange(CameraPosition position) {
+                eventListener.onEvent("camerachange", new JSONObject());
+            }
+        });
+
+        this.mapboxMap.setOnFlingListener(new MapboxMap.OnFlingListener() {
+            @Override
+            public void onFling() {
+                eventListener.onEvent("fling", new JSONObject());
+            }
+        });
+
+        this.mapboxMap.setOnInfoWindowClickListener(new MapboxMap.OnInfoWindowClickListener() {
+            @Override
+            public boolean onInfoWindowClick(Marker marker) {
+                eventListener.onEvent("infowindowclick", new JSONObject());
+                return true;
+            }
+        });
+
+        this.mapboxMap.setOnInfoWindowCloseListener(new MapboxMap.OnInfoWindowCloseListener() {
+            @Override
+            public void onInfoWindowClose(Marker marker) {
+                eventListener.onEvent("infowindowclose", new JSONObject());
+            }
+        });
+
+        this.mapboxMap.setOnInfoWindowLongClickListener(new MapboxMap.OnInfoWindowLongClickListener() {
+            @Override
+            public void onInfoWindowLongClick(Marker marker) {
+                eventListener.onEvent("infowindowlongclick", new JSONObject());
+            }
+        });
+
+        this.mapboxMap.setOnMapClickListener(new MapboxMap.OnMapClickListener() {
+            @Override
+            public void onMapClick(LatLng point) {
+                eventListener.onEvent("mapclick", new JSONObject());
+            }
+        });
+
+        this.mapboxMap.setOnMapLongClickListener(new MapboxMap.OnMapLongClickListener() {
+            @Override
+            public void onMapLongClick(LatLng point) {
+                eventListener.onEvent("maplongclick", new JSONObject());
+            }
+        });
+
+        this.mapboxMap.setOnMarkerClickListener(new MapboxMap.OnMarkerClickListener() {
+            @Override
+            public boolean onMarkerClick(Marker marker) {
+                eventListener.onEvent("markerclick", new JSONObject());
+                return true;
+            }
+        });
+
+        this.mapboxMap.setOnMyBearingTrackingModeChangeListener(new MapboxMap.OnMyBearingTrackingModeChangeListener() {
+            @Override
+            public void onMyBearingTrackingModeChange(int myBearingTrackingMode) {
+                eventListener.onEvent("bearingtrackingmodechange", new JSONObject());
+            }
+        });
+
+        this.mapboxMap.setOnMyLocationChangeListener(new MapboxMap.OnMyLocationChangeListener() {
+            @Override
+            public void onMyLocationChange(@Nullable Location location) {
+                eventListener.onEvent("locationchange", new JSONObject());
+            }
+        });
+
+        this.mapboxMap.setOnMyLocationTrackingModeChangeListener(new MapboxMap.OnMyLocationTrackingModeChangeListener() {
+            @Override
+            public void onMyLocationTrackingModeChange(int myLocationTrackingMode) {
+                eventListener.onEvent("locationtrackingmodechange", new JSONObject());
+            }
+        });
+
+        this.mapboxMap.setOnScrollListener(new MapboxMap.OnScrollListener() {
+            @Override
+            public void onScroll() {
+                eventListener.onEvent("onscroll", new JSONObject());
+            }
+        });
     }
 
     public void setFeatureManager(FeatureManager featureManager) {
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index ce726c6..9a6a219 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -80,7 +80,8 @@ public boolean execute(Command command) throws JSONException {
       final JSONObject options = args.getJSONObject(0);
       boolean showUserLocation = !options.isNull("showUserLocation") && options.getBoolean("showUserLocation");
       if (!showUserLocation || requestPermission(command, COARSE_LOCATION, FINE_LOCATION)) {
-        this.createMap(options, callbackContext);
+        final CallbackContext eventCallback = new CallbackContext(args.getString(1), this.webView);
+        this.createMap(options, eventCallback, callbackContext);
       }
     }
 
@@ -373,30 +374,52 @@ public void run() {
     return true;
   }
 
-  private void createMap(final JSONObject options, final CallbackContext callback) {
+  private void createMap(final JSONObject options, final CallbackContext eventCallback, final CallbackContext callback) {
     cordova.getActivity().runOnUiThread(new Runnable() {
       @Override
       public void run() {
-        mapboxManager.createMap(options, callback);
+        Map.MapEventListener eventListener = createEventListener(eventCallback);
+        mapboxManager.createMap(options, eventListener, callback);
       }
     });
   }
 
+  private Map.MapEventListener createEventListener(final CallbackContext callback) {
+    return new Map.MapEventListener() {
+      @Override
+      public void onEvent(String name, JSONObject data) {
+        try {
+          JSONObject event = new JSONObject()
+                  .put("name", name)
+                  .put("data", data);
+          PluginResult result = new PluginResult(PluginResult.Status.OK, event);
+          result.setKeepCallback(true);
+          callback.sendPluginResult(result);
+        } catch (JSONException e) {
+          String message = "Error during map event: " + e.getMessage();
+          PluginResult result = new PluginResult(PluginResult.Status.ERROR, message);
+          result.setKeepCallback(true);
+          callback.sendPluginResult(result);
+        }
+      }
+    };
+  }
+
   public void listOfflineRegions(final CallbackContext callback) {
     cordova.getActivity().runOnUiThread(new Runnable() {
       @Override
       public void run() {
         mapboxManager.loadOfflineRegions(new MapboxManager.LoadOfflineRegionsCallback() {
-            @Override
-            public void onList(JSONArray offlineRegions) {
-                callback.success(offlineRegions);
-            }
+          @Override
+          public void onList(JSONArray offlineRegions) {
+            callback.success(offlineRegions);
+          }
 
-            @Override
-            public void onError(String error) {
-                String message = "Error loading offline regions: " + error;
-                callback.error(message);
-            }
+          @Override
+          public void onError(String error) {
+            String message = "Error loading offline regions: " + error;
+            callback.error(message);
+          }
         });
       }
     });
diff --git a/src/android/MapboxManager.java b/src/android/MapboxManager.java
index c36cf56..61170af 100644
--- a/src/android/MapboxManager.java
+++ b/src/android/MapboxManager.java
@@ -81,13 +81,13 @@ public MapboxManager(String accessToken, Float screenDensity, CordovaWebView web
         this.offlineManager.setAccessToken(accessToken);
     }
 
-    public void createMap(final JSONObject options, final CallbackContext callback) {
+    public void createMap(final JSONObject options, Map.MapEventListener eventListener, final CallbackContext callback) {
         try {
             PositionInfo position = new PositionInfo(options.isNull("margins") ? null : options.getJSONObject("margins"), density);
             MapView mapView = createMapView(Map.createMapboxMapOptions(options), position);
 
             long id = ids++;
-            final Map map = new Map(id, mapView);
+            final Map map = new Map(id, eventListener, mapView);
             mapView.getMapAsync(new OnMapReadyCallback() {
                 @Override
                 public void onMapReady(MapboxMap mMap) {
diff --git a/www/events-mixin.js b/www/events-mixin.js
index 585a9c2..2ee5045 100644
--- a/www/events-mixin.js
+++ b/www/events-mixin.js
@@ -48,6 +48,9 @@ module.exports = function (prefix, target) {
         },
 
         fire: function (type, e) {
+            if (!this._channel(type)) {
+                this.createChannel(type);
+            }
             this._channel(type).fire(e);
         }
     })(target);
diff --git a/www/map-instance.js b/www/map-instance.js
index f2aab1a..ade0e07 100644
--- a/www/map-instance.js
+++ b/www/map-instance.js
@@ -4,11 +4,26 @@ var exec = require("cordova/exec"),
 
 function MapInstance(options) {
     var onLoad = this._onLoad.bind(this),
-        onError = this._error.bind(this);
+        onEvent = _onEvent.bind(this),
+        onError = _onError.bind(this);
 
     this.createStickyChannel("load");
-
-    exec(onLoad, onError, "Mapbox", "createMap", [options]);
+    this._onEventId = this._registerCallback('onEvent', onEvent, onError);
+
+    exec(onLoad, onError, "Mapbox", "createMap", [options, this._onEventId]);
+
+    function _onEvent(event) {
+        console.debug("Event recieved: " + event.name, event.data);
+        this.fire(event.name, event.data);
+    }
+
+    function _onError(error) {
+        try {
+            this.prototype._error.call(this, error);
+        } catch (e) {
+            this.fire("error", e);
+        }
+    }
 }
 
 MapboxPluginAPI('MapInstance', MapInstance.prototype);

From d4125fe78e368bbba4b1825aec4b13bdf2125131 Mon Sep 17 00:00:00 2001
From: Tom Nightingale <tom@tnightingale.com>
Date: Wed, 30 Mar 2016 18:17:27 -0700
Subject: [PATCH 37/37] Added initial attempt at map events.

---
 plugin.xml                            |   3 +
 src/android/FeatureManager.java       |  77 +++++++++++------
 src/android/GeoJSONMarker.java        |  25 ++++++
 src/android/GeoJSONMarkerOptions.java |  75 +++++++++++++++++
 src/android/Map.java                  | 116 ++++++++++++++++++++------
 src/android/Mapbox.java               |  17 ++--
 src/android/mapbox.gradle             |   3 +-
 www/map-events.js                     |  33 ++++++++
 www/map-instance.js                   |  27 +++++-
 9 files changed, 313 insertions(+), 63 deletions(-)
 create mode 100644 src/android/GeoJSONMarker.java
 create mode 100644 src/android/GeoJSONMarkerOptions.java
 create mode 100644 www/map-events.js

diff --git a/plugin.xml b/plugin.xml
index c132f98..1f6111e 100755
--- a/plugin.xml
+++ b/plugin.xml
@@ -27,6 +27,7 @@
   <js-module src="www/mixin.js" name="mixin" />
   <js-module src="www/mapbox-plugin-api-mixin.js" name="mapbox-plugin-api-mixin" />
   <js-module src="www/events-mixin.js" name="events-mixin" />
+  <js-module src="www/map-events.js" name="map-events" />
   <js-module src="www/map-instance.js" name="map-instance" />
   <js-module src="www/offline-region.js" name="offline-region" />
 
@@ -63,6 +64,8 @@
     <source-file src="src/android/OfflineRegion.java" target-dir="src/com/telerik/plugins/mapbox"/>
     <source-file src="src/android/Map.java" target-dir="src/com/telerik/plugins/mapbox"/>
     <source-file src="src/android/FeatureManager.java" target-dir="src/com/telerik/plugins/mapbox"/>
+    <source-file src="src/android/GeoJSONMarker.java" target-dir="src/com/telerik/plugins/mapbox"/>
+    <source-file src="src/android/GeoJSONMarkerOptions.java" target-dir="src/com/telerik/plugins/mapbox"/>
 
     <!-- This leads to trouble in AppBuilder when compiling for Cordova-Android 4 -->
     <!--source-file src="src/android/res/values/mapboxstrings.xml" target-dir="res/values" />
diff --git a/src/android/FeatureManager.java b/src/android/FeatureManager.java
index e1b9575..b333de8 100644
--- a/src/android/FeatureManager.java
+++ b/src/android/FeatureManager.java
@@ -4,6 +4,9 @@
 import android.content.res.AssetManager;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.Nullable;
 import android.util.DisplayMetrics;
 import android.util.Log;
 
@@ -21,6 +24,7 @@
 import com.cocoahero.android.geojson.Polygon;
 import com.cocoahero.android.geojson.Position;
 import com.cocoahero.android.geojson.Ring;
+import com.mapbox.mapboxsdk.annotations.BaseMarkerOptions;
 import com.mapbox.mapboxsdk.annotations.Icon;
 import com.mapbox.mapboxsdk.annotations.IconFactory;
 import com.mapbox.mapboxsdk.annotations.Marker;
@@ -50,6 +54,8 @@ interface DataSource {
 class FeatureManager {
     private String TAG = "FeatureManager";
 
+    protected long ids = 0;
+
     protected Context ctx;
 
     protected IconFactory iconFactory;
@@ -58,7 +64,7 @@ class FeatureManager {
 
     protected HashMap<String, DataSource> sources = new HashMap<String, DataSource>();
 
-    protected HashMap<Long, Feature> markerIndex = new HashMap<Long, Feature>();
+    protected HashMap<Long, GeoJSONMarker> markerIndex = new HashMap<Long, GeoJSONMarker>();
 
     public FeatureManager(Context ctx, MapboxMap mapboxMap) {
         this.ctx = ctx;
@@ -70,22 +76,18 @@ public boolean hasSource(String name) {
         return this.sources.containsKey(name);
     }
 
-    public boolean hasMarkerFeature(Long id) {
+    public boolean hasMarker(Long id) {
         return this.markerIndex.containsKey(id);
     }
 
-    public Feature getMarkerFeature(Long id) {
-        if (this.hasMarkerFeature(id)) {
+    public GeoJSONMarker getMarker(Long id) {
+        if (this.hasMarker(id)) {
             return this.markerIndex.get(id);
         } else {
             return null;
         }
     }
 
-    public Feature getMarkerFeature(Marker marker) {
-        return this.getMarkerFeature(marker.getId());
-    }
-
     public void addGeoJSONSource(String name, String json) throws JSONException {
         this.sources.put(name, new GeoJSONSource(name).addGeoJSON(json));
     }
@@ -132,39 +134,60 @@ public void addMarkerLayer(String id, String source, JSONObject layer) {
         List<Feature> features = this.sources.get(source).getSymbols();
 
         for (Feature feature : features) {
-            MarkerOptions options = this.createMarker(feature, layer);
-            Marker marker = this.mapboxMap.addMarker(options);
-            this.markerIndex.put(marker.getId(), feature);
+            GeoJSONMarkerOptions options = this.createMarker(feature, layer);
+            GeoJSONMarker marker = (GeoJSONMarker) this.mapboxMap.addMarker(options);
+            this.markerIndex.put(marker.getFeatureId(), marker);
         }
     }
 
-    protected MarkerOptions createMarker(Feature feature, JSONObject style) {
-        final JSONObject properties = feature.getProperties();
-        final Position p = ((Point) feature.getGeometry()).getPosition();
-        final MarkerOptions marker = new MarkerOptions()
-            .position(new LatLng(p.getLatitude(), p.getLongitude()));
+    public GeoJSONMarkerOptions createMarker(LatLng latLng, String title, String snippet, @Nullable Icon icon, @Nullable JSONObject properties) {
+        GeoJSONMarkerOptions marker = new GeoJSONMarkerOptions()
+                .title(title)
+                .snippet(snippet)
+                .position(latLng)
+                .featureId(this.ids++);
+
+        if (icon != null) {
+            marker.icon(icon);
+        }
+
+        if (properties != null) {
+            marker.properties(properties);
+        }
+
+        return marker;
+    }
+
+    public GeoJSONMarkerOptions createMarker(LatLng latLng, String title, String snippet) {
+        return createMarker(latLng, title, snippet, null, null);
+    }
+
+    protected GeoJSONMarkerOptions createMarker(Feature feature, JSONObject style) {
+        JSONObject properties = feature.getProperties();
+        Position p = ((Point) feature.getGeometry()).getPosition();
+        LatLng latLng = new LatLng(p.getLatitude(), p.getLongitude());
+        String title = "";
+        String snippet = "";
+        Icon icon = null;
 
         try {
-            final String textField = style.getJSONObject("layout").getString("text-field");
-            marker.title(textField.replace("{title}", properties.getString("title")));
+            String textField = style.getJSONObject("layout").getString("text-field");
+            title = textField.replace("{title}", properties.getString("title"));
         } catch (JSONException e) {
             Log.w(TAG, "Error parsing Style JSON properties: " + e.getMessage());
         }
 
         try {
-            marker.snippet(properties.getString("description"));
+            snippet = properties.getString("description");
         } catch (JSONException e) {
             Log.w(TAG, "Error parsing Style JSON properties: " + e.getMessage());
         }
 
         try {
-            final String iconImage = style.getJSONObject("layout").getString("icon-image");
-            final String markerSymbol = properties.getString("marker-symbol");
-            final URI uri = new URI(iconImage.replace("{marker-symbol}", markerSymbol));
-            final Icon icon = this.loadIcon(uri);
-            if (icon != null) {
-                marker.icon(icon);
-            }
+            String iconImage = style.getJSONObject("layout").getString("icon-image");
+            String markerSymbol = properties.getString("marker-symbol");
+            URI uri = new URI(iconImage.replace("{marker-symbol}", markerSymbol));
+            icon = this.loadIcon(uri);
         } catch (JSONException e) {
             Log.w(TAG, "Error parsing Style JSON properties: " + e.getMessage());
         } catch (URISyntaxException e) {
@@ -173,7 +196,7 @@ protected MarkerOptions createMarker(Feature feature, JSONObject style) {
             Log.w(TAG, "Error loading file: " + e.getMessage());
         }
 
-        return marker;
+        return this.createMarker(latLng, title, snippet, icon, feature.getProperties());
     }
 
     protected Icon loadIcon(URI uri) throws IOException {
diff --git a/src/android/GeoJSONMarker.java b/src/android/GeoJSONMarker.java
new file mode 100644
index 0000000..36c5344
--- /dev/null
+++ b/src/android/GeoJSONMarker.java
@@ -0,0 +1,25 @@
+package com.telerik.plugins.mapbox;
+
+import com.mapbox.mapboxsdk.annotations.Marker;
+
+import org.json.JSONObject;
+
+public class GeoJSONMarker extends Marker {
+    private JSONObject properties;
+
+    private long featureId;
+
+    public GeoJSONMarker(GeoJSONMarkerOptions options, JSONObject properties, long featureId) {
+        super(options);
+        this.properties = properties;
+        this.featureId = featureId;
+    }
+
+    public JSONObject getProperties() {
+        return this.properties != null ? this.properties : new JSONObject();
+    }
+
+    public long getFeatureId() {
+        return this.featureId;
+    }
+}
\ No newline at end of file
diff --git a/src/android/GeoJSONMarkerOptions.java b/src/android/GeoJSONMarkerOptions.java
new file mode 100644
index 0000000..04b6f50
--- /dev/null
+++ b/src/android/GeoJSONMarkerOptions.java
@@ -0,0 +1,75 @@
+package com.telerik.plugins.mapbox;
+
+import android.graphics.Bitmap;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.mapbox.mapboxsdk.annotations.BaseMarkerOptions;
+import com.mapbox.mapboxsdk.annotations.Icon;
+import com.mapbox.mapboxsdk.annotations.IconFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+
+import org.json.JSONObject;
+
+public class GeoJSONMarkerOptions extends BaseMarkerOptions<GeoJSONMarker, GeoJSONMarkerOptions> {
+    private long featureId;
+    private JSONObject properties;
+
+    public GeoJSONMarkerOptions properties(JSONObject properties) {
+        this.properties = properties;
+        return this.getThis();
+    }
+
+    public GeoJSONMarkerOptions featureId(long id) {
+        this.featureId = id;
+        return this.getThis();
+    }
+
+    public GeoJSONMarkerOptions() {
+
+    }
+
+    private GeoJSONMarkerOptions(Parcel in) {
+        position((LatLng) in.readParcelable(LatLng.class.getClassLoader()));
+        snippet(in.readString());
+        String iconId = in.readString();
+        Bitmap iconBitmap = in.readParcelable(Bitmap.class.getClassLoader());
+        Icon icon = IconFactory.recreate(iconId, iconBitmap);
+        icon(icon);
+        title(in.readString());
+    }
+
+    @Override
+    public GeoJSONMarkerOptions getThis() {
+        return this;
+    }
+
+    @Override
+    public GeoJSONMarker getMarker() {
+        return new GeoJSONMarker(this, this.properties, this.featureId);
+    }
+
+    public static final Parcelable.Creator<GeoJSONMarkerOptions> CREATOR = new Parcelable.Creator<GeoJSONMarkerOptions>() {
+        public GeoJSONMarkerOptions createFromParcel(Parcel in) {
+            return new GeoJSONMarkerOptions(in);
+        }
+
+        public GeoJSONMarkerOptions[] newArray(int size) {
+            return new GeoJSONMarkerOptions[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeParcelable(position, flags);
+        out.writeString(snippet);
+        out.writeString(icon.getId());
+        out.writeParcelable(icon.getBitmap(), flags);
+        out.writeString(title);
+    }
+}
\ No newline at end of file
diff --git a/src/android/Map.java b/src/android/Map.java
index 76ef884..33c2ebc 100644
--- a/src/android/Map.java
+++ b/src/android/Map.java
@@ -4,7 +4,6 @@
 import android.support.annotation.Nullable;
 
 import com.mapbox.mapboxsdk.annotations.Marker;
-import com.mapbox.mapboxsdk.annotations.MarkerOptions;
 import com.mapbox.mapboxsdk.camera.CameraPosition;
 import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
 import com.mapbox.mapboxsdk.geometry.LatLng;
@@ -18,8 +17,26 @@
 
 public class Map {
 
+    public static final String JSON_FIELD_ID = "id";
+    public static final String JSON_FIELD_PROPERTIES = "properties";
+    public static final String JSON_FIELD_LNGLAT = "lngLat";
+
+    public static final int EVENT_CAMERACHANGE = 0;
+    public static final int EVENT_FLING = 1;
+    public static final int EVENT_INFOWINDOWCLICK = 2;
+    public static final int EVENT_INFOWINDOWCLOSE = 3;
+    public static final int EVENT_INFOWINDOWLONGCLICK = 4;
+    public static final int EVENT_MAPCLICK = 5;
+    public static final int EVENT_MAPLONGCLICK = 6;
+    public static final int EVENT_MARKERCLICK = 7;
+    public static final int EVENT_BEARINGTRACKINGMODECHANGE = 8;
+    public static final int EVENT_LOCATIONCHANGE = 9;
+    public static final int EVENT_LOCATIONTRACKINGMODECHANGE = 10;
+    public static final int EVENT_ONSCROLL = 11;
+
     public interface MapEventListener {
-        void onEvent(String name, JSONObject event);
+        void onEvent(int code, JSONObject event);
+        void onError(String message);
     }
 
     public static MapboxMapOptions createMapboxMapOptions(JSONObject options) throws JSONException {
@@ -82,92 +99,146 @@ public MapView getMapView() {
         return this.mapView;
     }
 
+    public JSONArray latLngToJSON(LatLng latLng) throws JSONException {
+        return new JSONArray()
+                .put(latLng.getLongitude())
+                .put(latLng.getLatitude());
+    }
+
+    public JSONObject getMarkerJSON(Marker marker) throws JSONException {
+        LatLng point = marker.getPosition();
+        JSONObject data = new JSONObject()
+                .put(JSON_FIELD_LNGLAT, latLngToJSON(point));
+
+        if (marker instanceof GeoJSONMarker) {
+            long featureId = ((GeoJSONMarker) marker).getFeatureId();
+            data.put(JSON_FIELD_ID, featureId);
+            data.put(JSON_FIELD_PROPERTIES, features.getMarker(featureId).getProperties());
+        }
+
+        return data;
+    }
+
     public void setMapboxMap(MapboxMap mMap) {
         this.mapboxMap = mMap;
 
         this.mapboxMap.setOnCameraChangeListener(new MapboxMap.OnCameraChangeListener() {
             @Override
             public void onCameraChange(CameraPosition position) {
-                eventListener.onEvent("camerachange", new JSONObject());
+                eventListener.onEvent(EVENT_CAMERACHANGE, new JSONObject());
             }
         });
 
         this.mapboxMap.setOnFlingListener(new MapboxMap.OnFlingListener() {
             @Override
             public void onFling() {
-                eventListener.onEvent("fling", new JSONObject());
+                eventListener.onEvent(EVENT_FLING, new JSONObject());
             }
         });
 
         this.mapboxMap.setOnInfoWindowClickListener(new MapboxMap.OnInfoWindowClickListener() {
             @Override
             public boolean onInfoWindowClick(Marker marker) {
-                eventListener.onEvent("infowindowclick", new JSONObject());
-                return true;
+                try {
+                    JSONObject data = getMarkerJSON(marker);
+                    eventListener.onEvent(EVENT_INFOWINDOWCLICK, data);
+                    return true;
+                } catch (JSONException e) {
+                    eventListener.onError(e.getMessage());
+                    return false;
+                }
             }
         });
 
         this.mapboxMap.setOnInfoWindowCloseListener(new MapboxMap.OnInfoWindowCloseListener() {
             @Override
             public void onInfoWindowClose(Marker marker) {
-                eventListener.onEvent("infowindowclose", new JSONObject());
+                try {
+                    JSONObject data = getMarkerJSON(marker);
+                    eventListener.onEvent(EVENT_INFOWINDOWCLOSE, data);
+                } catch (JSONException e) {
+                    eventListener.onError(e.getMessage());
+                }
             }
         });
 
         this.mapboxMap.setOnInfoWindowLongClickListener(new MapboxMap.OnInfoWindowLongClickListener() {
             @Override
             public void onInfoWindowLongClick(Marker marker) {
-                eventListener.onEvent("infowindowlongclick", new JSONObject());
+                try {
+                    JSONObject data = getMarkerJSON(marker);
+                    eventListener.onEvent(EVENT_INFOWINDOWLONGCLICK, data);
+                } catch (JSONException e) {
+                    eventListener.onError(e.getMessage());
+                }
             }
         });
 
         this.mapboxMap.setOnMapClickListener(new MapboxMap.OnMapClickListener() {
             @Override
             public void onMapClick(LatLng point) {
-                eventListener.onEvent("mapclick", new JSONObject());
+                try {
+                    JSONObject data = new JSONObject()
+                            .put("lngLat", latLngToJSON(point));
+                    eventListener.onEvent(EVENT_MAPCLICK, data);
+                } catch (JSONException e) {
+                    eventListener.onError(e.getMessage());
+                }
             }
         });
 
         this.mapboxMap.setOnMapLongClickListener(new MapboxMap.OnMapLongClickListener() {
             @Override
             public void onMapLongClick(LatLng point) {
-                eventListener.onEvent("maplongclick", new JSONObject());
+                try {
+                    JSONObject data = new JSONObject()
+                            .put("lngLat", latLngToJSON(point));
+                    eventListener.onEvent(EVENT_MAPLONGCLICK, data);
+                } catch (JSONException e) {
+                    eventListener.onError(e.getMessage());
+                }
             }
         });
 
         this.mapboxMap.setOnMarkerClickListener(new MapboxMap.OnMarkerClickListener() {
             @Override
             public boolean onMarkerClick(Marker marker) {
-                eventListener.onEvent("markerclick", new JSONObject());
-                return true;
+                try {
+                    JSONObject data = getMarkerJSON(marker);
+                    eventListener.onEvent(EVENT_MARKERCLICK, data);
+                    return true;
+                } catch (JSONException e) {
+                    eventListener.onError(e.getMessage());
+                    return false;
+                }
             }
         });
 
         this.mapboxMap.setOnMyBearingTrackingModeChangeListener(new MapboxMap.OnMyBearingTrackingModeChangeListener() {
             @Override
             public void onMyBearingTrackingModeChange(int myBearingTrackingMode) {
-                eventListener.onEvent("bearingtrackingmodechange", new JSONObject());
+                eventListener.onEvent(EVENT_BEARINGTRACKINGMODECHANGE, new JSONObject());
             }
         });
 
         this.mapboxMap.setOnMyLocationChangeListener(new MapboxMap.OnMyLocationChangeListener() {
             @Override
             public void onMyLocationChange(@Nullable Location location) {
-                eventListener.onEvent("locationchange", new JSONObject());
+                eventListener.onEvent(EVENT_LOCATIONCHANGE, new JSONObject());
             }
         });
 
         this.mapboxMap.setOnMyLocationTrackingModeChangeListener(new MapboxMap.OnMyLocationTrackingModeChangeListener() {
             @Override
             public void onMyLocationTrackingModeChange(int myLocationTrackingMode) {
-                eventListener.onEvent("locationtrackingmodechange", new JSONObject());
+                eventListener.onEvent(EVENT_LOCATIONTRACKINGMODECHANGE, new JSONObject());
             }
         });
 
         this.mapboxMap.setOnScrollListener(new MapboxMap.OnScrollListener() {
             @Override
             public void onScroll() {
-                eventListener.onEvent("onscroll", new JSONObject());
+                eventListener.onEvent(EVENT_ONSCROLL, new JSONObject());
             }
         });
     }
@@ -220,14 +291,11 @@ public void jumpTo(JSONObject options) throws JSONException {
 
     public void addMarkers(JSONArray markers) throws JSONException {
         for (int i = 0; i < markers.length(); i++) {
-            final JSONObject marker = markers.getJSONObject(i);
-            final MarkerOptions mo = new MarkerOptions();
-
-            mo.title(marker.isNull("title") ? null : marker.getString("title"));
-            mo.snippet(marker.isNull("subtitle") ? null : marker.getString("subtitle"));
-            mo.position(new LatLng(marker.getDouble("lat"), marker.getDouble("lng")));
-
-            mapboxMap.addMarker(mo);
+            JSONObject marker = markers.getJSONObject(i);
+            LatLng latLng = new LatLng(marker.getDouble("lat"), marker.getDouble("lng"));
+            String title = marker.isNull("title") ? null : marker.getString("title");
+            String snippet = marker.isNull("subtitle") ? null : marker.getString("subtitle");
+            mapboxMap.addMarker(features.createMarker(latLng, title, snippet));
         }
     }
 
diff --git a/src/android/Mapbox.java b/src/android/Mapbox.java
index 9a6a219..cc4d449 100644
--- a/src/android/Mapbox.java
+++ b/src/android/Mapbox.java
@@ -387,21 +387,26 @@ public void run() {
   private Map.MapEventListener createEventListener(final CallbackContext callback) {
     return new Map.MapEventListener() {
       @Override
-      public void onEvent(String name, JSONObject data) {
+      public void onEvent(int code, JSONObject data) {
         try {
           JSONObject event = new JSONObject()
-                  .put("name", name)
+                  .put("code", code)
                   .put("data", data);
           PluginResult result = new PluginResult(PluginResult.Status.OK, event);
           result.setKeepCallback(true);
           callback.sendPluginResult(result);
         } catch (JSONException e) {
-          String message = "Error during map event: " + e.getMessage();
-          PluginResult result = new PluginResult(PluginResult.Status.ERROR, message);
-          result.setKeepCallback(true);
-          callback.sendPluginResult(result);
+          this.onError(e.getMessage());
         }
       }
+
+      @Override
+      public void onError(String error) {
+        String message = "Error during map event: " + error;
+        PluginResult result = new PluginResult(PluginResult.Status.ERROR, message);
+        result.setKeepCallback(true);
+        callback.sendPluginResult(result);
+      }
     };
   }
 
diff --git a/src/android/mapbox.gradle b/src/android/mapbox.gradle
index 1ce0310..d2c694b 100644
--- a/src/android/mapbox.gradle
+++ b/src/android/mapbox.gradle
@@ -2,13 +2,12 @@ ext.cdvMinSdkVersion = 15
 
 repositories {
     mavenCentral()
-    maven { url "http://oss.sonatype.org/content/repositories/snapshots/" }
 }
 
 dependencies {
     compile 'com.cocoahero.android:geojson:1.0.1@jar'
     compile 'com.android.support:appcompat-v7:23.0.1'
-    compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:4.0.0-SNAPSHOT@aar'){
+    compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:4.0.0@aar'){
         transitive=true
     }
 }
diff --git a/www/map-events.js b/www/map-events.js
new file mode 100644
index 0000000..92e0076
--- /dev/null
+++ b/www/map-events.js
@@ -0,0 +1,33 @@
+var codes = {
+        CAMERACHANGE: 0,
+        FLING: 1,
+        INFOWINDOWCLICK: 2,
+        INFOWINDOWCLOSE: 3,
+        INFOWINDOWLONGCLICK: 4,
+        MAPCLICK: 5,
+        MAPLONGCLICK: 6,
+        MARKERCLICK: 7,
+        BEARINGTRACKINGMODECHANGE: 8,
+        LOCATIONCHANGE: 9,
+        LOCATIONTRACKINGMODECHANGE: 10,
+        ONSCROLL: 11,
+    },
+    events = {};
+
+events[codes.CAMERACHANGE] = {name: "camerachange"};
+events[codes.FLING] = {name: "fling"};
+events[codes.INFOWINDOWCLICK] = {name: "infowindowclick"};
+events[codes.INFOWINDOWCLOSE] = {name: "infowindowclose"};
+events[codes.INFOWINDOWLONGCLICK] = {name: "infowindowlongclick"};
+events[codes.MAPCLICK] = {name: "mapclick"};
+events[codes.MAPLONGCLICK] = {name: "maplongclick"};
+events[codes.MARKERCLICK] = {name: "markerclick"};
+events[codes.BEARINGTRACKINGMODECHANGE] = {name: "bearingtrackingmodechange"};
+events[codes.LOCATIONCHANGE] = {name: "locationchange"};
+events[codes.LOCATIONTRACKINGMODECHANGE] = {name: "locationtrackingmodechange"};
+events[codes.ONSCROLL] = {name: "onscroll"};
+
+module.exports = {
+    events: events,
+    codes: codes
+};
diff --git a/www/map-instance.js b/www/map-instance.js
index ade0e07..dc6139f 100644
--- a/www/map-instance.js
+++ b/www/map-instance.js
@@ -1,6 +1,7 @@
 var exec = require("cordova/exec"),
     MapboxPluginAPI = require("./mapbox-plugin-api-mixin"),
-    EventsMixin = require("./events-mixin");
+    EventsMixin = require("./events-mixin"),
+    E = require("./map-events");
 
 function MapInstance(options) {
     var onLoad = this._onLoad.bind(this),
@@ -12,9 +13,20 @@ function MapInstance(options) {
 
     exec(onLoad, onError, "Mapbox", "createMap", [options, this._onEventId]);
 
-    function _onEvent(event) {
-        console.debug("Event recieved: " + event.name, event.data);
-        this.fire(event.name, event.data);
+    function _onEvent(e) {
+        var event = E.events[e.code];
+        switch (e.code) {
+            case E.codes.INFOWINDOWCLICK:
+            case E.codes.INFOWINDOWCLOSE:
+            case E.codes.INFOWINDOWLONGCLICK:
+            case E.codes.MARKERCLICK:
+                var marker = new Marker(e.data.id);
+                console.debug("Event recieved: " + event.name, marker);
+                return this.fire(event.name, marker);
+            default:
+                console.debug("Event recieved: " + event.name, e.data);
+                return this.fire(event.name, e.data);
+        }
     }
 
     function _onError(error) {
@@ -84,4 +96,11 @@ MapInstance.prototype._onLoad = function (resp) {
     this.fire("load", {map: this});
 };
 
+function Marker(id) {
+    this._id = id;
+}
+
+MapboxPluginAPI('Marker', Marker.prototype);
+EventsMixin('Marker', Marker.prototype);
+
 module.exports = MapInstance;