Skip to content

Commit 81b3834

Browse files
AidenMontgomeryAdel Grimm
authored and
Adel Grimm
committed
Fit to supplied markers (react-native-maps#386)
* 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
1 parent 45d4fc2 commit 81b3834

File tree

12 files changed

+268
-7
lines changed

12 files changed

+268
-7
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,12 @@ render() {
356356
}
357357
```
358358

359+
### Zoom to Specified Markers
360+
361+
Pass an array of marker identifiers to have the map re-focus.
362+
363+
![](http://i.giphy.com/3o7qEbOQnO0yoXqKJ2.gif) ![](http://i.giphy.com/l41YdrQZ7m6Dz4h0c.gif)
364+
359365
### Troubleshooting
360366

361367
#### My map is blank

android/lib/src/main/java/com/airbnb/android/react/maps/AirMapManager.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class AirMapManager extends ViewGroupManager<AirMapView> {
3030
private static final int ANIMATE_TO_REGION = 1;
3131
private static final int ANIMATE_TO_COORDINATE = 2;
3232
private static final int FIT_TO_ELEMENTS = 3;
33+
private static final int FIT_TO_SUPPLIED_MARKERS = 4;
3334

3435
private final Map<String, Integer> MAP_TYPES = MapBuilder.of(
3536
"standard", GoogleMap.MAP_TYPE_NORMAL,
@@ -208,6 +209,10 @@ public void receiveCommand(AirMapView view, int commandId, @Nullable ReadableArr
208209
case FIT_TO_ELEMENTS:
209210
view.fitToElements(args.getBoolean(0));
210211
break;
212+
213+
case FIT_TO_SUPPLIED_MARKERS:
214+
view.fitToSuppliedMarkers(args.getArray(0), args.getBoolean(1));
215+
break;
211216
}
212217
}
213218

@@ -240,7 +245,8 @@ public Map<String, Integer> getCommandsMap() {
240245
return MapBuilder.of(
241246
"animateToRegion", ANIMATE_TO_REGION,
242247
"animateToCoordinate", ANIMATE_TO_COORDINATE,
243-
"fitToElements", FIT_TO_ELEMENTS
248+
"fitToElements", FIT_TO_ELEMENTS,
249+
"fitToSuppliedMarkers", FIT_TO_SUPPLIED_MARKERS
244250
);
245251
}
246252

android/lib/src/main/java/com/airbnb/android/react/maps/AirMapMarker.java

+12-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class AirMapMarker extends AirMapFeature {
4040
private Marker marker;
4141
private int width;
4242
private int height;
43+
private String identifier;
4344

4445
private LatLng position;
4546
private String title;
@@ -123,6 +124,15 @@ public void setCoordinate(ReadableMap coordinate) {
123124
update();
124125
}
125126

127+
public void setIdentifier(String identifier) {
128+
this.identifier = identifier;
129+
update();
130+
}
131+
132+
public String getIdentifier() {
133+
return this.identifier;
134+
}
135+
126136
public void setTitle(String title) {
127137
this.title = title;
128138
if (marker != null) {
@@ -288,13 +298,13 @@ public void update() {
288298
}
289299

290300
marker.setIcon(getIcon());
291-
301+
292302
if (anchorIsSet) {
293303
marker.setAnchor(anchorX, anchorY);
294304
} else {
295305
marker.setAnchor(0.5f, 1.0f);
296306
}
297-
307+
298308
if (calloutAnchorIsSet) {
299309
marker.setInfoWindowAnchor(calloutAnchorX, calloutAnchorY);
300310
} else {

android/lib/src/main/java/com/airbnb/android/react/maps/AirMapMarkerManager.java

+5
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ public void setTitle(AirMapMarker view, String title) {
4545
view.setTitle(title);
4646
}
4747

48+
@ReactProp(name = "identifier")
49+
public void setIdentifier(AirMapMarker view, String identifier) {
50+
view.setIdentifier(identifier);
51+
}
52+
4853
@ReactProp(name = "description")
4954
public void setDescription(AirMapMarker view, String description) {
5055
view.setSnippet(description);

android/lib/src/main/java/com/airbnb/android/react/maps/AirMapView.java

+37
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import android.widget.RelativeLayout;
2424
import android.widget.TextView;
2525
import com.facebook.react.bridge.LifecycleEventListener;
26+
import com.facebook.react.bridge.ReadableArray;
2627
import com.facebook.react.bridge.ReadableMap;
2728
import com.facebook.react.bridge.WritableMap;
2829
import com.facebook.react.bridge.WritableNativeMap;
@@ -44,6 +45,7 @@
4445
import com.google.android.gms.maps.model.Polyline;
4546

4647
import java.util.ArrayList;
48+
import java.util.Arrays;
4749
import java.util.HashMap;
4850
import java.util.List;
4951
import java.util.Map;
@@ -512,6 +514,41 @@ public void fitToElements(boolean animated) {
512514
}
513515
}
514516

517+
public void fitToSuppliedMarkers(ReadableArray markerIDsArray, boolean animated) {
518+
LatLngBounds.Builder builder = new LatLngBounds.Builder();
519+
520+
String[] markerIDs = new String[markerIDsArray.size()];
521+
for (int i = 0; i < markerIDsArray.size(); i++) {
522+
markerIDs[i] = markerIDsArray.getString(i);
523+
}
524+
525+
boolean addedPosition = false;
526+
527+
List<String> markerIDList = Arrays.asList(markerIDs);
528+
529+
for (AirMapFeature feature : features) {
530+
if (feature instanceof AirMapMarker) {
531+
String identifier = ((AirMapMarker)feature).getIdentifier();
532+
Marker marker = (Marker)feature.getFeature();
533+
if (markerIDList.contains(identifier)) {
534+
builder.include(marker.getPosition());
535+
addedPosition = true;
536+
}
537+
}
538+
}
539+
540+
if (addedPosition) {
541+
LatLngBounds bounds = builder.build();
542+
CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, 50);
543+
if (animated) {
544+
startMonitoringRegion();
545+
map.animateCamera(cu);
546+
} else {
547+
map.moveCamera(cu);
548+
}
549+
}
550+
}
551+
515552
// InfoWindowAdapter interface
516553

517554
@Override

components/MapView.js

+4
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,10 @@ var MapView = React.createClass({
397397
this._runCommand('fitToElements', [animated]);
398398
},
399399

400+
fitToSuppliedMarkers: function(markers, animated) {
401+
this._runCommand('fitToSuppliedMarkers', [markers, animated]);
402+
},
403+
400404
takeSnapshot: function (width, height, region, callback) {
401405
if (!region) {
402406
region = this.props.region || this.props.initialRegion;

docs/mapview.md

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
| `animateToRegion` | `region: Region`, `duration: Number` |
5454
| `animateToCoordinate` | `region: Coordinate`, `duration: Number` |
5555
| `fitToElements` | `animated: Boolean` |
56+
| `fitToSuppliedMarkers` | `markerIDs: String[]` |
5657

5758

5859

docs/marker.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
| `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.
1313
| `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.
1414
| `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.
15-
| `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.
15+
| `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.
1616
| `flat` | `Boolean` | | Sets whether this marker should be flat against the map true or a billboard facing the camera false.
17-
17+
| `identifier` | `String` | | An identifier used to reference this marker at a later date.
1818

1919
## Events
2020

example/App.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ var DefaultMarkers = require('./examples/DefaultMarkers');
2222
var CachedMap = require('./examples/CachedMap');
2323
var LoadingMap = require('./examples/LoadingMap');
2424
var TakeSnapshot = require('./examples/TakeSnapshot');
25-
25+
var FitToSuppliedMarkers = require('./examples/FitToSuppliedMarkers');
2626

2727
var App = React.createClass({
2828

@@ -87,6 +87,7 @@ var App = React.createClass({
8787
[TakeSnapshot, 'Take Snapshot'],
8888
[CachedMap, 'Cached Map'],
8989
[LoadingMap, 'Map with loading'],
90+
[FitToSuppliedMarkers, 'Focus Map On Markers'],
9091
]);
9192
},
9293
});
+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
var React = require('react');
2+
var ReactNative = require('react-native');
3+
var {
4+
StyleSheet,
5+
PropTypes,
6+
View,
7+
Text,
8+
Dimensions,
9+
TouchableOpacity,
10+
Image,
11+
} = ReactNative;
12+
13+
var MapView = require('react-native-maps');
14+
var PriceMarker = require('./PriceMarker');
15+
16+
var { width, height } = Dimensions.get('window');
17+
18+
const ASPECT_RATIO = width / height;
19+
const LATITUDE = 37.78825;
20+
const LONGITUDE = -122.4324;
21+
const LATITUDE_DELTA = 0.0922;
22+
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;
23+
const SPACE = 0.01;
24+
25+
var markerIDs = ['Marker1', 'Marker2', 'Marker3', 'Marker4', 'Marker5'];
26+
var timeout = 4000;
27+
var animationTimeout;
28+
29+
var FocusOnMarkers = React.createClass({
30+
getInitialState() {
31+
return {
32+
a: {
33+
latitude: LATITUDE + SPACE,
34+
longitude: LONGITUDE + SPACE,
35+
},
36+
b: {
37+
latitude: LATITUDE - SPACE,
38+
longitude: LONGITUDE - SPACE,
39+
},
40+
c: {
41+
latitude: LATITUDE - (SPACE * 2),
42+
longitude: LONGITUDE - (SPACE * 2),
43+
},
44+
d: {
45+
latitude: LATITUDE - (SPACE * 3),
46+
longitude: LONGITUDE - (SPACE * 3),
47+
},
48+
e: {
49+
latitude: LATITUDE - (SPACE * 4),
50+
longitude: LONGITUDE - (SPACE * 4),
51+
},
52+
}
53+
},
54+
focusMap(markers, animated) {
55+
console.log("Markers received to populate map: " + markers);
56+
this.refs.map.fitToSuppliedMarkers(markers, animated);
57+
},
58+
focus1() {
59+
animationTimeout = setTimeout(() => {
60+
this.focusMap([
61+
markerIDs[1],
62+
markerIDs[4]
63+
], true);
64+
65+
this.focus2();
66+
}, timeout);
67+
},
68+
focus2() {
69+
animationTimeout = setTimeout(() => {
70+
this.focusMap([
71+
markerIDs[2],
72+
markerIDs[3]
73+
], false);
74+
75+
this.focus3()
76+
}, timeout);
77+
},
78+
focus3() {
79+
animationTimeout = setTimeout(() => {
80+
this.focusMap([
81+
markerIDs[1],
82+
markerIDs[2]
83+
], false);
84+
85+
this.focus4();
86+
}, timeout);
87+
},
88+
focus4() {
89+
animationTimeout = setTimeout(() => {
90+
this.focusMap([
91+
markerIDs[0],
92+
markerIDs[3]
93+
], true);
94+
95+
this.focus1();
96+
}, timeout)
97+
},
98+
componentDidMount() {
99+
animationTimeout = setTimeout(() => {
100+
this.focus1();
101+
}, timeout)
102+
},
103+
componentWillUnmount() {
104+
if (animationTimeout) {
105+
clearTimeout(animationTimeout);
106+
}
107+
},
108+
render() {
109+
return (
110+
<View style={styles.container}>
111+
<MapView
112+
ref="map"
113+
style={styles.map}
114+
initialRegion={{
115+
latitude: LATITUDE,
116+
longitude: LONGITUDE,
117+
latitudeDelta: LATITUDE_DELTA,
118+
longitudeDelta: LONGITUDE_DELTA,
119+
}}
120+
>
121+
<MapView.Marker
122+
identifier={'Marker1'}
123+
coordinate={this.state.a}
124+
/>
125+
<MapView.Marker
126+
identifier={'Marker2'}
127+
coordinate={this.state.b}
128+
/>
129+
<MapView.Marker
130+
identifier={'Marker3'}
131+
coordinate={this.state.c}
132+
/>
133+
<MapView.Marker
134+
identifier={'Marker4'}
135+
coordinate={this.state.d}
136+
/>
137+
<MapView.Marker
138+
identifier={'Marker5'}
139+
coordinate={this.state.e}
140+
/>
141+
</MapView>
142+
</View>
143+
);
144+
},
145+
});
146+
147+
var styles = StyleSheet.create({
148+
container: {
149+
position: 'absolute',
150+
top: 0,
151+
left: 0,
152+
right: 0,
153+
bottom: 0,
154+
justifyContent: 'flex-end',
155+
alignItems: 'center',
156+
},
157+
map: {
158+
position: 'absolute',
159+
top: 0,
160+
left: 0,
161+
right: 0,
162+
bottom: 0,
163+
},
164+
});
165+
166+
module.exports = FocusOnMarkers;

