From 4001fbb51a16b2affcebd77bb69e9d241f41706c Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 1 Sep 2017 09:58:36 -0700 Subject: [PATCH] Map events and methods --- android/rctmgl/build.gradle | 8 +- .../components/AbstractEventEmitter.java | 25 +- .../components/mapview/RCTMGLMapView.java | 214 ++++++++++--- .../mapview/RCTMGLMapViewManager.java | 70 +++-- ...actRCTMGLEvent.java => AbstractEvent.java} | 14 +- .../events/{IRCTMGLEvent.java => IEvent.java} | 4 +- .../mapbox/rctmgl/events/MapChangeEvent.java | 35 +++ .../mapbox/rctmgl/events/MapClickEvent.java | 51 ++++ .../rctmgl/events/RCTMGLMapChangeEvent.java | 18 -- .../rctmgl/events/RCTMGLMapClickEvent.java | 47 --- .../events/UserLocationChangeEvent.java | 44 +++ .../EventKeys.java} | 6 +- .../EventTypes.java} | 6 +- .../mapbox/rctmgl/modules/RCTMGLModule.java | 115 ++++--- .../mapbox/rctmgl/utils/ConvertUtils.java | 78 +++++ .../java/mapbox/rctmgl/utils/MGLGeoUtils.java | 66 ---- .../rctmgl/utils/RCTMGLMapStyleURL.java | 41 --- .../rctmgl/utils/SimpleEventCallback.java | 30 ++ ios/RCTMGL.xcodeproj/project.pbxproj | 56 ++-- ios/RCTMGL/MGLModule.m | 45 +++ ios/RCTMGL/RCTMGLEvent.h | 1 + ios/RCTMGL/RCTMGLEvent.m | 8 + ios/RCTMGL/RCTMGLEventTypes.h | 3 + ios/RCTMGL/RCTMGLEventTypes.m | 3 + ios/RCTMGL/RCTMGLMapTouchEvent.h | 3 +- ios/RCTMGL/RCTMGLMapTouchEvent.m | 12 +- ios/RCTMGL/RCTMGLMapView.h | 6 +- ios/RCTMGL/RCTMGLMapView.m | 48 ++- ios/RCTMGL/RCTMGLMapViewManager.h | 4 +- ios/RCTMGL/RCTMGLMapViewManager.m | 125 +++++++- ios/RCTMGL/RCTMGLUtils.h | 5 +- ios/RCTMGL/RCTMGLUtils.m | 37 ++- ios/RCTMGL/ViewManager.h | 16 + ios/RCTMGL/ViewManager.m | 52 ++++ javascript/components/MapView.js | 287 ++++++++++-------- javascript/index.js | 7 +- javascript/utils/geoUtils.js | 12 + package.json | 2 +- 38 files changed, 1113 insertions(+), 491 deletions(-) rename android/rctmgl/src/main/java/mapbox/rctmgl/events/{AbstractRCTMGLEvent.java => AbstractEvent.java} (64%) rename android/rctmgl/src/main/java/mapbox/rctmgl/events/{IRCTMGLEvent.java => IEvent.java} (74%) create mode 100644 android/rctmgl/src/main/java/mapbox/rctmgl/events/MapChangeEvent.java create mode 100644 android/rctmgl/src/main/java/mapbox/rctmgl/events/MapClickEvent.java delete mode 100644 android/rctmgl/src/main/java/mapbox/rctmgl/events/RCTMGLMapChangeEvent.java delete mode 100644 android/rctmgl/src/main/java/mapbox/rctmgl/events/RCTMGLMapClickEvent.java create mode 100644 android/rctmgl/src/main/java/mapbox/rctmgl/events/UserLocationChangeEvent.java rename android/rctmgl/src/main/java/mapbox/rctmgl/events/{RCTMGLEventKeys.java => constants/EventKeys.java} (73%) rename android/rctmgl/src/main/java/mapbox/rctmgl/events/{RCTMGLEventTypes.java => constants/EventTypes.java} (85%) create mode 100644 android/rctmgl/src/main/java/mapbox/rctmgl/utils/ConvertUtils.java delete mode 100644 android/rctmgl/src/main/java/mapbox/rctmgl/utils/MGLGeoUtils.java delete mode 100644 android/rctmgl/src/main/java/mapbox/rctmgl/utils/RCTMGLMapStyleURL.java create mode 100644 android/rctmgl/src/main/java/mapbox/rctmgl/utils/SimpleEventCallback.java create mode 100644 ios/RCTMGL/ViewManager.h create mode 100644 ios/RCTMGL/ViewManager.m create mode 100644 javascript/utils/geoUtils.js diff --git a/android/rctmgl/build.gradle b/android/rctmgl/build.gradle index ed99ac5..6e33827 100644 --- a/android/rctmgl/build.gradle +++ b/android/rctmgl/build.gradle @@ -22,10 +22,16 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - // deps + // React Native provided "com.facebook.react:react-native:+" + // Mapbox SDK + compile 'com.mapbox.mapboxsdk:mapbox-android-services:2.2.2' + compile('com.mapbox.mapboxsdk:mapbox-android-sdk:5.1.3@aar') { transitive=true } + + // Mapbox plugins + compile 'com.mapbox.mapboxsdk:mapbox-android-plugin-locationlayer:0.1.0' } diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/components/AbstractEventEmitter.java b/android/rctmgl/src/main/java/mapbox/rctmgl/components/AbstractEventEmitter.java index 137d86b..71d2a78 100644 --- a/android/rctmgl/src/main/java/mapbox/rctmgl/components/AbstractEventEmitter.java +++ b/android/rctmgl/src/main/java/mapbox/rctmgl/components/AbstractEventEmitter.java @@ -3,7 +3,6 @@ package mapbox.rctmgl.components; import android.view.ViewGroup; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.events.RCTEventEmitter; @@ -13,20 +12,31 @@ import java.util.Map; import javax.annotation.Nullable; -import mapbox.rctmgl.events.IRCTMGLEvent; +import mapbox.rctmgl.events.IEvent; /** * Created by nickitaliano on 8/23/17. */ abstract public class AbstractEventEmitter extends ViewGroupManager { + private static final double BRIDGE_TIMEOUT_MS = 100; + private Map mRateLimitedEvents; private ReactApplicationContext mRCTAppContext; public AbstractEventEmitter(ReactApplicationContext reactApplicationContext) { mRCTAppContext = reactApplicationContext; + mRateLimitedEvents = new HashMap<>(); } - public void handleEvent(IRCTMGLEvent event) { + public void handleEvent(IEvent event) { + String eventCacheKey = getEventCacheKey(event); + + // fail safe to protect bridge from being spammed + if (shouldDropEvent(eventCacheKey, event)) { + return; + } + + mRateLimitedEvents.put(eventCacheKey, System.currentTimeMillis()); getEventEmitter().receiveEvent(event.getID(), event.getKey(), event.toJSON()); } @@ -48,4 +58,13 @@ abstract public class AbstractEventEmitter extends ViewGrou private RCTEventEmitter getEventEmitter() { return mRCTAppContext.getJSModule(RCTEventEmitter.class); } + + private boolean shouldDropEvent(String cacheKey, IEvent event) { + Long lastEventTimestamp = mRateLimitedEvents.get(cacheKey); + return lastEventTimestamp != null && (event.getTimestamp() - lastEventTimestamp) <= BRIDGE_TIMEOUT_MS; + } + + private String getEventCacheKey(IEvent event) { + return String.format("%s-%s", event.getKey(), event.getType()); + } } diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/components/mapview/RCTMGLMapView.java b/android/rctmgl/src/main/java/mapbox/rctmgl/components/mapview/RCTMGLMapView.java index f9a5be8..0462b91 100644 --- a/android/rctmgl/src/main/java/mapbox/rctmgl/components/mapview/RCTMGLMapView.java +++ b/android/rctmgl/src/main/java/mapbox/rctmgl/components/mapview/RCTMGLMapView.java @@ -1,48 +1,68 @@ package mapbox.rctmgl.components.mapview; import android.content.Context; +import android.graphics.PointF; +import android.location.Location; import android.support.annotation.NonNull; import android.util.Log; -import android.view.View; import android.widget.RelativeLayout; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; import com.mapbox.mapboxsdk.camera.CameraPosition; import com.mapbox.mapboxsdk.camera.CameraUpdate; import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; 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.MapboxMapOptions; import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; import com.mapbox.mapboxsdk.maps.UiSettings; +import com.mapbox.mapboxsdk.plugins.locationlayer.LocationLayerPlugin; +import com.mapbox.services.android.location.LostLocationEngine; +import com.mapbox.services.android.telemetry.location.LocationEngine; +import com.mapbox.services.android.telemetry.location.LocationEngineListener; +import com.mapbox.services.android.telemetry.location.LocationEnginePriority; +import com.mapbox.services.commons.geojson.FeatureCollection; import com.mapbox.services.commons.geojson.Point; -import mapbox.rctmgl.events.IRCTMGLEvent; -import mapbox.rctmgl.events.RCTMGLEventTypes; -import mapbox.rctmgl.events.RCTMGLMapChangeEvent; -import mapbox.rctmgl.utils.MGLGeoUtils; -import mapbox.rctmgl.events.RCTMGLMapClickEvent; +import mapbox.rctmgl.events.IEvent; +import mapbox.rctmgl.events.MapChangeEvent; +import mapbox.rctmgl.events.MapClickEvent; +import mapbox.rctmgl.events.UserLocationChangeEvent; +import mapbox.rctmgl.events.constants.EventTypes; +import mapbox.rctmgl.utils.ConvertUtils; +import mapbox.rctmgl.utils.SimpleEventCallback; /** * Created by nickitaliano on 8/18/17. */ +@SuppressWarnings({"MissingPermission"}) public class RCTMGLMapView extends RelativeLayout implements OnMapReadyCallback, MapboxMap.OnMapClickListener, MapboxMap.OnMapLongClickListener, - MapView.OnMapChangedListener + MapView.OnMapChangedListener, LocationEngineListener { public static final String LOG_TAG = RCTMGLMapView.class.getSimpleName(); private RCTMGLMapViewManager mManager; + private Context mContext; private MapboxMap mMap; private MapView mMapView; + private LocationEngine mLocationEngine; + private LocationLayerPlugin mLocationLayer; private String mStyleURL; private boolean mAnimated; private boolean mScrollEnabled; private boolean mPitchEnabled; + private boolean mShowUserLocation; + + private int mUserTrackingMode; private double mHeading; private double mPitch; @@ -55,9 +75,17 @@ public class RCTMGLMapView extends RelativeLayout implements public RCTMGLMapView(Context context, RCTMGLMapViewManager manager) { super(context); + mContext = context; mManager = manager; } + public void dispose() { + if (mLocationEngine != null) { + mLocationEngine.removeLocationEngineListener(this); + mLocationEngine.deactivate(); + } + } + //region Map Callbacks @Override @@ -67,6 +95,7 @@ public class RCTMGLMapView extends RelativeLayout implements return; } mMap = mapboxMap; + mMap.setOnMapClickListener(this); mMap.setOnMapLongClickListener(this); mMapView.addOnMapChangedListener(this); @@ -74,67 +103,75 @@ public class RCTMGLMapView extends RelativeLayout implements // in case props were set before the map was ready lets set them updateUISettings(); setMinMaxZoomLevels(); + + if (mShowUserLocation) { + enableLocationLayer(); + } } @Override public void onMapClick(@NonNull LatLng point) { - RCTMGLMapClickEvent event = new RCTMGLMapClickEvent(this); - event.setLatLng(point); + PointF screenPoint = mMap.getProjection().toScreenLocation(point); + MapClickEvent event = new MapClickEvent(this, point, screenPoint); mManager.handleEvent(event); } @Override public void onMapLongClick(@NonNull LatLng point) { - RCTMGLMapClickEvent event = new RCTMGLMapClickEvent(this, RCTMGLEventTypes.MAP_LONG_CLICK); - event.setLatLng(point); + PointF screenPoint = mMap.getProjection().toScreenLocation(point); + MapClickEvent event = new MapClickEvent(this, point, screenPoint, EventTypes.MAP_LONG_CLICK); mManager.handleEvent(event); } @Override public void onMapChanged(int changed) { - IRCTMGLEvent event = null; + IEvent event = null; switch (changed) { case MapView.REGION_WILL_CHANGE: + event = new MapChangeEvent(this, makeRegionPayload(false), EventTypes.REGION_WILL_CHANGE); + break; case MapView.REGION_WILL_CHANGE_ANIMATED: - event = new RCTMGLMapChangeEvent(this, RCTMGLEventTypes.REGION_WILL_CHANGE); + event = new MapChangeEvent(this, makeRegionPayload(true), EventTypes.REGION_WILL_CHANGE); break; case MapView.REGION_IS_CHANGING: - event = new RCTMGLMapChangeEvent(this, RCTMGLEventTypes.REGION_IS_CHANGING); + event = new MapChangeEvent(this, EventTypes.REGION_IS_CHANGING); break; case MapView.REGION_DID_CHANGE: + event = new MapChangeEvent(this, makeRegionPayload(false), EventTypes.REGION_WILL_CHANGE); + break; case MapView.REGION_DID_CHANGE_ANIMATED: - event = new RCTMGLMapChangeEvent(this, RCTMGLEventTypes.REGION_DID_CHANGE); + event = new MapChangeEvent(this, makeRegionPayload(true), EventTypes.REGION_DID_CHANGE); break; case MapView.WILL_START_LOADING_MAP: - event = new RCTMGLMapChangeEvent(this, RCTMGLEventTypes.WILL_START_LOADING_MAP); + event = new MapChangeEvent(this, EventTypes.WILL_START_LOADING_MAP); break; case MapView.DID_FAIL_LOADING_MAP: - event = new RCTMGLMapChangeEvent(this, RCTMGLEventTypes.DID_FAIL_LOADING_MAP); + event = new MapChangeEvent(this, EventTypes.DID_FAIL_LOADING_MAP); break; case MapView.DID_FINISH_LOADING_MAP: - event = new RCTMGLMapChangeEvent(this, RCTMGLEventTypes.DID_FINISH_LOADING_MAP); + event = new MapChangeEvent(this, EventTypes.DID_FINISH_LOADING_MAP); break; case MapView.WILL_START_RENDERING_FRAME: - event = new RCTMGLMapChangeEvent(this, RCTMGLEventTypes.WILL_START_RENDERING_FRAME); + event = new MapChangeEvent(this, EventTypes.WILL_START_RENDERING_FRAME); break; case MapView.DID_FINISH_RENDERING_FRAME: - event = new RCTMGLMapChangeEvent(this, RCTMGLEventTypes.DID_FINISH_RENDERING_FRAME); + event = new MapChangeEvent(this, EventTypes.DID_FINISH_RENDERING_FRAME); break; case MapView.DID_FINISH_RENDERING_FRAME_FULLY_RENDERED: - event = new RCTMGLMapChangeEvent(this, RCTMGLEventTypes.DID_FINISH_RENDERING_FRAME_FULLY); + event = new MapChangeEvent(this, EventTypes.DID_FINISH_RENDERING_FRAME_FULLY); break; case MapView.WILL_START_RENDERING_MAP: - event = new RCTMGLMapChangeEvent(this, RCTMGLEventTypes.WILL_START_RENDERING_MAP); + event = new MapChangeEvent(this, EventTypes.WILL_START_RENDERING_MAP); break; case MapView.DID_FINISH_RENDERING_MAP: - event = new RCTMGLMapChangeEvent(this, RCTMGLEventTypes.DID_FINISH_RENDERING_MAP); + event = new MapChangeEvent(this, EventTypes.DID_FINISH_RENDERING_MAP); break; case MapView.DID_FINISH_RENDERING_MAP_FULLY_RENDERED: - event = new RCTMGLMapChangeEvent(this, RCTMGLEventTypes.DID_FINISH_RENDERING_MAP_FULLY); + event = new MapChangeEvent(this, EventTypes.DID_FINISH_RENDERING_MAP_FULLY); break; case MapView.DID_FINISH_LOADING_STYLE: - event = new RCTMGLMapChangeEvent(this, RCTMGLEventTypes.DID_FINISH_LOADING_STYLE); + event = new MapChangeEvent(this, EventTypes.DID_FINISH_LOADING_STYLE); break; } @@ -200,33 +237,102 @@ public class RCTMGLMapView extends RelativeLayout implements updateCameraPositionIfNeeded(true); } + public void setShowUserLocation(boolean showUserLocation) { + mShowUserLocation = showUserLocation; + + if (mLocationEngine != null) { + // deactive location engine if we are hiding the location layer + if (!mShowUserLocation) { + mLocationEngine.deactivate(); + return; + } + + if (mMap != null) { + enableLocationLayer(); + } + } + } + + public void setUserTrackingMode(int userTrackingMode) { + mUserTrackingMode = userTrackingMode; + + if (mLocationLayer != null) { + mLocationLayer.setLocationLayerEnabled(mUserTrackingMode); + } + } + //endregion //region Methods public void flyTo(Point flyToPoint, int durationMS) { - final IRCTMGLEvent event = new RCTMGLMapChangeEvent(this, RCTMGLEventTypes.FLY_TO_COMPLETE); + final IEvent event = new MapChangeEvent(this, EventTypes.FLY_TO_COMPLETE); CameraPosition nextPosition = new CameraPosition.Builder(mMap.getCameraPosition()) - .target(MGLGeoUtils.pointToLatLng(flyToPoint)) + .target(ConvertUtils.toLatLng(flyToPoint)) .build(); CameraUpdate flyToUpdate = CameraUpdateFactory.newCameraPosition(nextPosition); - mMap.animateCamera(flyToUpdate, durationMS, new MapboxMap.CancelableCallback() { - @Override - public void onCancel() { - mManager.handleEvent(event); - } + mMap.animateCamera(flyToUpdate, durationMS, new SimpleEventCallback(mManager, event)); + } - @Override - public void onFinish() { - mManager.handleEvent(event); - } - }); + public void setCamera(ReadableMap map) { + final IEvent event = new MapChangeEvent(this, EventTypes.SET_CAMERA_COMPLETE); + CameraPosition.Builder builder = new CameraPosition.Builder(mMap.getCameraPosition()); + + if (map.hasKey("pitch")) { + builder.tilt(map.getDouble("pitch")); + } + + if (map.hasKey("heading")) { + builder.bearing(map.getDouble("heading")); + } + + if (map.hasKey("centerCoordinate")) { + Point target = ConvertUtils.toPointGemetry(map.getString("centerCoordinate")); + builder.target(ConvertUtils.toLatLng(target)); + } + + int durationMS = 2000; + if (map.hasKey("duration")) { + durationMS = map.getInt("duration"); + } + + CameraUpdate update = CameraUpdateFactory.newCameraPosition(builder.build()); + mMap.easeCamera(update, durationMS); + mMap.easeCamera(update, durationMS, new SimpleEventCallback(mManager, event)); + } + + public void fitBounds(FeatureCollection featureCollection, int padding, int durationMS) { + LatLngBounds bounds = ConvertUtils.toLatLngBounds(featureCollection); + if (bounds == null) { + return; + } + mMap.easeCamera(CameraUpdateFactory.newLatLngBounds(bounds, padding), durationMS); } //endregion + @Override + public void onConnected() { + mLocationEngine.requestLocationUpdates(); + + Location location = mLocationEngine.getLastLocation(); + if (location != null) { + LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); + mMap.moveCamera(CameraUpdateFactory.newLatLng(latLng)); + } + } + + @Override + public void onLocationChanged(Location location) { + IEvent event = new UserLocationChangeEvent(this, location); + mManager.handleEvent(event); + + LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); + mMap.animateCamera(CameraUpdateFactory.newLatLng(latLng)); + } + public void makeView() { if (mMapView != null) { return; @@ -269,7 +375,7 @@ public class RCTMGLMapView extends RelativeLayout implements .zoom(mZoomLevel); if (shouldUpdateTarget) { - builder.target(MGLGeoUtils.pointToLatLng(mCenterCoordinate)); + builder.target(ConvertUtils.toLatLng(mCenterCoordinate)); } return builder.build(); @@ -279,6 +385,7 @@ public class RCTMGLMapView extends RelativeLayout implements if (mMap == null) { return; } + // Gesture settings UiSettings uiSettings = mMap.getUiSettings(); uiSettings.setScrollGesturesEnabled(mScrollEnabled); uiSettings.setTiltGesturesEnabled(mPitchEnabled); @@ -297,4 +404,35 @@ public class RCTMGLMapView extends RelativeLayout implements mMap.setMaxZoomPreference(mMaxZoomLevel); } } + + private void enableLocationLayer() { + if (mLocationEngine == null) { + mLocationEngine = LostLocationEngine.getLocationEngine(mContext); + mLocationEngine.setPriority(LocationEnginePriority.HIGH_ACCURACY); + mLocationEngine.addLocationEngineListener(this); + mLocationEngine.activate(); + } + + if (mLocationLayer == null) { + mLocationLayer = new LocationLayerPlugin(mMapView, mMap, mLocationEngine); + + } + + if (mUserTrackingMode != mLocationLayer.getLocationLayerMode()) { + mLocationLayer.setLocationLayerEnabled(mUserTrackingMode); + } + } + + private WritableMap makeRegionPayload(boolean isAnimated) { + CameraPosition position = mMap.getCameraPosition(); + LatLng latLng = new LatLng(position.target.getLatitude(), position.target.getLongitude()); + + WritableMap properties = new WritableNativeMap(); + properties.putDouble("zoomLevel", position.zoom); + properties.putDouble("heading", position.bearing); + properties.putDouble("pitch", position.tilt); + properties.putBoolean("animated", isAnimated); + + return ConvertUtils.toPointFeature(latLng, properties); + } } diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/components/mapview/RCTMGLMapViewManager.java b/android/rctmgl/src/main/java/mapbox/rctmgl/components/mapview/RCTMGLMapViewManager.java index 1e4f705..b07ff57 100644 --- a/android/rctmgl/src/main/java/mapbox/rctmgl/components/mapview/RCTMGLMapViewManager.java +++ b/android/rctmgl/src/main/java/mapbox/rctmgl/components/mapview/RCTMGLMapViewManager.java @@ -1,12 +1,11 @@ package mapbox.rctmgl.components.mapview; -import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.annotations.ReactProp; +import com.mapbox.services.commons.geojson.FeatureCollection; import com.mapbox.services.commons.geojson.Point; import java.util.Map; @@ -14,10 +13,8 @@ import java.util.Map; import javax.annotation.Nullable; import mapbox.rctmgl.components.AbstractEventEmitter; -import mapbox.rctmgl.events.RCTMGLEventKeys; -import mapbox.rctmgl.utils.MGLGeoUtils; -import mapbox.rctmgl.utils.RCTMGLMapStyleURL; -import mapbox.rctmgl.events.RCTMGLEventTypes; +import mapbox.rctmgl.events.constants.EventKeys; +import mapbox.rctmgl.utils.ConvertUtils; /** * Created by nickitaliano on 8/18/17. @@ -46,17 +43,15 @@ public class RCTMGLMapViewManager extends AbstractEventEmitter { return new RCTMGLMapView(themedReactContext, this); } + @Override + public void onDropViewInstance(RCTMGLMapView mapView) { + mapView.dispose(); + } + //region React Props @ReactProp(name="styleURL") public void setStyleURL(RCTMGLMapView mapView, String styleURL) { - RCTMGLMapStyleURL mapStyle = RCTMGLMapStyleURL.fromKey(styleURL); - - if (mapStyle != null) { - mapView.setStyleURL(mapStyle.getURL()); - return; - } - mapView.setStyleURL(styleURL); } @@ -101,14 +96,23 @@ public class RCTMGLMapViewManager extends AbstractEventEmitter { } @ReactProp(name="centerCoordinate") - public void setCenterCoordinate(RCTMGLMapView mapView, ReadableMap map) { - Point centerCoordinate = MGLGeoUtils.readableMapToPoint(map); - + public void setCenterCoordinate(RCTMGLMapView mapView, String featureJSONStr) { + Point centerCoordinate = ConvertUtils.toPointGemetry(featureJSONStr); if (centerCoordinate != null) { mapView.setCenterCoordinate(centerCoordinate); } } + @ReactProp(name="showUserLocation") + public void setShowUserLocation(RCTMGLMapView mapView, boolean showUserLocation) { + mapView.setShowUserLocation(showUserLocation); + } + + @ReactProp(name="userTrackingMode") + public void setUserTrackingMode(RCTMGLMapView mapView, int userTrackingMode) { + mapView.setUserTrackingMode(userTrackingMode); + } + //endregion //region Custom Events @@ -116,9 +120,10 @@ public class RCTMGLMapViewManager extends AbstractEventEmitter { @Override public Map customEvents() { return MapBuilder.builder() - .put(RCTMGLEventKeys.MAP_CLICK, "onPress") - .put(RCTMGLEventKeys.MAP_LONG_CLICK,"onLongPress") - .put(RCTMGLEventKeys.MAP_ONCHANGE, "onMapChange") + .put(EventKeys.MAP_CLICK, "onPress") + .put(EventKeys.MAP_LONG_CLICK,"onLongPress") + .put(EventKeys.MAP_ONCHANGE, "onMapChange") + .put(EventKeys.USER_LOCATION_CHANGE, "onUserLocationChange") .build(); } @@ -127,34 +132,49 @@ public class RCTMGLMapViewManager extends AbstractEventEmitter { //region React Methods public static final int METHOD_FLY_TO = 1; - + public static final int METHOD_SET_CAMERA = 2; + public static final int METHOD_FIT_BOUNDS = 3; @Nullable @Override public Map getCommandsMap() { return MapBuilder.builder() .put("flyTo", METHOD_FLY_TO) + .put("setCamera", METHOD_SET_CAMERA) + .put("fitBounds", METHOD_FIT_BOUNDS) .build(); } @Override public void receiveCommand(RCTMGLMapView mapView, int commandID, @Nullable ReadableArray args) { - Assertions.assertNotNull(args); - switch (commandID) { case METHOD_FLY_TO: - flyTo(mapView, args.getMap(0), args.getInt(1)); + flyTo(mapView, args.getString(0), args.getInt(1)); + break; + case METHOD_SET_CAMERA: + mapView.setCamera(args.getMap(0)); + break; + case METHOD_FIT_BOUNDS: + fitBounds(mapView, args.getString(0), args.getInt(1), args.getInt(2)); break; } } - private void flyTo(RCTMGLMapView mapView, ReadableMap flyToMap, int durationMS) { - Point flyToPoint = MGLGeoUtils.readableMapToPoint(flyToMap); + private void flyTo(RCTMGLMapView mapView, String featureJSONStr, int durationMS) { + Point flyToPoint = ConvertUtils.toPointGemetry(featureJSONStr); if (flyToPoint != null) { mapView.flyTo(flyToPoint, durationMS); } } + private void fitBounds(RCTMGLMapView mapView, String boundsJSONStr, int padding, int durationMS) { + FeatureCollection featureCollection = FeatureCollection.fromJson(boundsJSONStr); + + if (featureCollection != null) { + mapView.fitBounds(featureCollection, padding, durationMS); + } + } + //endregion } diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/events/AbstractRCTMGLEvent.java b/android/rctmgl/src/main/java/mapbox/rctmgl/events/AbstractEvent.java similarity index 64% rename from android/rctmgl/src/main/java/mapbox/rctmgl/events/AbstractRCTMGLEvent.java rename to android/rctmgl/src/main/java/mapbox/rctmgl/events/AbstractEvent.java index f354707..64298a1 100644 --- a/android/rctmgl/src/main/java/mapbox/rctmgl/events/AbstractRCTMGLEvent.java +++ b/android/rctmgl/src/main/java/mapbox/rctmgl/events/AbstractEvent.java @@ -9,13 +9,15 @@ import com.facebook.react.bridge.WritableMap; * Created by nickitaliano on 8/27/17. */ -abstract public class AbstractRCTMGLEvent implements IRCTMGLEvent { +abstract public class AbstractEvent implements IEvent { private int mTagID; private String mEventType; + private long mTimestamp; - public AbstractRCTMGLEvent(View view, String eventType) { + public AbstractEvent(View view, String eventType) { mEventType = eventType; mTagID = view.getId(); + mTimestamp = System.currentTimeMillis(); } public int getID() { @@ -26,10 +28,18 @@ abstract public class AbstractRCTMGLEvent implements IRCTMGLEvent { return mEventType; } + public boolean equals(IEvent event) { + return getKey().equals(event.getKey()) && mEventType.equals(event.getType()); + } + public WritableMap getPayload() { return Arguments.createMap(); } + public long getTimestamp() { + return mTimestamp; + } + public WritableMap toJSON() { WritableMap map = Arguments.createMap(); map.putString("type", getType()); diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/events/IRCTMGLEvent.java b/android/rctmgl/src/main/java/mapbox/rctmgl/events/IEvent.java similarity index 74% rename from android/rctmgl/src/main/java/mapbox/rctmgl/events/IRCTMGLEvent.java rename to android/rctmgl/src/main/java/mapbox/rctmgl/events/IEvent.java index 6866a3a..7ba7e29 100644 --- a/android/rctmgl/src/main/java/mapbox/rctmgl/events/IRCTMGLEvent.java +++ b/android/rctmgl/src/main/java/mapbox/rctmgl/events/IEvent.java @@ -6,10 +6,12 @@ import com.facebook.react.bridge.WritableMap; * Created by nickitaliano on 8/23/17. */ -public interface IRCTMGLEvent { +public interface IEvent { int getID(); String getKey(); String getType(); + long getTimestamp(); + boolean equals(IEvent event); WritableMap getPayload(); WritableMap toJSON(); } diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/events/MapChangeEvent.java b/android/rctmgl/src/main/java/mapbox/rctmgl/events/MapChangeEvent.java new file mode 100644 index 0000000..2c224d0 --- /dev/null +++ b/android/rctmgl/src/main/java/mapbox/rctmgl/events/MapChangeEvent.java @@ -0,0 +1,35 @@ +package mapbox.rctmgl.events; + +import android.view.View; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; + +import mapbox.rctmgl.events.constants.EventKeys; + +/** + * Created by nickitaliano on 8/27/17. + */ + +public class MapChangeEvent extends AbstractEvent { + private WritableMap mPayload; + + public MapChangeEvent(View view, String eventType) { + this(view, Arguments.createMap(), eventType); + } + + public MapChangeEvent(View view, WritableMap payload, String eventType) { + super(view, eventType); + mPayload = payload; + } + + @Override + public String getKey() { + return EventKeys.MAP_ONCHANGE; + } + + @Override + public WritableMap getPayload() { + return mPayload; + } +} diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/events/MapClickEvent.java b/android/rctmgl/src/main/java/mapbox/rctmgl/events/MapClickEvent.java new file mode 100644 index 0000000..e92ab47 --- /dev/null +++ b/android/rctmgl/src/main/java/mapbox/rctmgl/events/MapClickEvent.java @@ -0,0 +1,51 @@ +package mapbox.rctmgl.events; + +import android.graphics.PointF; +import android.support.annotation.NonNull; +import android.view.View; + +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.mapbox.mapboxsdk.geometry.LatLng; + +import mapbox.rctmgl.events.constants.EventKeys; +import mapbox.rctmgl.events.constants.EventTypes; +import mapbox.rctmgl.utils.ConvertUtils; + +/** + * Created by nickitaliano on 8/23/17. + */ + +public class MapClickEvent extends AbstractEvent { + private LatLng mTouchedLatLng; + private PointF mScreenPoint; + + public MapClickEvent(View view, @NonNull LatLng latLng, @NonNull PointF screenPoint) { + this(view, latLng, screenPoint, EventTypes.MAP_CLICK); + } + + public MapClickEvent(View view, @NonNull LatLng latLng, @NonNull PointF screenPoint, String eventType) { + super(view, eventType); + mTouchedLatLng = latLng; + mScreenPoint = screenPoint; + } + + @Override + public String getKey() { + String eventType = getType(); + + if (eventType.equals(EventTypes.MAP_LONG_CLICK)) { + return EventKeys.MAP_LONG_CLICK; + } + + return EventKeys.MAP_CLICK; + } + + @Override + public WritableMap getPayload() { + WritableMap properties = new WritableNativeMap(); + properties.putDouble("screenPointX", mScreenPoint.x); + properties.putDouble("screenPointY", mScreenPoint.y); + return ConvertUtils.toPointFeature(mTouchedLatLng, properties); + } +} diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/events/RCTMGLMapChangeEvent.java b/android/rctmgl/src/main/java/mapbox/rctmgl/events/RCTMGLMapChangeEvent.java deleted file mode 100644 index d0bdbe8..0000000 --- a/android/rctmgl/src/main/java/mapbox/rctmgl/events/RCTMGLMapChangeEvent.java +++ /dev/null @@ -1,18 +0,0 @@ -package mapbox.rctmgl.events; - -import android.view.View; - -/** - * Created by nickitaliano on 8/27/17. - */ - -public class RCTMGLMapChangeEvent extends AbstractRCTMGLEvent { - public RCTMGLMapChangeEvent(View view, String eventType) { - super(view, eventType); - } - - @Override - public String getKey() { - return RCTMGLEventKeys.MAP_ONCHANGE; - } -} diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/events/RCTMGLMapClickEvent.java b/android/rctmgl/src/main/java/mapbox/rctmgl/events/RCTMGLMapClickEvent.java deleted file mode 100644 index f26868a..0000000 --- a/android/rctmgl/src/main/java/mapbox/rctmgl/events/RCTMGLMapClickEvent.java +++ /dev/null @@ -1,47 +0,0 @@ -package mapbox.rctmgl.events; - -import android.view.View; - -import com.facebook.react.bridge.WritableMap; -import com.mapbox.mapboxsdk.geometry.LatLng; - -import mapbox.rctmgl.utils.MGLGeoUtils; - -/** - * Created by nickitaliano on 8/23/17. - */ - -public class RCTMGLMapClickEvent extends AbstractRCTMGLEvent { - private LatLng mTouchedLatLng; - - public RCTMGLMapClickEvent(View view) { - this(view, RCTMGLEventTypes.MAP_CLICK); - } - - public RCTMGLMapClickEvent(View view, String eventType) { - super(view, eventType); - } - - @Override - public String getKey() { - String eventType = getType(); - - if (eventType.equals(RCTMGLEventTypes.MAP_LONG_CLICK)) { - return RCTMGLEventKeys.MAP_LONG_CLICK; - } - - return RCTMGLEventKeys.MAP_CLICK; - } - - @Override - public WritableMap getPayload() { - return MGLGeoUtils.latLngToWritableMap(mTouchedLatLng); - } - - public void setLatLng(LatLng touchedLatLng) { - if (touchedLatLng == null) { - return; - } - mTouchedLatLng = touchedLatLng; - } -} diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/events/UserLocationChangeEvent.java b/android/rctmgl/src/main/java/mapbox/rctmgl/events/UserLocationChangeEvent.java new file mode 100644 index 0000000..efab3f1 --- /dev/null +++ b/android/rctmgl/src/main/java/mapbox/rctmgl/events/UserLocationChangeEvent.java @@ -0,0 +1,44 @@ +package mapbox.rctmgl.events; + +import android.location.Location; +import android.view.View; + +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.mapbox.mapboxsdk.geometry.LatLng; + +import mapbox.rctmgl.events.constants.EventKeys; +import mapbox.rctmgl.events.constants.EventTypes; +import mapbox.rctmgl.utils.ConvertUtils; + +/** + * Created by nickitaliano on 8/31/17. + */ + +public class UserLocationChangeEvent extends AbstractEvent { + private Location mLocation; + + public UserLocationChangeEvent(View view, Location location) { + super(view, EventTypes.USER_LOCATION_CHANGE); + mLocation = location; + } + + @Override + public String getKey() { + return EventKeys.USER_LOCATION_CHANGE; + } + + @Override + public WritableMap getPayload() { + WritableMap properties = new WritableNativeMap(); + + LatLng latLng = new LatLng(mLocation.getLatitude(), mLocation.getLongitude()); + properties.putDouble("heading", mLocation.getBearing()); + properties.putDouble("speed", mLocation.getSpeed()); + properties.putDouble("accuracy", mLocation.getAccuracy()); + properties.putDouble("altitude", mLocation.getAltitude()); + properties.putString("timestamp", Long.toString(mLocation.getTime())); + + return ConvertUtils.toPointFeature(latLng, properties); + } +} diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/events/RCTMGLEventKeys.java b/android/rctmgl/src/main/java/mapbox/rctmgl/events/constants/EventKeys.java similarity index 73% rename from android/rctmgl/src/main/java/mapbox/rctmgl/events/RCTMGLEventKeys.java rename to android/rctmgl/src/main/java/mapbox/rctmgl/events/constants/EventKeys.java index 565f05f..0c8ecf3 100644 --- a/android/rctmgl/src/main/java/mapbox/rctmgl/events/RCTMGLEventKeys.java +++ b/android/rctmgl/src/main/java/mapbox/rctmgl/events/constants/EventKeys.java @@ -1,16 +1,18 @@ -package mapbox.rctmgl.events; +package mapbox.rctmgl.events.constants; /** * Created by nickitaliano on 8/27/17. */ -public class RCTMGLEventKeys { +public class EventKeys { public static final String NAMESPACE = "rct.mapbox"; public static final String MAP_CLICK = ns("map.press"); public static final String MAP_LONG_CLICK = ns("map.longpress"); public static final String MAP_ONCHANGE = ns("map.change"); + public static final String USER_LOCATION_CHANGE = ns("userlocation.change"); + private static String ns(String name) { return String.format("%s.%s", NAMESPACE, name); } diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/events/RCTMGLEventTypes.java b/android/rctmgl/src/main/java/mapbox/rctmgl/events/constants/EventTypes.java similarity index 85% rename from android/rctmgl/src/main/java/mapbox/rctmgl/events/RCTMGLEventTypes.java rename to android/rctmgl/src/main/java/mapbox/rctmgl/events/constants/EventTypes.java index e01a3e8..6061c19 100644 --- a/android/rctmgl/src/main/java/mapbox/rctmgl/events/RCTMGLEventTypes.java +++ b/android/rctmgl/src/main/java/mapbox/rctmgl/events/constants/EventTypes.java @@ -1,10 +1,10 @@ -package mapbox.rctmgl.events; +package mapbox.rctmgl.events.constants; /** * Created by nickitaliano on 8/24/17. */ -public class RCTMGLEventTypes { +public class EventTypes { public static final String MAP_CLICK = "press"; public static final String MAP_LONG_CLICK = "longpress"; @@ -27,4 +27,6 @@ public class RCTMGLEventTypes { public static final String DID_FINISH_LOADING_STYLE = "didfinishloadingstyle"; public static final String FLY_TO_COMPLETE = "flytocomplete"; + public static final String SET_CAMERA_COMPLETE = "setcameracomplete"; + public static final String USER_LOCATION_CHANGE = "userlocationchange"; } diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/modules/RCTMGLModule.java b/android/rctmgl/src/main/java/mapbox/rctmgl/modules/RCTMGLModule.java index 2b59f7b..9146436 100644 --- a/android/rctmgl/src/main/java/mapbox/rctmgl/modules/RCTMGLModule.java +++ b/android/rctmgl/src/main/java/mapbox/rctmgl/modules/RCTMGLModule.java @@ -1,13 +1,7 @@ package mapbox.rctmgl.modules; -import android.Manifest; -import android.app.Activity; -import android.content.pm.PackageManager; -import android.os.Build; import android.os.Handler; import android.os.Looper; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; @@ -15,10 +9,20 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.MapBuilder; import com.mapbox.mapboxsdk.Mapbox; +import com.mapbox.mapboxsdk.constants.Style; +import com.mapbox.mapboxsdk.plugins.locationlayer.LocationLayerMode; +import com.mapbox.services.android.telemetry.permissions.PermissionsListener; +import com.mapbox.services.android.telemetry.permissions.PermissionsManager; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import mapbox.rctmgl.events.constants.EventTypes; /** * Created by nickitaliano on 8/18/17. @@ -28,9 +32,11 @@ public class RCTMGLModule extends ReactContextBaseJavaModule { public static final String REACT_CLASS = RCTMGLModule.class.getSimpleName(); private Handler mUiThreadHandler; + private ReactApplicationContext mReactContext; public RCTMGLModule(ReactApplicationContext reactApplicationContext) { super(reactApplicationContext); + mReactContext = reactApplicationContext; mUiThreadHandler = new Handler(Looper.getMainLooper()); } @@ -39,6 +45,51 @@ public class RCTMGLModule extends ReactContextBaseJavaModule { return REACT_CLASS; } + @Override + @Nullable + public Map getConstants() { + // map style urls + Map styleURLS = new HashMap<>(); + styleURLS.put("Street", Style.MAPBOX_STREETS); + styleURLS.put("Dark", Style.DARK); + styleURLS.put("Light", Style.LIGHT); + styleURLS.put("Outdoors", Style.OUTDOORS); + styleURLS.put("Satellite", Style.SATELLITE); + + // events + Map eventTypes = new HashMap<>(); + eventTypes.put("MapClick", EventTypes.MAP_CLICK); + eventTypes.put("MapLongClick", EventTypes.MAP_LONG_CLICK); + eventTypes.put("RegionWilChange", EventTypes.REGION_WILL_CHANGE); + eventTypes.put("RegionIsChanging", EventTypes.REGION_IS_CHANGING); + eventTypes.put("RegionDidChange", EventTypes.REGION_DID_CHANGE); + eventTypes.put("WillStartLoadingMap", EventTypes.WILL_START_LOADING_MAP); + eventTypes.put("DidFinishLoadingMap", EventTypes.DID_FINISH_LOADING_MAP); + eventTypes.put("DidFailLoadingMap", EventTypes.DID_FAIL_LOADING_MAP); + eventTypes.put("WillStartRenderingFrame", EventTypes.WILL_START_RENDERING_FRAME); + eventTypes.put("DidFinishRenderingFrame", EventTypes.DID_FINISH_RENDERING_FRAME); + eventTypes.put("DidFinishRenderingFrameFully", EventTypes.DID_FINISH_RENDERING_FRAME_FULLY); + eventTypes.put("WillStartRenderingMap", EventTypes.WILL_START_RENDERING_MAP); + eventTypes.put("DidFinishRenderingMap", EventTypes.DID_FINISH_RENDERING_MAP); + eventTypes.put("DidFinishRenderingMapFully", EventTypes.DID_FINISH_RENDERING_MAP_FULLY); + eventTypes.put("DidFinishLoadingStyle", EventTypes.DID_FINISH_LOADING_STYLE); + eventTypes.put("FlyToComplete", EventTypes.FLY_TO_COMPLETE); + eventTypes.put("SetCameraComplete", EventTypes.SET_CAMERA_COMPLETE); + + // user tracking modes + Map userTrackingModes = new HashMap<>(); + userTrackingModes.put("None", LocationLayerMode.NONE); + userTrackingModes.put("Tracking", LocationLayerMode.TRACKING); + userTrackingModes.put("Navigation", LocationLayerMode.NAVIGATION); + userTrackingModes.put("Compass", LocationLayerMode.COMPASS); + + return MapBuilder.builder() + .put("StyleURL", styleURLS) + .put("EventTypes", eventTypes) + .put("UserTrackingModes", userTrackingModes) + .build(); + } + @ReactMethod public void setAccessToken(final String accessToken) { mUiThreadHandler.post(new Runnable() { @@ -57,36 +108,30 @@ public class RCTMGLModule extends ReactContextBaseJavaModule { } @ReactMethod - public void requestPermissions(Promise promise) { - if (Build.VERSION.SDK_INT < 23) { - return; - } + public void requestPermissions(final Promise promise) { + if (!PermissionsManager.areLocationPermissionsGranted(mReactContext)) { + // ask user to grant location permissions + PermissionsManager permissionsManager = new PermissionsManager(new PermissionsListener() { + @Override + public void onExplanationNeeded(List permissionsToExplain) { + // do nothing + } - requestPermissionsIfNeeded(new String[]{ - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.ACCESS_FINE_LOCATION - }); + @Override + public void onPermissionResult(boolean granted) { + handlePermissionResponse(promise, granted); + } + }); + + permissionsManager.requestLocationPermissions(getCurrentActivity()); + } else { + handlePermissionResponse(promise, true); + } } - private void requestPermissionsIfNeeded(final String[] permissions) { - Activity curActivity = getCurrentActivity(); - if (curActivity == null) { - return; - } - - List permissionsToCheck = new ArrayList<>(); - for (String permission: permissions) { - int permissionCheck = ContextCompat.checkSelfPermission(curActivity, permission); - - if (permissionCheck != PackageManager.PERMISSION_GRANTED) { - permissionsToCheck.add(permission); - } - } - - int checkCount = permissionsToCheck.size(); - if (checkCount > 0) { - ActivityCompat.requestPermissions( - curActivity, permissionsToCheck.toArray(new String[checkCount]), 0); - } + private void handlePermissionResponse(Promise promise, boolean isGranted) { + WritableMap map = Arguments.createMap(); + map.putBoolean("isGranted", isGranted); + promise.resolve(map); } } diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/utils/ConvertUtils.java b/android/rctmgl/src/main/java/mapbox/rctmgl/utils/ConvertUtils.java new file mode 100644 index 0000000..9e2ea55 --- /dev/null +++ b/android/rctmgl/src/main/java/mapbox/rctmgl/utils/ConvertUtils.java @@ -0,0 +1,78 @@ +package mapbox.rctmgl.utils; + +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import com.mapbox.services.commons.geojson.Feature; +import com.mapbox.services.commons.geojson.FeatureCollection; +import com.mapbox.services.commons.geojson.Point; +import com.mapbox.services.commons.models.Position; + +import java.util.List; + +/** + * Created by nickitaliano on 8/18/17. + */ + +public class ConvertUtils { + public static WritableMap toPointFeature(LatLng latLng, WritableMap properties) { + WritableMap map = new WritableNativeMap(); + map.putString("type", "Feature"); + map.putMap("geometry", toPointGeometry(latLng)); + map.putMap("properties", properties); + return map; + } + + public static WritableMap toPointGeometry(LatLng latLng) { + WritableMap geometry = new WritableNativeMap(); + geometry.putString("type", "Point"); + geometry.putArray("coordinates", fromLatLng(latLng)); + return geometry; + } + + public static WritableArray fromLatLng(LatLng latLng) { + double[] coords = new double[]{ latLng.getLongitude(), latLng.getLatitude() }; + WritableArray writableCoords = new WritableNativeArray(); + writableCoords.pushDouble(coords[0]); + writableCoords.pushDouble(coords[1]); + return writableCoords; + } + + public static LatLng toLatLng(Point point) { + if (point == null) { + return null; + } + + Position position = point.getCoordinates(); + if (position == null) { + return null; + } + + return new LatLng(position.getLatitude(), position.getLongitude()); + } + + public static Point toPointGemetry(String featureJSONString) { + Feature feature = Feature.fromJson(featureJSONString); + if (feature == null) { + return null; + } + return (Point)feature.getGeometry(); + } + + public static LatLngBounds toLatLngBounds(FeatureCollection featureCollection) { + List features = featureCollection.getFeatures(); + + if (features.size() != 2) { + return null; + } + + LatLng neLatLng = ConvertUtils.toLatLng((Point)features.get(0).getGeometry()); + LatLng swLatLng = ConvertUtils.toLatLng((Point)features.get(1).getGeometry()); + + return LatLngBounds.from(neLatLng.getLatitude(), neLatLng.getLongitude(), + swLatLng.getLatitude(), swLatLng.getLongitude()); + } +} diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/utils/MGLGeoUtils.java b/android/rctmgl/src/main/java/mapbox/rctmgl/utils/MGLGeoUtils.java deleted file mode 100644 index f382b74..0000000 --- a/android/rctmgl/src/main/java/mapbox/rctmgl/utils/MGLGeoUtils.java +++ /dev/null @@ -1,66 +0,0 @@ -package mapbox.rctmgl.utils; - -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableArray; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.bridge.WritableNativeArray; -import com.facebook.react.bridge.WritableNativeMap; -import com.mapbox.mapboxsdk.geometry.LatLng; -import com.mapbox.services.android.telemetry.constants.GeoConstants; -import com.mapbox.services.commons.geojson.GeoJSON; -import com.mapbox.services.commons.geojson.Geometry; -import com.mapbox.services.commons.geojson.Point; -import com.mapbox.services.commons.models.Position; -import com.mapbox.services.commons.geojson.custom.GeometryDeserializer; - -import java.util.List; - -/** - * Created by nickitaliano on 8/18/17. - */ - -public class MGLGeoUtils { - public static WritableMap latLngToWritableMap(LatLng point) { - WritableMap map = Arguments.createMap(); - map.putString("type", "Point"); - - WritableArray coordinates = Arguments.createArray(); - coordinates.pushDouble(point.getLongitude()); - coordinates.pushDouble(point.getLatitude()); - map.putArray("coordinates", coordinates); - - return map; - } - - public static LatLng pointToLatLng(Point point) { - if (point == null) { - return null; - } - - Position position = point.getCoordinates(); - if (position == null) { - return null; - } - - return new LatLng(position.getLatitude(), position.getLongitude()); - } - - public static Point readableMapToPoint(ReadableMap map) { - final String type = map.getString("type"); - - if (!type.equals("Point")) { - return null; - } - - return Point.fromCoordinates(readableArrayToDoubleArray(map.getArray("coordinates"))); - } - - private static double[] readableArrayToDoubleArray(ReadableArray coordArray) { - if (coordArray == null && coordArray.size() >= 2) { - return null; - } - return new double[]{ coordArray.getDouble(0), coordArray.getDouble(1) }; - } -} diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/utils/RCTMGLMapStyleURL.java b/android/rctmgl/src/main/java/mapbox/rctmgl/utils/RCTMGLMapStyleURL.java deleted file mode 100644 index db5048b..0000000 --- a/android/rctmgl/src/main/java/mapbox/rctmgl/utils/RCTMGLMapStyleURL.java +++ /dev/null @@ -1,41 +0,0 @@ -package mapbox.rctmgl.utils; - -import com.mapbox.mapboxsdk.constants.Style; - -/** - * Created by nickitaliano on 8/21/17. - */ - - -public enum RCTMGLMapStyleURL { - Streets("mapbox-streets", Style.MAPBOX_STREETS), - Dark("mapbox-dark", Style.DARK), - Light("mapbox-light", Style.LIGHT), - Outdoors("mapbox-outdoors", Style.OUTDOORS), - Satellite("mapbox-satellite", Style.SATELLITE); - - private String mKey; - private String mURL; - - RCTMGLMapStyleURL(String key, String url) { - mKey = key; - mURL = url; - } - - private String getKey() { - return mKey; - } - - public String getURL() { - return mURL; - } - - public static RCTMGLMapStyleURL fromKey(String key) { - for (RCTMGLMapStyleURL mapStyle: RCTMGLMapStyleURL.values()) { - if (mapStyle.getKey().equals(key)) { - return mapStyle; - } - } - return null; - } -} diff --git a/android/rctmgl/src/main/java/mapbox/rctmgl/utils/SimpleEventCallback.java b/android/rctmgl/src/main/java/mapbox/rctmgl/utils/SimpleEventCallback.java new file mode 100644 index 0000000..e3f06a1 --- /dev/null +++ b/android/rctmgl/src/main/java/mapbox/rctmgl/utils/SimpleEventCallback.java @@ -0,0 +1,30 @@ +package mapbox.rctmgl.utils; + +import com.mapbox.mapboxsdk.maps.MapboxMap; + +import mapbox.rctmgl.components.AbstractEventEmitter; +import mapbox.rctmgl.events.IEvent; + +/** + * Created by nickitaliano on 8/31/17. + */ + +public class SimpleEventCallback implements MapboxMap.CancelableCallback { + private AbstractEventEmitter mEventEmitter; + private IEvent mEvent; + + public SimpleEventCallback(AbstractEventEmitter eventEmitter, IEvent event) { + mEventEmitter = eventEmitter; + mEvent = event; + } + + @Override + public void onCancel() { + mEventEmitter.handleEvent(mEvent); + } + + @Override + public void onFinish() { + mEventEmitter.handleEvent(mEvent); + } +} diff --git a/ios/RCTMGL.xcodeproj/project.pbxproj b/ios/RCTMGL.xcodeproj/project.pbxproj index 2270d7e..db41c13 100644 --- a/ios/RCTMGL.xcodeproj/project.pbxproj +++ b/ios/RCTMGL.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + C435A5F71F587D9100824A30 /* ViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C435A5F61F587D9100824A30 /* ViewManager.m */; }; C43CA5061F507BCC000B9CB7 /* RCTMGLMapTouchEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = C43CA5051F507BCC000B9CB7 /* RCTMGLMapTouchEvent.m */; }; C43CA50A1F50C145000B9CB7 /* RCTMGLEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = C43CA5091F50C145000B9CB7 /* RCTMGLEvent.m */; }; C43CA50D1F53A4EF000B9CB7 /* RCTMGLEventTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = C43CA50C1F53A4EF000B9CB7 /* RCTMGLEventTypes.m */; }; @@ -32,6 +33,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + C435A5F51F587D9100824A30 /* ViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewManager.h; sourceTree = ""; }; + C435A5F61F587D9100824A30 /* ViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ViewManager.m; sourceTree = ""; }; C43CA5041F507BCC000B9CB7 /* RCTMGLMapTouchEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMGLMapTouchEvent.h; sourceTree = ""; }; C43CA5051F507BCC000B9CB7 /* RCTMGLMapTouchEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMGLMapTouchEvent.m; sourceTree = ""; }; C43CA5071F507D88000B9CB7 /* RCTMGLEventProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTMGLEventProtocol.h; sourceTree = ""; }; @@ -63,6 +66,36 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + C43773B91F56024900AF6D71 /* Components */ = { + isa = PBXGroup; + children = ( + C43773BA1F56025800AF6D71 /* MapView */, + C435A5F51F587D9100824A30 /* ViewManager.h */, + C435A5F61F587D9100824A30 /* ViewManager.m */, + ); + name = Components; + sourceTree = ""; + }; + C43773BA1F56025800AF6D71 /* MapView */ = { + isa = PBXGroup; + children = ( + C4D1444A1F4E178100396F26 /* RCTMGLMapView.h */, + C4D1444B1F4E178100396F26 /* RCTMGLMapView.m */, + C4D1444D1F4E181100396F26 /* RCTMGLMapViewManager.h */, + C4D1444E1F4E181100396F26 /* RCTMGLMapViewManager.m */, + ); + name = MapView; + sourceTree = ""; + }; + C43773BD1F5603AA00AF6D71 /* Constants */ = { + isa = PBXGroup; + children = ( + C43CA50B1F53A4EF000B9CB7 /* RCTMGLEventTypes.h */, + C43CA50C1F53A4EF000B9CB7 /* RCTMGLEventTypes.m */, + ); + name = Constants; + sourceTree = ""; + }; C43CA5031F507B8D000B9CB7 /* Events */ = { isa = PBXGroup; children = ( @@ -71,8 +104,7 @@ C43CA5071F507D88000B9CB7 /* RCTMGLEventProtocol.h */, C43CA5081F50C145000B9CB7 /* RCTMGLEvent.h */, C43CA5091F50C145000B9CB7 /* RCTMGLEvent.m */, - C43CA50B1F53A4EF000B9CB7 /* RCTMGLEventTypes.h */, - C43CA50C1F53A4EF000B9CB7 /* RCTMGLEventTypes.m */, + C43773BD1F5603AA00AF6D71 /* Constants */, ); name = Events; sourceTree = ""; @@ -96,34 +128,23 @@ C4D1443F1F4E16F600396F26 /* RCTMGL */ = { isa = PBXGroup; children = ( + C43773B91F56024900AF6D71 /* Components */, C43CA5031F507B8D000B9CB7 /* Events */, C4D144941F4E2EC800396F26 /* Utils */, - C4D144501F4E188F00396F26 /* MGLModule */, - C4D144491F4E176300396F26 /* MapView */, + C4D144501F4E188F00396F26 /* Modules */, C4D144401F4E16F600396F26 /* RCTMGL.h */, C4D144411F4E16F600396F26 /* RCTMGL.m */, ); path = RCTMGL; sourceTree = ""; }; - C4D144491F4E176300396F26 /* MapView */ = { - isa = PBXGroup; - children = ( - C4D1444A1F4E178100396F26 /* RCTMGLMapView.h */, - C4D1444B1F4E178100396F26 /* RCTMGLMapView.m */, - C4D1444D1F4E181100396F26 /* RCTMGLMapViewManager.h */, - C4D1444E1F4E181100396F26 /* RCTMGLMapViewManager.m */, - ); - name = MapView; - sourceTree = ""; - }; - C4D144501F4E188F00396F26 /* MGLModule */ = { + C4D144501F4E188F00396F26 /* Modules */ = { isa = PBXGroup; children = ( C4D144511F4E190C00396F26 /* MGLModule.h */, C4D144521F4E190C00396F26 /* MGLModule.m */, ); - name = MGLModule; + name = Modules; sourceTree = ""; }; C4D144941F4E2EC800396F26 /* Utils */ = { @@ -200,6 +221,7 @@ C43CA50D1F53A4EF000B9CB7 /* RCTMGLEventTypes.m in Sources */, C4D144421F4E16F600396F26 /* RCTMGL.m in Sources */, C43CA5061F507BCC000B9CB7 /* RCTMGLMapTouchEvent.m in Sources */, + C435A5F71F587D9100824A30 /* ViewManager.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/RCTMGL/MGLModule.m b/ios/RCTMGL/MGLModule.m index 9324e4c..fb598e4 100644 --- a/ios/RCTMGL/MGLModule.m +++ b/ios/RCTMGL/MGLModule.m @@ -7,12 +7,57 @@ // #import "MGLModule.h" +#import "RCTMGLEventTypes.h" @import Mapbox; @implementation MGLModule RCT_EXPORT_MODULE(); +- (NSDictionary *)constantsToExport +{ + // style urls + NSMutableDictionary *styleURLS = [[NSMutableDictionary alloc] init]; + [styleURLS setObject:[MGLStyle.streetsStyleURL absoluteString] forKey:@"Street"]; + [styleURLS setObject:[MGLStyle.darkStyleURL absoluteString] forKey:@"Dark"]; + [styleURLS setObject:[MGLStyle.lightStyleURL absoluteString] forKey:@"Light"]; + [styleURLS setObject:[MGLStyle.outdoorsStyleURL absoluteString] forKey:@"Outdoors"]; + [styleURLS setObject:[MGLStyle.satelliteStyleURL absoluteString] forKey:@"Satellite"]; + + // event types + NSMutableDictionary *eventTypes = [[NSMutableDictionary alloc] init]; + [eventTypes setObject:RCT_MAPBOX_EVENT_TAP forKey:@"MapClick"]; + [eventTypes setObject:RCT_MAPBOX_EVENT_LONGPRESS forKey:@"MapLongClick"]; + [eventTypes setObject:RCT_MAPBOX_REGION_WILL_CHANGE_EVENT forKey:@"RegionWillChange"]; + [eventTypes setObject:RCT_MAPBOX_REGION_IS_CHANGING forKey:@"RegionIsChanging"]; + [eventTypes setObject:RCT_MAPBOX_REGION_DID_CHANGE forKey:@"RegionDidChange"]; + [eventTypes setObject:RCT_MAPBOX_WILL_START_LOADING_MAP forKey:@"WillStartLoadingMap"]; + [eventTypes setObject:RCT_MAPBOX_DID_FINISH_LOADING_MAP forKey:@"DidFinishLoadingMap"]; + [eventTypes setObject:RCT_MAPBOX_DID_FAIL_LOADING_MAP forKey:@"DidFailLaodingMap"]; + [eventTypes setObject:RCT_MAPBOX_WILL_START_RENDERING_FRAME forKey:@"WillStartRenderingFrame"]; + [eventTypes setObject:RCT_MAPBOX_DID_FINSIH_RENDERING_FRAME forKey:@"DidFinishRenderingFrame"]; + [eventTypes setObject:RCT_MAPBOX_DID_FINISH_RENDERING_FRAME_FULLY forKey:@"DidFinishRenderingFrameFully"]; + [eventTypes setObject:RCT_MAPBOX_WILL_START_RENDERING_MAP forKey:@"WillStartRenderingMap"]; + [eventTypes setObject:RCT_MAPBOX_DID_FINISH_RENDERING_MAP forKey:@"DidFinishRenderingMap"]; + [eventTypes setObject:RCT_MAPBOX_DID_FINISH_RENDERING_MAP_FULLY forKey:@"DidFinishRenderingMapFully"]; + [eventTypes setObject:RCT_MAPBOX_DID_FINISH_LOADING_STYLE forKey:@"DidFinishLoadingStyle"]; + [eventTypes setObject:RCT_MAPBOX_FLY_TO_COMPLETE forKey:@"FlyToComplete"]; + [eventTypes setObject:RCT_MAPBOX_SET_CAMERA_COMPLETE forKey:@"SetCameraComplete"]; + + // user tracking modes + NSMutableDictionary *userTrackingModes = [[NSMutableDictionary alloc] init]; + [userTrackingModes setObject:[NSNumber numberWithInt:MGLUserTrackingModeNone] forKey:@"None"]; + [userTrackingModes setObject:[NSNumber numberWithInt:MGLUserTrackingModeFollow] forKey:@"Tracking"]; + [userTrackingModes setObject:[NSNumber numberWithInt:MGLUserTrackingModeFollowWithHeading] forKey:@"Compass"]; + [userTrackingModes setObject:[NSNumber numberWithInt:MGLUserTrackingModeFollowWithCourse] forKey:@"Navigation"]; + + return @{ + @"StyleURL": styleURLS, + @"EventTypes": eventTypes, + @"UserTrackingModes": userTrackingModes + }; +} + RCT_EXPORT_METHOD(setAccessToken:(NSString *)accessToken) { [MGLAccountManager setAccessToken:accessToken]; diff --git a/ios/RCTMGL/RCTMGLEvent.h b/ios/RCTMGL/RCTMGLEvent.h index fe9bcbc..3433558 100644 --- a/ios/RCTMGL/RCTMGLEvent.h +++ b/ios/RCTMGL/RCTMGLEvent.h @@ -13,6 +13,7 @@ @property (nonatomic, copy) NSString *type; @property (nonatomic, strong) NSDictionary *payload; +@property (nonatomic, readonly) NSTimeInterval timestamp; + (RCTMGLEvent*)makeEvent:(NSString*)eventType; + (RCTMGLEvent*)makeEvent:(NSString*)eventType withPayload:(NSDictionary*)payload; diff --git a/ios/RCTMGL/RCTMGLEvent.m b/ios/RCTMGL/RCTMGLEvent.m index cfa52ca..662c927 100644 --- a/ios/RCTMGL/RCTMGLEvent.m +++ b/ios/RCTMGL/RCTMGLEvent.m @@ -10,6 +10,14 @@ @implementation RCTMGLEvent +- (instancetype)init +{ + if (self = [super init]) { + _timestamp = [[NSDate date] timeIntervalSince1970]; + } + return self; +} + - (NSDictionary*)payload { if (_payload == nil) { diff --git a/ios/RCTMGL/RCTMGLEventTypes.h b/ios/RCTMGL/RCTMGLEventTypes.h index 9dac17c..b356107 100644 --- a/ios/RCTMGL/RCTMGLEventTypes.h +++ b/ios/RCTMGL/RCTMGLEventTypes.h @@ -32,5 +32,8 @@ extern NSString *const RCT_MAPBOX_DID_FINISH_RENDERING_MAP_FULLY; extern NSString *const RCT_MAPBOX_DID_FINISH_LOADING_STYLE; extern NSString *const RCT_MAPBOX_FLY_TO_COMPLETE; +extern NSString *const RCT_MAPBOX_SET_CAMERA_COMPLETE; + +extern NSString *const RCT_MAPBOX_USER_LOCATION_UPDATE; @end diff --git a/ios/RCTMGL/RCTMGLEventTypes.m b/ios/RCTMGL/RCTMGLEventTypes.m index 343329f..f483829 100644 --- a/ios/RCTMGL/RCTMGLEventTypes.m +++ b/ios/RCTMGL/RCTMGLEventTypes.m @@ -32,5 +32,8 @@ NSString *const RCT_MAPBOX_DID_FINISH_RENDERING_MAP_FULLY = @"didfinishrendering NSString *const RCT_MAPBOX_DID_FINISH_LOADING_STYLE = @"didfinishloadingstyle"; NSString *const RCT_MAPBOX_FLY_TO_COMPLETE = @"flytocomplete"; +NSString *const RCT_MAPBOX_SET_CAMERA_COMPLETE = @"@setcameracomplete"; + +NSString *const RCT_MAPBOX_USER_LOCATION_UPDATE = @"@userlocationchange"; @end diff --git a/ios/RCTMGL/RCTMGLMapTouchEvent.h b/ios/RCTMGL/RCTMGLMapTouchEvent.h index baf170d..1809916 100644 --- a/ios/RCTMGL/RCTMGLMapTouchEvent.h +++ b/ios/RCTMGL/RCTMGLMapTouchEvent.h @@ -12,7 +12,8 @@ @interface RCTMGLMapTouchEvent : RCTMGLEvent -@property (nonatomic, strong) NSArray *coordinates; +@property (nonatomic, assign) CLLocationCoordinate2D coordinate; +@property (nonatomic, assign) CGPoint screenPoint; + (RCTMGLMapTouchEvent*)makeTapEvent:(MGLMapView*)mapView withPoint:(CGPoint)point; + (RCTMGLMapTouchEvent*)makeLongPressEvent:(MGLMapView*)mapView withPoint:(CGPoint)point; diff --git a/ios/RCTMGL/RCTMGLMapTouchEvent.m b/ios/RCTMGL/RCTMGLMapTouchEvent.m index 3971a67..4b611e4 100644 --- a/ios/RCTMGL/RCTMGLMapTouchEvent.m +++ b/ios/RCTMGL/RCTMGLMapTouchEvent.m @@ -14,7 +14,13 @@ - (NSDictionary*)payload { - return @{ @"type": @"Point", @"coordinates": _coordinates }; + MGLPointFeature *feature = [[MGLPointFeature alloc] init]; + feature.coordinate = _coordinate; + feature.attributes = @{ + @"screenPointX": [NSNumber numberWithDouble:_screenPoint.x], + @"screenPointY":[NSNumber numberWithDouble:_screenPoint.y] + }; + return [feature geoJSONDictionary]; } + (RCTMGLMapTouchEvent*)makeTapEvent:(MGLMapView*)mapView withPoint:(CGPoint)point @@ -29,10 +35,10 @@ + (RCTMGLMapTouchEvent*)_fromPoint:(CGPoint)point withMapView:(MGLMapView *)mapView andEventType:(NSString*)eventType { - CLLocationCoordinate2D coord = [mapView convertPoint:point toCoordinateFromView:mapView]; RCTMGLMapTouchEvent *event = [[RCTMGLMapTouchEvent alloc] init]; event.type = eventType; - event.coordinates = @[@(coord.longitude), @(coord.latitude)]; + event.coordinate =[mapView convertPoint:point toCoordinateFromView:mapView]; + event.screenPoint = point; return event; } diff --git a/ios/RCTMGL/RCTMGLMapView.h b/ios/RCTMGL/RCTMGLMapView.h index 25dde46..f1689b7 100644 --- a/ios/RCTMGL/RCTMGLMapView.h +++ b/ios/RCTMGL/RCTMGLMapView.h @@ -14,10 +14,13 @@ @property (nonatomic, assign) BOOL animated; @property (nonatomic, assign) BOOL reactScrollEnabled; @property (nonatomic, assign) BOOL reactPitchEnabled; +@property (nonatomic, assign) BOOL reactShowUserLocation; -@property (nonatomic, strong) NSDictionary *reactCenterCoordinate; +@property (nonatomic, copy) NSString *reactCenterCoordinate; @property (nonatomic, copy) NSString *reactStyleURL; +@property (nonatomic, assign) int reactUserTrackingMode; + @property (nonatomic, assign) double heading; @property (nonatomic, assign) double pitch; @property (nonatomic, assign) double reactZoomLevel; @@ -27,5 +30,6 @@ @property (nonatomic, copy) RCTBubblingEventBlock onPress; @property (nonatomic, copy) RCTBubblingEventBlock onLongPress; @property (nonatomic, copy) RCTBubblingEventBlock onMapChange; +@property (nonatomic, copy) RCTBubblingEventBlock onUserLocationChange; @end diff --git a/ios/RCTMGL/RCTMGLMapView.m b/ios/RCTMGL/RCTMGLMapView.m index ab9678a..0467fd8 100644 --- a/ios/RCTMGL/RCTMGLMapView.m +++ b/ios/RCTMGL/RCTMGLMapView.m @@ -11,24 +11,6 @@ #import "UIView+React.h" @implementation RCTMGLMapView -{ - NSDictionary *_mapStyleURLS; -} - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - _mapStyleURLS = @{ - @"mapbox-streets": MGLStyle.streetsStyleURL, - @"mapbox-dark": MGLStyle.darkStyleURL, - @"mapbox-light": MGLStyle.lightStyleURL, - @"mapbox-outdoors": MGLStyle.outdoorsStyleURL, - @"mapbox-satellite": MGLStyle.satelliteStyleURL - }; - } - - return self; -} - (void)setReactScrollEnabled:(BOOL)reactScrollEnabled { @@ -42,7 +24,13 @@ self.pitchEnabled = _reactPitchEnabled; } -- (void)setReactCenterCoordinate:(NSDictionary *)reactCenterCoordinate +- (void)setReactShowUserLocation:(BOOL)reactShowUserLocation +{ + _reactShowUserLocation = reactShowUserLocation; + self.showsUserLocation = _reactShowUserLocation; +} + +- (void)setReactCenterCoordinate:(NSString *)reactCenterCoordinate { _reactCenterCoordinate = reactCenterCoordinate; [self _updateCameraIfNeeded:YES]; @@ -84,28 +72,26 @@ self.maximumZoomLevel = reactMaxZoomLevel; } -- (NSURL*)_getStyleURLFromKey:(NSString *)styleKey +- (void)setReactUserTrackingMode:(int)reactUserTrackingMode { - NSString *styleURL = [_mapStyleURLS objectForKey:styleKey]; - - // mapbox base style urls - if (styleURL != nil) { - return (NSURL*)styleURL; - } - - // custom style url - return [NSURL URLWithString:styleKey]; + _reactUserTrackingMode = reactUserTrackingMode; + [self setUserTrackingMode:(NSUInteger)_reactUserTrackingMode animated:_animated]; +} + +- (NSURL*)_getStyleURLFromKey:(NSString *)styleURL +{ + return [NSURL URLWithString:styleURL]; } - (void)_updateCameraIfNeeded:(BOOL)shouldUpdateCenterCoord { if (shouldUpdateCenterCoord) { - [self setCenterCoordinate:[RCTMGLUtils GeoJSONPoint:_reactCenterCoordinate] animated:NO]; + [self setCenterCoordinate:[RCTMGLUtils fromFeature:_reactCenterCoordinate] animated:_animated]; } else { MGLMapCamera *camera = [self.camera copy]; camera.pitch = _pitch; camera.heading = _heading; - [self setCamera:camera animated:NO]; + [self setCamera:camera animated:_animated]; } } diff --git a/ios/RCTMGL/RCTMGLMapViewManager.h b/ios/RCTMGL/RCTMGLMapViewManager.h index f090b24..f3cce52 100644 --- a/ios/RCTMGL/RCTMGLMapViewManager.h +++ b/ios/RCTMGL/RCTMGLMapViewManager.h @@ -6,9 +6,9 @@ // Copyright © 2017 Mapbox Inc. All rights reserved. // -#import +#import "ViewManager.h" -@interface RCTMGLMapViewManager : RCTViewManager +@interface RCTMGLMapViewManager : ViewManager - (void)didTapMap:(UITapGestureRecognizer *)recognizer; - (void)didLongPressMap:(UILongPressGestureRecognizer *)recognizer; diff --git a/ios/RCTMGL/RCTMGLMapViewManager.m b/ios/RCTMGL/RCTMGLMapViewManager.m index 7aece9c..ac01750 100644 --- a/ios/RCTMGL/RCTMGLMapViewManager.m +++ b/ios/RCTMGL/RCTMGLMapViewManager.m @@ -46,10 +46,13 @@ RCT_EXPORT_MODULE() RCT_EXPORT_VIEW_PROPERTY(animated, BOOL) RCT_REMAP_VIEW_PROPERTY(scrollEnabled, reactScrollEnabled, BOOL) RCT_REMAP_VIEW_PROPERTY(pitchEnabled, reactPitchEnabled, BOOL) +RCT_REMAP_VIEW_PROPERTY(showUserLocation, reactShowUserLocation, BOOL) -RCT_REMAP_VIEW_PROPERTY(centerCoordinate, reactCenterCoordinate, NSDictionary) +RCT_REMAP_VIEW_PROPERTY(centerCoordinate, reactCenterCoordinate, NSString) RCT_REMAP_VIEW_PROPERTY(styleURL, reactStyleURL, NSString) +RCT_REMAP_VIEW_PROPERTY(userTrackingMode, reactUserTrackingMode, int) + RCT_EXPORT_VIEW_PROPERTY(heading, double) RCT_EXPORT_VIEW_PROPERTY(pitch, double) RCT_REMAP_VIEW_PROPERTY(zoomLevel, reactZoomLevel, double) @@ -59,11 +62,12 @@ RCT_REMAP_VIEW_PROPERTY(maxZoomLevel, reactMaxZoomLevel, double) RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLongPress, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onMapChange, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onUserLocationChange, RCTBubblingEventBlock) #pragma mark - React Methods RCT_EXPORT_METHOD(flyTo:(nonnull NSNumber*)reactTag - withPoint:(nonnull NSDictionary*)point + withFeature:(nonnull NSString*)featureJSONStr withDuration:(nonnull NSNumber*)durationMS) { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *manager, NSDictionary *viewRegistry) { @@ -74,10 +78,10 @@ RCT_EXPORT_METHOD(flyTo:(nonnull NSNumber*)reactTag return; } - RCTMGLMapView *reactMapView = (RCTMGLMapView*)view; + __weak RCTMGLMapView *reactMapView = (RCTMGLMapView*)view; MGLMapCamera *camera = [reactMapView.camera copy]; - camera.centerCoordinate = [RCTMGLUtils GeoJSONPoint:point]; - CGFloat durationS = [durationMS doubleValue] * 0.001; + camera.centerCoordinate = [RCTMGLUtils fromFeature:featureJSONStr]; + CGFloat durationS = [RCTMGLUtils fromMS:durationMS]; [reactMapView flyToCamera:camera withDuration:durationS completionHandler:^{ [self reactMapDidChange:reactMapView eventType:RCT_MAPBOX_FLY_TO_COMPLETE]; @@ -85,6 +89,64 @@ RCT_EXPORT_METHOD(flyTo:(nonnull NSNumber*)reactTag }]; } +RCT_EXPORT_METHOD(setCamera:(nonnull NSNumber*)reactTag + withConfiguration:(nonnull NSDictionary*)config) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *manager, NSDictionary *viewRegistry) { + id view = viewRegistry[reactTag]; + + if (![view isKindOfClass:[RCTMGLMapView class]]) { + RCTLogError(@"Invalid react tag, could not find RCTMGLMapView"); + return; + } + + __weak RCTMGLMapView *reactMapView = (RCTMGLMapView*)view; + MGLMapCamera *camera = [reactMapView.camera copy]; + + if (config[@"pitch"]) { + camera.pitch = [config[@"pitch"] doubleValue]; + } + + if (config[@"heading"]) { + camera.heading = [config[@"heading"] doubleValue]; + } + + if (config[@"centerCoordinate"]) { + camera.centerCoordinate = [RCTMGLUtils fromFeature:config[@"centerCoordinate"]]; + } + + NSTimeInterval durationS = 2.0; + if (config[@"duration"]) { + durationS = [RCTMGLUtils fromMS:config[@"duration"]]; + } + + [reactMapView setCamera:camera withDuration:durationS animationTimingFunction:0 completionHandler:^{ + [self reactMapDidChange:reactMapView eventType:RCT_MAPBOX_SET_CAMERA_COMPLETE]; + }]; + }]; +} + +RCT_EXPORT_METHOD(fitBounds:(nonnull NSNumber*)reactTag + withFeatureCollection:(NSString*)featureCollectionJSONStr + withPadding:(nonnull NSNumber*)insetPadding + withDuration:(nonnull NSNumber*)durationMS) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *manager, NSDictionary *viewRegistry) { + id view = viewRegistry[reactTag]; + + if (![view isKindOfClass:[RCTMGLMapView class]]) { + RCTLogError(@"Invalid react tag, could not find RCTMGLMapView"); + return; + } + + CGFloat padding = [insetPadding floatValue]; + RCTMGLMapView *reactMapView = (RCTMGLMapView*)view; + [reactMapView setVisibleCoordinateBounds:[RCTMGLUtils fromFeatureCollection:featureCollectionJSONStr] + edgePadding:UIEdgeInsetsMake(padding, padding, padding, padding) + animated:durationMS != 0]; + }]; +} + #pragma mark - UIGestureRecognizers - (void)didTapMap:(UITapGestureRecognizer *)recognizer @@ -96,7 +158,7 @@ RCT_EXPORT_METHOD(flyTo:(nonnull NSNumber*)reactTag } RCTMGLMapTouchEvent *event = [RCTMGLMapTouchEvent makeTapEvent:mapView withPoint:[recognizer locationInView:mapView]]; - mapView.onPress([event toJSON]); + [self fireEvent:event withCallback:mapView.onPress]; } - (void)didLongPressMap:(UILongPressGestureRecognizer *)recognizer @@ -108,14 +170,15 @@ RCT_EXPORT_METHOD(flyTo:(nonnull NSNumber*)reactTag } RCTMGLMapTouchEvent *event = [RCTMGLMapTouchEvent makeLongPressEvent:mapView withPoint:[recognizer locationInView:mapView]]; - mapView.onLongPress([event toJSON]); + [self fireEvent:event withCallback:mapView.onLongPress]; } #pragma mark - MGLMapViewDelegate - (void)mapView:(MGLMapView *)mapView regionWillChangeAnimated:(BOOL)animated { - [self reactMapDidChange:mapView eventType:RCT_MAPBOX_REGION_WILL_CHANGE_EVENT]; + NSDictionary *payload = [self _makeRegionPayload:mapView animated:animated]; + [self reactMapDidChange:mapView eventType:RCT_MAPBOX_REGION_WILL_CHANGE_EVENT andPayload:payload]; } - (void)mapViewRegionIsChanging:(MGLMapView *)mapView @@ -125,7 +188,8 @@ RCT_EXPORT_METHOD(flyTo:(nonnull NSNumber*)reactTag - (void)mapView:(MGLMapView *)mapView regionDidChangeAnimated:(BOOL)animated { - [self reactMapDidChange:mapView eventType:RCT_MAPBOX_REGION_DID_CHANGE]; + NSDictionary *payload = [self _makeRegionPayload:mapView animated:animated]; + [self reactMapDidChange:mapView eventType:RCT_MAPBOX_REGION_DID_CHANGE andPayload:payload]; } - (void)mapViewWillStartLoadingMap:(MGLMapView *)mapView @@ -177,14 +241,47 @@ RCT_EXPORT_METHOD(flyTo:(nonnull NSNumber*)reactTag [self reactMapDidChange:mapView eventType:RCT_MAPBOX_DID_FINISH_LOADING_STYLE]; } +- (void)mapView:(MGLMapView *)mapView didUpdateUserLocation:(MGLUserLocation *)userLocation +{ + NSDictionary *properties = @{ + @"accuracy": [NSNumber numberWithDouble:userLocation.location.horizontalAccuracy], + @"heading": [NSNumber numberWithDouble:userLocation.heading.magneticHeading], + @"altitude": [NSNumber numberWithDouble:userLocation.location.altitude], + @"speed": [NSNumber numberWithDouble:userLocation.location.speed] + }; + + MGLPointFeature *feature = [[MGLPointFeature alloc] init]; + feature.coordinate = userLocation.coordinate; + feature.attributes = properties; + + RCTMGLMapView *reactMapView = (RCTMGLMapView*)mapView; + [self fireEvent:[RCTMGLEvent makeEvent:RCT_MAPBOX_USER_LOCATION_UPDATE withPayload:feature.geoJSONDictionary] + withCallback:reactMapView.onUserLocationChange]; +} + - (void)reactMapDidChange:(MGLMapView*)mapView eventType:(NSString*)type +{ + [self reactMapDidChange:mapView eventType:type andPayload:nil]; +} + +- (void)reactMapDidChange:(MGLMapView*)mapView eventType:(NSString*)type andPayload:(NSDictionary*)payload { RCTMGLMapView *reactMapView = (RCTMGLMapView*)mapView; - RCTMGLEvent *event = [RCTMGLEvent makeEvent:type]; - - if (reactMapView.onMapChange != nil) { - reactMapView.onMapChange([event toJSON]); - } + RCTMGLEvent *event = [RCTMGLEvent makeEvent:type withPayload:payload]; + [self fireEvent:event withCallback:reactMapView.onMapChange]; +} + +- (NSDictionary*)_makeRegionPayload:(MGLMapView*)mapView animated:(BOOL)animated +{ + MGLPointFeature *feature = [[MGLPointFeature alloc] init]; + feature.coordinate = mapView.centerCoordinate; + feature.attributes = @{ + @"zoomLevel": [NSNumber numberWithDouble:mapView.zoomLevel], + @"heading": [NSNumber numberWithDouble:mapView.camera.heading], + @"pitch": [NSNumber numberWithDouble:mapView.camera.pitch], + @"animated": [NSNumber numberWithBool:animated] + }; + return feature.geoJSONDictionary; } @end diff --git a/ios/RCTMGL/RCTMGLUtils.h b/ios/RCTMGL/RCTMGLUtils.h index 53b270d..17c6410 100644 --- a/ios/RCTMGL/RCTMGLUtils.h +++ b/ios/RCTMGL/RCTMGLUtils.h @@ -7,9 +7,12 @@ // #import +@import Mapbox; @interface RCTMGLUtils: NSObject -+ (CLLocationCoordinate2D)GeoJSONPoint:(id)json; ++ (CLLocationCoordinate2D)fromFeature:(NSString*)json; ++ (MGLCoordinateBounds)fromFeatureCollection:(NSString*)json; ++ (NSTimeInterval)fromMS:(NSNumber*)number; @end diff --git a/ios/RCTMGL/RCTMGLUtils.m b/ios/RCTMGL/RCTMGLUtils.m index a65b22c..8f57654 100644 --- a/ios/RCTMGL/RCTMGLUtils.m +++ b/ios/RCTMGL/RCTMGLUtils.m @@ -7,26 +7,33 @@ // #import "RCTMGLUtils.h" +@import Mapbox; @implementation RCTMGLUtils -+ (CLLocationCoordinate2D)GeoJSONPoint:(NSDictionary*)json -{ - NSDictionary *point = json; - - if (![[point objectForKey:@"type"] isEqual: @"Point"]) { - return CLLocationCoordinate2DMake(0, 0); - } - - NSArray *coords = (NSArray*)[point objectForKey:@"coordinates"]; - if (coords == nil || coords.count < 2) { - return CLLocationCoordinate2DMake(0, 0); - } +static double const MS_TO_S = 0.001; - double lat = [[coords objectAtIndex:1] doubleValue]; - double lng = [[coords objectAtIndex:0] doubleValue]; ++ (CLLocationCoordinate2D)fromFeature:(NSString*)jsonStr +{ + NSData* data = [jsonStr dataUsingEncoding:NSUTF8StringEncoding]; + MGLPointFeature *feature = (MGLPointFeature*)[MGLShape shapeWithData:data encoding:NSUTF8StringEncoding error:nil]; + return feature.coordinate; +} + ++ (MGLCoordinateBounds)fromFeatureCollection:(NSString*)jsonStr +{ + NSData* data = [jsonStr dataUsingEncoding:NSUTF8StringEncoding]; + MGLShapeCollectionFeature *featureCollection = (MGLShapeCollectionFeature*)[MGLShapeCollectionFeature shapeWithData:data encoding:NSUTF8StringEncoding error:nil]; - return CLLocationCoordinate2DMake(lat, lng); + CLLocationCoordinate2D ne = featureCollection.shapes[0].coordinate; + CLLocationCoordinate2D sw = featureCollection.shapes[1].coordinate; + + return MGLCoordinateBoundsMake(sw, ne); +} + ++ (NSTimeInterval)fromMS:(NSNumber *)number +{ + return [number doubleValue] * MS_TO_S; } @end diff --git a/ios/RCTMGL/ViewManager.h b/ios/RCTMGL/ViewManager.h new file mode 100644 index 0000000..83a5b1d --- /dev/null +++ b/ios/RCTMGL/ViewManager.h @@ -0,0 +1,16 @@ +// +// ViewManager.h +// RCTMGL +// +// Created by Nick Italiano on 8/31/17. +// Copyright © 2017 Mapbox Inc. All rights reserved. +// + +#import +#import "RCTMGLEvent.h" + +@interface ViewManager : RCTViewManager + +-(void)fireEvent:(RCTMGLEvent*)event withCallback:(RCTBubblingEventBlock)callback; + +@end diff --git a/ios/RCTMGL/ViewManager.m b/ios/RCTMGL/ViewManager.m new file mode 100644 index 0000000..3dd79fc --- /dev/null +++ b/ios/RCTMGL/ViewManager.m @@ -0,0 +1,52 @@ +// +// ViewManager.m +// RCTMGL +// +// Created by Nick Italiano on 8/31/17. +// Copyright © 2017 Mapbox Inc. All rights reserved. +// + +#import "ViewManager.h" + +@implementation ViewManager +{ + NSMutableDictionary *eventTimestampCache; +} + +static NSTimeInterval EVENT_THROTTLE_S = 0.1; + +- (instancetype)init +{ + if (self = [super init]) { + eventTimestampCache = [[NSMutableDictionary alloc] init]; + } + + return self; +} + +- (void)fireEvent:(RCTMGLEvent*)event withCallback:(RCTBubblingEventBlock)callback +{ + if (![self _shouldDropEvent:event]) { + NSString *cacheKey = [self _getCacheKey:event]; + NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; + [eventTimestampCache setObject:[NSNumber numberWithDouble:now] forKey:cacheKey]; + + if (callback != nil) { + callback([event toJSON]); + } + } +} + +- (BOOL)_shouldDropEvent:(RCTMGLEvent *)event +{ + NSString *cacheKey = [self _getCacheKey:event]; + NSNumber *lastTimestamp = [eventTimestampCache objectForKey:cacheKey]; + return lastTimestamp != nil && (event.timestamp - [lastTimestamp doubleValue]) <= EVENT_THROTTLE_S; +} + +- (NSString*)_getCacheKey:(RCTMGLEvent*)event +{ + return event.type; +} + +@end diff --git a/javascript/components/MapView.js b/javascript/components/MapView.js index 218a897..5a45ea5 100644 --- a/javascript/components/MapView.js +++ b/javascript/components/MapView.js @@ -1,55 +1,21 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { requireNativeComponent } from 'react-native'; -import { isFunction, runNativeCommand, noop } from '../utils'; +import { NativeModules, requireNativeComponent } from 'react-native'; +import { makePoint, makeLatLngBounds } from '../utils/geoUtils'; +import { isFunction, runNativeCommand } from '../utils'; -const DEFAULT_CENTER_COORDINATE = { - type: 'Point', - coordinates: [-77.036086, 38.910233], -}; +const MapboxGL = NativeModules.MGLModule; export const NATIVE_MODULE_NAME = 'RCTMGLMapView'; const RCTMGLMapView = requireNativeComponent(NATIVE_MODULE_NAME, MapView, { - nativeOnly: { onMapChange: true } + nativeOnly: { onMapChange: true }, }); /** * MapView backed by Mapbox Native GL */ class MapView extends React.Component { - static StyleURL = { - Street: 'mapbox-streets', - Dark: 'mapbox-dark', - Light: 'mapbox-light', - Outdoors: 'mapbox-outdoors', - Satellite: 'mapbox-satellite', - }; - - static EventTypes = { - // UI Interaction events - Press: 'press', - LongPress: 'longpress', - - // Map change events - RegionWillChange: 'regionwillchange', - RegionIsChanging: 'regionischanging', - RegionDidChange: 'regiondidchange', - WillStartLoadinMap: 'willstartloadingmap', - DidFinishLoadingMap: 'didfinishloadingmap', - DidFailLoadingMap: 'didfailoadingmap', - WillStartRenderingFrame: 'willstartrenderingframe', - DidFinishRenderingFrame: 'didfinishrenderingframe', - DidFinishRenderingFrameFully: 'didfinishrenderingframefully', - WillStartRenderingMap: 'willstartrenderingmap', - DidFinishRenderingMap: 'didfinishrenderingmap', - DidFinishRenderingMapFully: 'didfinishrenderingmapfully', - DidFinishLoadingStyle: 'didfinishloadingstyle', - - // Animation completion events - FlyToComplete: 'flytocomplete', - }; - static propTypes = { /** * Animates changes between pitch and bearing @@ -57,9 +23,19 @@ class MapView extends React.Component { animated: PropTypes.bool, /** - * Initial center coordinate on map + * Initial center coordinate on map [lng, lat] */ - centerCoordinate: PropTypes.object, + centerCoordinate: PropTypes.arrayOf(PropTypes.number), + + /** + * Shows the users location on the map + */ + showUserLocation: PropTypes.bool, + + /** + * The mode used to track the user location on the map + */ + userTrackingMode: PropTypes.number, /** * Initial heading on map @@ -109,93 +85,103 @@ class MapView extends React.Component { /** * Map press listener, gets called when a user presses the map */ - onPress: PropTypes.func, + onPress: PropTypes.func, - /** - * Map long press listener, gets called when a user long presses the map - */ - onLongPress: PropTypes.func, + /** + * Map long press listener, gets called when a user long presses the map + */ + onLongPress: PropTypes.func, - /** - * This event is triggered whenever the currently displayed map region is about to change. - */ - onRegionWillChange: PropTypes.func, + /** + * This event is triggered whenever the currently displayed map region is about to change. + */ + onRegionWillChange: PropTypes.func, - /** - * This event is triggered whenever the currently displayed map region is changing. - */ - onRegionIsChanging: PropTypes.func, + /** + * This event is triggered whenever the currently displayed map region is changing. + */ + onRegionIsChanging: PropTypes.func, - /** - * This event is triggered whenever the currently displayed map region finished changing - */ - onRegionDidChange: PropTypes.func, + /** + * This event is triggered whenever the currently displayed map region finished changing + */ + onRegionDidChange: PropTypes.func, - /** - * This event is triggered when the map is about to start loading a new map style. - */ - onWillStartLoadingMap: PropTypes.func, + /** + * This event is triggered when the map is about to start loading a new map style. + */ + onWillStartLoadingMap: PropTypes.func, - /** - * This is triggered when the map has successfully loaded a new map style. - */ - onDidFinishLoadingMap: PropTypes.func, + /** + * This is triggered when the map has successfully loaded a new map style. + */ + onDidFinishLoadingMap: PropTypes.func, - /** - * This event is triggered when the map has failed to load a new map style. - */ - onDidFailLoadingMap: PropTypes.func, + /** + * This event is triggered when the map has failed to load a new map style. + */ + onDidFailLoadingMap: PropTypes.func, - /** - * This event is triggered when the map will start rendering a frame. - */ - onWillStartRenderingFrame: PropTypes.func, + /** + * This event is triggered when the map will start rendering a frame. + */ + onWillStartRenderingFrame: PropTypes.func, - /** - * This event is triggered when the map finished rendering a frame. - */ - onDidFinishRenderingFrame: PropTypes.func, + /** + * This event is triggered when the map finished rendering a frame. + */ + onDidFinishRenderingFrame: PropTypes.func, - /** - * This event is triggered when the map fully finished rendering a frame. - */ - onDidFinishRenderingFrameFully: PropTypes.func, + /** + * This event is triggered when the map fully finished rendering a frame. + */ + onDidFinishRenderingFrameFully: PropTypes.func, - /** - * This event is triggered when the map will start rendering the map. - */ - onWillStartRenderingMap: PropTypes.func, + /** + * This event is triggered when the map will start rendering the map. + */ + onWillStartRenderingMap: PropTypes.func, - /** - * This event is triggered when the map finished rendering the map. - */ - onDidFinishRenderingMap: PropTypes.func, + /** + * This event is triggered when the map finished rendering the map. + */ + onDidFinishRenderingMap: PropTypes.func, - /** - * This event is triggered when the map fully finished rendering the map. - */ - onDidFinishRenderingMapFully: PropTypes.func, + /** + * This event is triggered when the map fully finished rendering the map. + */ + onDidFinishRenderingMapFully: PropTypes.func, - /** - * This event is triggered when a style has finished loading. - */ - onDidFinishLoadingStyle: PropTypes.func, + /** + * This event is triggered when a style has finished loading. + */ + onDidFinishLoadingStyle: PropTypes.func, - /** - * This event is triggered when a fly to animation is cancelled or completed - */ - onFlyToComplete: PropTypes.func, + /** + * This event is triggered when a fly to animation is cancelled or completed after calling flyTo + */ + onFlyToComplete: PropTypes.func, + + /** + * This event is triggered once the camera is finished after calling setCamera + */ + onSetCameraComplete: PropTypes.func, + + /** + * This event is triggered when the users location changes depands on showUserLocation + */ + onUserLocationChange: PropTypes.func, }; static defaultProps = { - animated: true, - centerCoordinate: DEFAULT_CENTER_COORDINATE, + animated: false, heading: 0, pitch: 0, scrollEnabled: true, pitchEnabled: true, zoomLevel: 16, - styleURL: MapView.StyleURL.Street, + userTrackingMode: MapboxGL.UserTrackingModes.None, + styleURL: MapboxGL.StyleURL.Street, }; constructor (props) { @@ -204,6 +190,18 @@ class MapView extends React.Component { this._onPress = this._onPress.bind(this); this._onLongPress = this._onLongPress.bind(this); this._onChange = this._onChange.bind(this); + this._onUserLocationChange = this._onUserLocationChange.bind(this); + } + + fitBounds (northEastCoordinates, southWestCoordinates, padding = 0, duration = 2000) { + if (!this._nativeRef) { + return; + } + runNativeCommand(NATIVE_MODULE_NAME, 'fitBounds', this._nativeRef, [ + this._packGeoJSON(makeLatLngBounds(northEastCoordinates, southWestCoordinates)), + padding, + duration, + ]); } flyTo (coordinates, duration = 2000) { @@ -211,11 +209,31 @@ class MapView extends React.Component { return; } runNativeCommand(NATIVE_MODULE_NAME, 'flyTo', this._nativeRef, [ - { type: 'Point', coordinates: coordinates }, + this._packGeoJSON(makePoint(coordinates)), duration, ]); } + setCamera (config = {}) { + if (!this._nativeRef) { + return; + } + + let centerCoordFeature; + if (config.centerCoordinate) { + centerCoordFeature = this._packGeoJSON(makePoint(config.centerCoordinate)); + } + + const cameraConfig = { + centerCoordinate: centerCoordFeature, + pitch: config.pitch, + heading: config.heading, + duration: config.duration || 2000, + }; + + runNativeCommand(NATIVE_MODULE_NAME, 'setCamera', this._nativeRef, [cameraConfig]); + } + _onPress (e) { if (isFunction(this.props.onPress)) { this.props.onPress(e.nativeEvent.payload); @@ -228,53 +246,62 @@ class MapView extends React.Component { } } + _onUserLocationChange (e) { + if (isFunction(this.props.onUserLocationChange)) { + this.props.onUserLocationChange(e.nativeEvent); + } + } + _onChange (e) { const { type, payload } = e.nativeEvent; let propName = ''; switch (type) { - case MapView.EventTypes.RegionWillChange: + case MapboxGL.EventTypes.RegionWillChange: propName = 'onRegionWillChange'; break; - case MapView.EventTypes.RegionIsChanging: + case MapboxGL.EventTypes.RegionIsChanging: propName = 'onRegionIsChanging'; break; - case MapView.EventTypes.RegionDidChange: + case MapboxGL.EventTypes.RegionDidChange: propName = 'onRegionDidChange'; break; - case MapView.EventTypes.WillStartLoadinMap: + case MapboxGL.EventTypes.WillStartLoadinMap: propName = 'onWillStartLoadingMap'; break; - case MapView.EventTypes.DidFinishLoadingMap: + case MapboxGL.EventTypes.DidFinishLoadingMap: propName = 'onDidFinishLoadingMap'; break; - case MapView.EventTypes.DidFailLoadingMap: + case MapboxGL.EventTypes.DidFailLoadingMap: propName = 'onDidFailLoadingMap'; break; - case MapView.EventTypes.WillStartRenderingFrame: + case MapboxGL.EventTypes.WillStartRenderingFrame: propName = 'onWillStartRenderingFrame'; break; - case MapView.EventTypes.DidFinishRenderingFrame: + case MapboxGL.EventTypes.DidFinishRenderingFrame: propName = 'onDidFinishRenderingFrame'; break; - case MapView.EventTypes.DidFinishRenderingFrameFully: + case MapboxGL.EventTypes.DidFinishRenderingFrameFully: propName = 'onDidFinishRenderingFrameFully'; break; - case MapView.EventTypes.WillStartRenderingMap: + case MapboxGL.EventTypes.WillStartRenderingMap: propName = 'onWillStartRenderingMap'; break; - case MapView.EventTypes.DidFinishRenderingMap: + case MapboxGL.EventTypes.DidFinishRenderingMap: propName = 'onDidFinishRenderingMap'; break; - case MapView.EventTypes.DidFinishRenderingMapFully: + case MapboxGL.EventTypes.DidFinishRenderingMapFully: propName = 'onDidFinishRenderingMapFully'; break; - case MapView.EventTypes.DidFinishLoadingStyle: + case MapboxGL.EventTypes.DidFinishLoadingStyle: propName = 'onDidFinishLoadingStyle'; break; - case MapView.EventTypes.FlyToComplete: + case MapboxGL.EventTypes.FlyToComplete: propName = 'onFlyToComplete'; break; + case MapboxGL.EventTypes.SetCameraComplete: + propName = 'onSetCameraComplete'; + break; } if (propName.length) { @@ -282,16 +309,29 @@ class MapView extends React.Component { } } - _handleOnChange(propName, payload) { + _handleOnChange (propName, payload) { if (isFunction(this.props[propName])) { this.props[propName](payload); } } + _getCenterCoordinate () { + if (!this.props.centerCoordinate) { + return; + } + return this._packGeoJSON((makePoint(this.props.centerCoordinate))); + } + + _packGeoJSON (geoJSON) { + return JSON.stringify(geoJSON); + } + render () { - const props = { + let props = { animated: this.props.animated, - centerCoordinate: this.props.centerCoordinate, + centerCoordinate: this._getCenterCoordinate(), + showUserLocation: this.props.showUserLocation, + userTrackingMode: this.props.userTrackingMode, heading: this.props.heading, pitch: this.props.pitch, style: this.props.style, @@ -307,6 +347,7 @@ class MapView extends React.Component { onPress: this._onPress, onLongPress: this._onLongPress, onMapChange: this._onChange, + onUserLocationChange: this._onUserLocationChange, }; return ( diff --git a/javascript/index.js b/javascript/index.js index cbbb7cc..0531a4f 100644 --- a/javascript/index.js +++ b/javascript/index.js @@ -1,8 +1,13 @@ +import { NativeModules } from 'react-native'; import MapView from './components/MapView'; +import * as geoUtils from './utils/geoUtils'; -let MapboxGL = {}; +let MapboxGL = { ...NativeModules.MGLModule }; // components MapboxGL.MapView = MapView; +// utils +MapboxGL.geoUtils = geoUtils; + export default MapboxGL; diff --git a/javascript/utils/geoUtils.js b/javascript/utils/geoUtils.js new file mode 100644 index 0000000..7ddf612 --- /dev/null +++ b/javascript/utils/geoUtils.js @@ -0,0 +1,12 @@ +import turfHelpers from '@turf/helpers'; + +export function makePoint (coordinates) { + return turfHelpers.point(coordinates); +} + +export function makeLatLngBounds (northEastCoordinates, southWestCoordinates) { + return turfHelpers.featureCollection([ + turfHelpers.point(northEastCoordinates), + turfHelpers.point(southWestCoordinates), + ]); +} diff --git a/package.json b/package.json index 1634ecb..115a855 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "eslint-plugin-react": "^5.2.2" }, "dependencies": { - "lodash": "^4.13.1" + "@turf/helpers": "^4.6.0" }, "peerDependencies": { "react": "16.0.0-alpha.12",