From d7226abe46573923fd2cf40c60c79eba49ce0dbb Mon Sep 17 00:00:00 2001 From: Daniel Cohen Gindi Date: Wed, 10 Jan 2018 13:04:56 +0200 Subject: [PATCH] New methods to convert between LatLng and Point (#1851) * New methods to convert between LatLng and Point * Clarification for `pointForCoordinate` and `coordinateForPoint` --- docs/mapview.md | 2 + .../android/react/maps/AirMapModule.java | 75 +++++++++++++++++++ lib/components/MapView.js | 58 ++++++++++++++ lib/ios/AirGoogleMaps/AIRGoogleMapManager.m | 53 +++++++++++++ lib/ios/AirMaps/AIRMap.m | 32 ++++++++ 5 files changed, 220 insertions(+) diff --git a/docs/mapview.md b/docs/mapview.md index b2c021414..ddb8dc75c 100644 --- a/docs/mapview.md +++ b/docs/mapview.md @@ -71,6 +71,8 @@ To access event data, you will need to use `e.nativeEvent`. For example, `onPres | `fitToElements` | `animated: Boolean` | | `fitToSuppliedMarkers` | `markerIDs: String[]`, `animated: Boolean` | If you need to use this in `ComponentDidMount`, make sure you put it in a timeout or it will cause performance problems. | `fitToCoordinates` | `coordinates: Array, options: { edgePadding: EdgePadding, animated: Boolean }` | If called in `ComponentDidMount` in android, it will cause an exception. It is recommended to call it from the MapView `onLayout` event. +| `pointForCoordinate` | `coordinate: LatLng` | Converts a map coordinate to a view coordinate (`Point`). Returns a `Promise`. +| `coordinateForPoint` | `point: Point` | Converts a view coordinate (`Point`) to a map coordinate. Returns a `Promise`. diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapModule.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapModule.java index dfc71d6d9..02c43b2fb 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapModule.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapModule.java @@ -2,6 +2,7 @@ import android.app.Activity; import android.graphics.Bitmap; +import android.graphics.Point; import android.net.Uri; import android.util.Base64; import android.util.DisplayMetrics; @@ -11,11 +12,13 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.uimanager.NativeViewHierarchyManager; import com.facebook.react.uimanager.UIBlock; import com.facebook.react.uimanager.UIManagerModule; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.common.GoogleApiAvailability; +import com.google.android.gms.maps.model.LatLng; import java.io.ByteArrayOutputStream; import java.io.Closeable; @@ -135,4 +138,76 @@ public void onSnapshotReady(@Nullable Bitmap snapshot) { } }); } + + @ReactMethod + public void pointForCoordinate(final int tag, ReadableMap coordinate, final Promise promise) { + final LatLng coord = new LatLng( + coordinate.hasKey("latitude") ? coordinate.getDouble("latitude") : 0.0, + coordinate.hasKey("longitude") ? coordinate.getDouble("longitude") : 0.0 + ); + + final ReactApplicationContext context = getReactApplicationContext(); + UIManagerModule uiManager = context.getNativeModule(UIManagerModule.class); + uiManager.addUIBlock(new UIBlock() + { + @Override + public void execute(NativeViewHierarchyManager nvhm) + { + AirMapView view = (AirMapView) nvhm.resolveView(tag); + if (view == null) { + promise.reject("AirMapView not found"); + return; + } + if (view.map == null) { + promise.reject("AirMapView.map is not valid"); + return; + } + + Point pt = view.map.getProjection().toScreenLocation(coord); + + WritableMap ptJson = new WritableNativeMap(); + ptJson.putDouble("x", pt.x); + ptJson.putDouble("y", pt.y); + + promise.resolve(ptJson); + } + }); + } + + @ReactMethod + public void coordinateForPoint(final int tag, ReadableMap point, final Promise promise) { + final Point pt = new Point( + point.hasKey("x") ? point.getInt("x") : 0, + point.hasKey("y") ? point.getInt("y") : 0 + ); + + final ReactApplicationContext context = getReactApplicationContext(); + UIManagerModule uiManager = context.getNativeModule(UIManagerModule.class); + uiManager.addUIBlock(new UIBlock() + { + @Override + public void execute(NativeViewHierarchyManager nvhm) + { + AirMapView view = (AirMapView) nvhm.resolveView(tag); + if (view == null) + { + promise.reject("AirMapView not found"); + return; + } + if (view.map == null) + { + promise.reject("AirMapView.map is not valid"); + return; + } + + LatLng coord = view.map.getProjection().fromScreenLocation(pt); + + WritableMap coordJson = new WritableNativeMap(); + coordJson.putDouble("latitude", coord.latitude); + coordJson.putDouble("longitude", coord.longitude); + + promise.resolve(coordJson); + } + }); + } } diff --git a/lib/components/MapView.js b/lib/components/MapView.js index 1e821192c..bcc03f344 100644 --- a/lib/components/MapView.js +++ b/lib/components/MapView.js @@ -633,6 +633,64 @@ class MapView extends React.Component { return Promise.reject('takeSnapshot not supported on this platform'); } + /** + * Convert a map coordinate to user-space point + * + * @param coordinate Coordinate + * @param [coordinate.latitude] Latitude + * @param [coordinate.longitude] Longitude + * + * @return Promise Promise with the point ({ x: Number, y: Number }) + */ + pointForCoordinate(coordinate) { + if (Platform.OS === 'android') { + return NativeModules.AirMapModule.pointForCoordinate(this._getHandle(), coordinate); + } else if (Platform.OS === 'ios') { + return new Promise((resolve, reject) => { + this._runCommand('pointForCoordinate', [ + coordinate, + (err, snapshot) => { + if (err) { + reject(err); + } else { + resolve(snapshot); + } + }, + ]); + }); + } + return Promise.reject('pointForCoordinate not supported on this platform'); + } + + /** + * Convert a user-space point to a map coordinate + * + * @param point Point + * @param [point.x] X + * @param [point.x] Y + * + * @return Promise Promise with the coordinate ({ latitude: Number, longitude: Number }) + */ + coordinateFromPoint(point) { + if (Platform.OS === 'android') { + return NativeModules.AirMapModule.coordinateFromPoint(this._getHandle(), point); + } else if (Platform.OS === 'ios') { + return new Promise((resolve, reject) => { + this._runCommand('coordinateFromPoint', [ + point, + (err, snapshot) => { + if (err) { + reject(err); + } else { + resolve(snapshot); + } + }, + ]); + }); + } + return Promise.reject('coordinateFromPoint not supported on this platform'); + } + _uiManagerCommand(name) { return NativeModules.UIManager[getAirMapName(this.props.provider)].Commands[name]; } diff --git a/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m b/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m index 0d5e1d5c4..ba2077c96 100644 --- a/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m +++ b/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m @@ -294,6 +294,59 @@ - (UIView *)view }]; } +RCT_EXPORT_METHOD(pointForCoordinate:(nonnull NSNumber *)reactTag + coordinate:(NSDictionary *)coordinate + withCallback:(RCTResponseSenderBlock)callback) +{ + CLLocationCoordinate2D coord = + CLLocationCoordinate2DMake( + [coordinate[@"latitude"] doubleValue], + [coordinate[@"longitude"] doubleValue] + ); + + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + id view = viewRegistry[reactTag]; + if (![view isKindOfClass:[AIRGoogleMap class]]) { + RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); + } else { + AIRGoogleMap *mapView = (AIRGoogleMap *)view; + + CGPoint touchPoint = [mapView.projection pointForCoordinate:coord]; + + callback(@[[NSNull null], @{ + @"x": @(touchPoint.x), + @"y": @(touchPoint.y), + }]); + } + }]; +} + +RCT_EXPORT_METHOD(coordinateForPoint:(nonnull NSNumber *)reactTag + point:(NSDictionary *)point + withCallback:(RCTResponseSenderBlock)callback) +{ + CGPoint pt = CGPointMake( + [point[@"x"] doubleValue], + [point[@"y"] doubleValue] + ); + + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + id view = viewRegistry[reactTag]; + if (![view isKindOfClass:[AIRGoogleMap class]]) { + RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); + } else { + AIRGoogleMap *mapView = (AIRGoogleMap *)view; + + CLLocationCoordinate2D coordinate = [mapView.projection coordinateForPoint:pt]; + + callback(@[[NSNull null], @{ + @"latitude": @(coordinate.latitude), + @"longitude": @(coordinate.longitude), + }]); + } + }]; +} + RCT_EXPORT_METHOD(setMapBoundaries:(nonnull NSNumber *)reactTag northEast:(CLLocationCoordinate2D)northEast southWest:(CLLocationCoordinate2D)southWest) diff --git a/lib/ios/AirMaps/AIRMap.m b/lib/ios/AirMaps/AIRMap.m index cf194363a..335059877 100644 --- a/lib/ios/AirMaps/AIRMap.m +++ b/lib/ios/AirMaps/AIRMap.m @@ -302,6 +302,38 @@ - (void)setLoadingIndicatorColor:(UIColor *)loadingIndicatorColor { self.activityIndicatorView.color = loadingIndicatorColor; } +RCT_EXPORT_METHOD(pointForCoordinate:(NSDictionary *)coordinate resolver: (RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + CGPoint touchPoint = [self convertCoordinate: + CLLocationCoordinate2DMake( + [coordinate[@"lat"] doubleValue], + [coordinate[@"lng"] doubleValue] + ) + toPointToView:self]; + + resolve(@{ + @"x": @(touchPoint.x), + @"y": @(touchPoint.y), + }); +} + +RCT_EXPORT_METHOD(coordinateForPoint:(NSDictionary *)point resolver: (RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + CLLocationCoordinate2D coordinate = [self convertPoint: + CGPointMake( + [point[@"x"] doubleValue], + [point[@"y"] doubleValue] + ) + toCoordinateFromView:self]; + + resolve(@{ + @"lat": @(coordinate.latitude), + @"lng": @(coordinate.longitude), + }); +} + // Include properties of MKMapView which are only available on iOS 9+ // and check if their selector is available before calling super method.