ios/AirMaps/AIRMapManager.m

+25
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,31 @@ - (UIView *)view
161161
}];
162162
}
163163

164+
RCT_EXPORT_METHOD(fitToSuppliedMarkers:(nonnull NSNumber *)reactTag
165+
markers:(nonnull NSArray *)markers
166+
animated:(BOOL)animated)
167+
{
168+
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
169+
id view = viewRegistry[reactTag];
170+
if (![view isKindOfClass:[AIRMap class]]) {
171+
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
172+
} else {
173+
AIRMap *mapView = (AIRMap *)view;
174+
// TODO(lmr): we potentially want to include overlays here... and could concat the two arrays together.
175+
id annotations = mapView.annotations;
176+
177+
NSPredicate *filterMarkers = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
178+
AIRMapMarker *marker = (AIRMapMarker *)evaluatedObject;
179+
return [markers containsObject:marker.identifier];
180+
}];
181+
182+
NSArray *filteredMarkers = [mapView.annotations filteredArrayUsingPredicate:filterMarkers];
183+
184+
[mapView showAnnotations:filteredMarkers animated:animated];
185+
}
186+
}];
187+
}
188+
164189
RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)reactTag
165190
withWidth:(nonnull NSNumber *)width
166191
withHeight:(nonnull NSNumber *)height

0 commit comments

Comments
 (0)