diff --git a/README.md b/README.md index 4e308a4a4..6b36fb930 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 615351a0a..5ca02097b 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 dbc835f1d..8623812c2 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 0f3ba76e7..52a0e6fc7 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 57af2c787..b817f804f 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 1214412b4..e27c67d19 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 2fede1684..1b8abc8d7 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 b4e3fd034..557490ab4 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 282559b31..bec55f008 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 000000000..bc5b86706 --- /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 be4b60f22..4fa8df4c9 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 6dc8e40bc..83a18c4a2 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)