Skip to content

Commit

Permalink
Fit to supplied markers (react-native-maps#386)
Browse files Browse the repository at this point in the history
* Added support for zooming the map to specific markers

* Moved list creation outside of a loop in Android. Updated documentation for fitToSuppliedMarkers

* Tidied up the fitToSuppliedMarkers example

* Updated Android gif in the readme
  • Loading branch information
AidenMontgomery authored and Adel Grimm committed Dec 1, 2016
1 parent 45d4fc2 commit 81b3834
Show file tree
Hide file tree
Showing 12 changed files with 268 additions and 7 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class AirMapManager extends ViewGroupManager<AirMapView> {
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<String, Integer> MAP_TYPES = MapBuilder.of(
"standard", GoogleMap.MAP_TYPE_NORMAL,
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -240,7 +245,8 @@ public Map<String, Integer> 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
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String> 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
Expand Down
4 changes: 4 additions & 0 deletions components/MapView.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions docs/mapview.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
| `animateToRegion` | `region: Region`, `duration: Number` |
| `animateToCoordinate` | `region: Coordinate`, `duration: Number` |
| `fitToElements` | `animated: Boolean` |
| `fitToSuppliedMarkers` | `markerIDs: String[]` |



Expand Down
4 changes: 2 additions & 2 deletions docs/marker.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
| `centerOffset` | `Point` | | The offset (in points) at which to display the view.<br/><br/> 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.<br/><br/> For android, see the `anchor` prop.
| `calloutOffset` | `Point` | | The offset (in points) at which to place the callout bubble.<br/><br/> 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.<br/><br/> For android, see the `calloutAnchor` prop.
| `anchor` | `Point` | | Sets the anchor point for the marker.<br/><br/> The anchor specifies the point in the icon image that is anchored to the marker's position on the Earth's surface.<br/><br/> 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).<br/><br/> 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.<br/><br/> The default is the top middle of the image.<br/><br/> 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.<br/><br/> The default is the top middle of the image.<br/><br/> 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

Expand Down
3 changes: 2 additions & 1 deletion example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({

Expand Down Expand Up @@ -87,6 +87,7 @@ var App = React.createClass({
[TakeSnapshot, 'Take Snapshot'],
[CachedMap, 'Cached Map'],
[LoadingMap, 'Map with loading'],
[FitToSuppliedMarkers, 'Focus Map On Markers'],
]);
},
});
Expand Down
166 changes: 166 additions & 0 deletions example/examples/FitToSuppliedMarkers.js
Original file line number Diff line number Diff line change
@@ -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 (
<View style={styles.container}>
<MapView
ref="map"
style={styles.map}
initialRegion={{
latitude: LATITUDE,
longitude: LONGITUDE,
latitudeDelta: LATITUDE_DELTA,
longitudeDelta: LONGITUDE_DELTA,
}}
>
<MapView.Marker
identifier={'Marker1'}
coordinate={this.state.a}
/>
<MapView.Marker
identifier={'Marker2'}
coordinate={this.state.b}
/>
<MapView.Marker
identifier={'Marker3'}
coordinate={this.state.c}
/>
<MapView.Marker
identifier={'Marker4'}
coordinate={this.state.d}
/>
<MapView.Marker
identifier={'Marker5'}
coordinate={this.state.e}
/>
</MapView>
</View>
);
},
});

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;
25 changes: 25 additions & 0 deletions ios/AirMaps/AIRMapManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSNumber *, UIView *> *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
Expand Down
Loading

0 comments on commit 81b3834

Please sign in to comment.