diff --git a/README.md b/README.md index 4e308a4a4a..6b36fb930a 100644 --- a/README.md +++ b/README.md @@ -356,6 +356,12 @@ render() { } ``` +### Zoom to Specified Markers + +Pass an array of marker identifiers to have the map re-focus. + +![](http://i.giphy.com/3o7qEbOQnO0yoXqKJ2.gif) ![](http://i.giphy.com/l41YdrQZ7m6Dz4h0c.gif) + ### Troubleshooting #### My map is blank diff --git a/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapManager.java b/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapManager.java index 615351a0ae..5ca02097b8 100644 --- a/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapManager.java +++ b/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapManager.java @@ -30,6 +30,7 @@ public class AirMapManager extends ViewGroupManager { private static final int ANIMATE_TO_REGION = 1; private static final int ANIMATE_TO_COORDINATE = 2; private static final int FIT_TO_ELEMENTS = 3; + private static final int FIT_TO_SUPPLIED_MARKERS = 4; private final Map MAP_TYPES = MapBuilder.of( "standard", GoogleMap.MAP_TYPE_NORMAL, @@ -208,6 +209,10 @@ public void receiveCommand(AirMapView view, int commandId, @Nullable ReadableArr case FIT_TO_ELEMENTS: view.fitToElements(args.getBoolean(0)); break; + + case FIT_TO_SUPPLIED_MARKERS: + view.fitToSuppliedMarkers(args.getArray(0), args.getBoolean(1)); + break; } } @@ -240,7 +245,8 @@ public Map getCommandsMap() { return MapBuilder.of( "animateToRegion", ANIMATE_TO_REGION, "animateToCoordinate", ANIMATE_TO_COORDINATE, - "fitToElements", FIT_TO_ELEMENTS + "fitToElements", FIT_TO_ELEMENTS, + "fitToSuppliedMarkers", FIT_TO_SUPPLIED_MARKERS ); } diff --git a/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapMarker.java b/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapMarker.java index dbc835f1dd..8623812c26 100644 --- a/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapMarker.java +++ b/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapMarker.java @@ -40,6 +40,7 @@ public class AirMapMarker extends AirMapFeature { private Marker marker; private int width; private int height; + private String identifier; private LatLng position; private String title; @@ -123,6 +124,15 @@ public void setCoordinate(ReadableMap coordinate) { update(); } + public void setIdentifier(String identifier) { + this.identifier = identifier; + update(); + } + + public String getIdentifier() { + return this.identifier; + } + public void setTitle(String title) { this.title = title; if (marker != null) { @@ -288,13 +298,13 @@ public void update() { } marker.setIcon(getIcon()); - + if (anchorIsSet) { marker.setAnchor(anchorX, anchorY); } else { marker.setAnchor(0.5f, 1.0f); } - + if (calloutAnchorIsSet) { marker.setInfoWindowAnchor(calloutAnchorX, calloutAnchorY); } else { diff --git a/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapMarkerManager.java b/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapMarkerManager.java index 0f3ba76e76..52a0e6fc78 100644 --- a/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapMarkerManager.java +++ b/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapMarkerManager.java @@ -45,6 +45,11 @@ public void setTitle(AirMapMarker view, String title) { view.setTitle(title); } + @ReactProp(name = "identifier") + public void setIdentifier(AirMapMarker view, String identifier) { + view.setIdentifier(identifier); + } + @ReactProp(name = "description") public void setDescription(AirMapMarker view, String description) { view.setSnippet(description); diff --git a/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapView.java b/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapView.java index 57af2c7873..b817f804f5 100644 --- a/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapView.java +++ b/android/lib/src/main/java/com/airbnb/android/react/maps/AirMapView.java @@ -23,6 +23,7 @@ import android.widget.RelativeLayout; import android.widget.TextView; import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; @@ -44,6 +45,7 @@ import com.google.android.gms.maps.model.Polyline; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -512,6 +514,41 @@ public void fitToElements(boolean animated) { } } + public void fitToSuppliedMarkers(ReadableArray markerIDsArray, boolean animated) { + LatLngBounds.Builder builder = new LatLngBounds.Builder(); + + String[] markerIDs = new String[markerIDsArray.size()]; + for (int i = 0; i < markerIDsArray.size(); i++) { + markerIDs[i] = markerIDsArray.getString(i); + } + + boolean addedPosition = false; + + List markerIDList = Arrays.asList(markerIDs); + + for (AirMapFeature feature : features) { + if (feature instanceof AirMapMarker) { + String identifier = ((AirMapMarker)feature).getIdentifier(); + Marker marker = (Marker)feature.getFeature(); + if (markerIDList.contains(identifier)) { + builder.include(marker.getPosition()); + addedPosition = true; + } + } + } + + if (addedPosition) { + LatLngBounds bounds = builder.build(); + CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, 50); + if (animated) { + startMonitoringRegion(); + map.animateCamera(cu); + } else { + map.moveCamera(cu); + } + } + } + // InfoWindowAdapter interface @Override diff --git a/components/MapView.js b/components/MapView.js index 1214412b4a..e27c67d197 100644 --- a/components/MapView.js +++ b/components/MapView.js @@ -397,6 +397,10 @@ var MapView = React.createClass({ this._runCommand('fitToElements', [animated]); }, + fitToSuppliedMarkers: function(markers, animated) { + this._runCommand('fitToSuppliedMarkers', [markers, animated]); + }, + takeSnapshot: function (width, height, region, callback) { if (!region) { region = this.props.region || this.props.initialRegion; diff --git a/docs/mapview.md b/docs/mapview.md index 2fede1684d..1b8abc8d7f 100644 --- a/docs/mapview.md +++ b/docs/mapview.md @@ -53,6 +53,7 @@ | `animateToRegion` | `region: Region`, `duration: Number` | | `animateToCoordinate` | `region: Coordinate`, `duration: Number` | | `fitToElements` | `animated: Boolean` | +| `fitToSuppliedMarkers` | `markerIDs: String[]` | diff --git a/docs/marker.md b/docs/marker.md index b4e3fd034a..557490ab44 100644 --- a/docs/marker.md +++ b/docs/marker.md @@ -12,9 +12,9 @@ | `centerOffset` | `Point` | | The offset (in points) at which to display the view.

By default, the center point of an annotation view is placed at the coordinate point of the associated annotation. You can use this property to reposition the annotation view as needed. This x and y offset values are measured in points. Positive offset values move the annotation view down and to the right, while negative values move it up and to the left.

For android, see the `anchor` prop. | `calloutOffset` | `Point` | | The offset (in points) at which to place the callout bubble.

This property determines the additional distance by which to move the callout bubble. When this property is set to (0, 0), the anchor point of the callout bubble is placed on the top-center point of the marker view’s frame. Specifying positive offset values moves the callout bubble down and to the right, while specifying negative values moves it up and to the left.

For android, see the `calloutAnchor` prop. | `anchor` | `Point` | | Sets the anchor point for the marker.

The anchor specifies the point in the icon image that is anchored to the marker's position on the Earth's surface.

The anchor point is specified in the continuous space [0.0, 1.0] x [0.0, 1.0], where (0, 0) is the top-left corner of the image, and (1, 1) is the bottom-right corner. The anchoring point in a W x H image is the nearest discrete grid point in a (W + 1) x (H + 1) grid, obtained by scaling the then rounding. For example, in a 4 x 2 image, the anchor point (0.7, 0.6) resolves to the grid point at (3, 1).

For ios, see the `centerOffset` prop. -| `calloutAnchor` | `Point` | | Specifies the point in the marker image at which to anchor the callout when it is displayed. This is specified in the same coordinate system as the anchor. See the `andor` prop for more details.

The default is the top middle of the image.

For ios, see the `calloutOffset` prop. +| `calloutAnchor` | `Point` | | Specifies the point in the marker image at which to anchor the callout when it is displayed. This is specified in the same coordinate system as the anchor. See the `anchor` prop for more details.

The default is the top middle of the image.

For ios, see the `calloutOffset` prop. | `flat` | `Boolean` | | Sets whether this marker should be flat against the map true or a billboard facing the camera false. - +| `identifier` | `String` | | An identifier used to reference this marker at a later date. ## Events diff --git a/example/App.js b/example/App.js index 282559b31e..bec55f008a 100644 --- a/example/App.js +++ b/example/App.js @@ -22,7 +22,7 @@ var DefaultMarkers = require('./examples/DefaultMarkers'); var CachedMap = require('./examples/CachedMap'); var LoadingMap = require('./examples/LoadingMap'); var TakeSnapshot = require('./examples/TakeSnapshot'); - +var FitToSuppliedMarkers = require('./examples/FitToSuppliedMarkers'); var App = React.createClass({ @@ -87,6 +87,7 @@ var App = React.createClass({ [TakeSnapshot, 'Take Snapshot'], [CachedMap, 'Cached Map'], [LoadingMap, 'Map with loading'], + [FitToSuppliedMarkers, 'Focus Map On Markers'], ]); }, }); diff --git a/example/examples/FitToSuppliedMarkers.js b/example/examples/FitToSuppliedMarkers.js new file mode 100644 index 0000000000..bc5b867062 --- /dev/null +++ b/example/examples/FitToSuppliedMarkers.js @@ -0,0 +1,166 @@ +var React = require('react'); +var ReactNative = require('react-native'); +var { + StyleSheet, + PropTypes, + View, + Text, + Dimensions, + TouchableOpacity, + Image, +} = ReactNative; + +var MapView = require('react-native-maps'); +var PriceMarker = require('./PriceMarker'); + +var { width, height } = Dimensions.get('window'); + +const ASPECT_RATIO = width / height; +const LATITUDE = 37.78825; +const LONGITUDE = -122.4324; +const LATITUDE_DELTA = 0.0922; +const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO; +const SPACE = 0.01; + +var markerIDs = ['Marker1', 'Marker2', 'Marker3', 'Marker4', 'Marker5']; +var timeout = 4000; +var animationTimeout; + +var FocusOnMarkers = React.createClass({ + getInitialState() { + return { + a: { + latitude: LATITUDE + SPACE, + longitude: LONGITUDE + SPACE, + }, + b: { + latitude: LATITUDE - SPACE, + longitude: LONGITUDE - SPACE, + }, + c: { + latitude: LATITUDE - (SPACE * 2), + longitude: LONGITUDE - (SPACE * 2), + }, + d: { + latitude: LATITUDE - (SPACE * 3), + longitude: LONGITUDE - (SPACE * 3), + }, + e: { + latitude: LATITUDE - (SPACE * 4), + longitude: LONGITUDE - (SPACE * 4), + }, + } + }, + focusMap(markers, animated) { + console.log("Markers received to populate map: " + markers); + this.refs.map.fitToSuppliedMarkers(markers, animated); + }, + focus1() { + animationTimeout = setTimeout(() => { + this.focusMap([ + markerIDs[1], + markerIDs[4] + ], true); + + this.focus2(); + }, timeout); + }, + focus2() { + animationTimeout = setTimeout(() => { + this.focusMap([ + markerIDs[2], + markerIDs[3] + ], false); + + this.focus3() + }, timeout); + }, + focus3() { + animationTimeout = setTimeout(() => { + this.focusMap([ + markerIDs[1], + markerIDs[2] + ], false); + + this.focus4(); + }, timeout); + }, + focus4() { + animationTimeout = setTimeout(() => { + this.focusMap([ + markerIDs[0], + markerIDs[3] + ], true); + + this.focus1(); + }, timeout) + }, + componentDidMount() { + animationTimeout = setTimeout(() => { + this.focus1(); + }, timeout) + }, + componentWillUnmount() { + if (animationTimeout) { + clearTimeout(animationTimeout); + } + }, + render() { + return ( + + + + + + + + + + ); + }, +}); + +var styles = StyleSheet.create({ + container: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'flex-end', + alignItems: 'center', + }, + map: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + }, +}); + +module.exports = FocusOnMarkers; diff --git a/ios/AirMaps/AIRMapManager.m b/ios/AirMaps/AIRMapManager.m index be4b60f227..4fa8df4c92 100644 --- a/ios/AirMaps/AIRMapManager.m +++ b/ios/AirMaps/AIRMapManager.m @@ -161,6 +161,31 @@ - (UIView *)view }]; } +RCT_EXPORT_METHOD(fitToSuppliedMarkers:(nonnull NSNumber *)reactTag + markers:(nonnull NSArray *)markers + animated:(BOOL)animated) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + id view = viewRegistry[reactTag]; + if (![view isKindOfClass:[AIRMap class]]) { + RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); + } else { + AIRMap *mapView = (AIRMap *)view; + // TODO(lmr): we potentially want to include overlays here... and could concat the two arrays together. + id annotations = mapView.annotations; + + NSPredicate *filterMarkers = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { + AIRMapMarker *marker = (AIRMapMarker *)evaluatedObject; + return [markers containsObject:marker.identifier]; + }]; + + NSArray *filteredMarkers = [mapView.annotations filteredArrayUsingPredicate:filterMarkers]; + + [mapView showAnnotations:filteredMarkers animated:animated]; + } + }]; +} + RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)reactTag withWidth:(nonnull NSNumber *)width withHeight:(nonnull NSNumber *)height diff --git a/ios/AirMaps/AIRMapMarkerManager.m b/ios/AirMaps/AIRMapMarkerManager.m index 6dc8e40bc0..83a18c4a29 100644 --- a/ios/AirMaps/AIRMapMarkerManager.m +++ b/ios/AirMaps/AIRMapMarkerManager.m @@ -33,7 +33,7 @@ - (UIView *)view return marker; } -//RCT_EXPORT_VIEW_PROPERTY(identifier, NSString) +RCT_EXPORT_VIEW_PROPERTY(identifier, NSString) //RCT_EXPORT_VIEW_PROPERTY(reuseIdentifier, NSString) RCT_EXPORT_VIEW_PROPERTY(title, NSString) RCT_REMAP_VIEW_PROPERTY(description, subtitle, NSString)