Skip to content

Commit 2e3112f

Browse files
committed
feat(snapshotter): add withLogo support and Android bounds parity
1 parent 1dc27ad commit 2e3112f

File tree

3 files changed

+201
-53
lines changed

3 files changed

+201
-53
lines changed

android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXSnapshotModule.kt

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ import com.facebook.react.bridge.ReactMethod
88
import com.facebook.react.bridge.ReadableMap
99
import com.facebook.react.module.annotations.ReactModule
1010
import com.mapbox.geojson.Feature
11+
import com.mapbox.geojson.FeatureCollection
1112
import com.mapbox.geojson.Point
1213
import com.mapbox.maps.CameraOptions
14+
import com.mapbox.maps.EdgeInsets
1315
import com.mapbox.maps.MapSnapshotOptions
1416
import com.mapbox.maps.Size
17+
import com.mapbox.maps.SnapshotOverlayOptions
1518
import com.mapbox.maps.Snapshotter
1619
import com.rnmapbox.rnmbx.modules.RNMBXModule.Companion.getAccessToken
1720
import com.rnmapbox.rnmbx.modules.RNMBXSnapshotModule
@@ -43,30 +46,39 @@ class RNMBXSnapshotModule(private val mContext: ReactApplicationContext) :
4346
// FileSource.getInstance(mContext).activate();
4447
mContext.runOnUiQueueThread {
4548
val snapshotterID = UUID.randomUUID().toString()
46-
val snapshotter = Snapshotter(mContext, getOptions(jsOptions))
49+
val showLogo = if (jsOptions.hasKey("withLogo")) jsOptions.getBoolean("withLogo") else true
50+
val overlayOptions = SnapshotOverlayOptions(showLogo = showLogo)
51+
val snapshotter = Snapshotter(mContext, getOptions(jsOptions), overlayOptions)
4752
snapshotter.setStyleUri(jsOptions.getString("styleURL")!!)
48-
snapshotter.setCamera(getCameraOptions(jsOptions))
53+
try {
54+
snapshotter.setCamera(getCameraOptions(jsOptions, snapshotter))
55+
} catch (e: IllegalArgumentException) {
56+
promise.reject(REACT_CLASS, e.message, e)
57+
mSnapshotterMap.remove(snapshotterID)
58+
return@runOnUiQueueThread
59+
}
4960
mSnapshotterMap[snapshotterID] = snapshotter
50-
snapshotter.startV11 { image,error ->
61+
62+
snapshotter.start(null) { image, error ->
5163
try {
5264
if (image == null) {
5365
Log.w(REACT_CLASS, "Snapshot failed: $error")
5466
promise.reject(REACT_CLASS, "Snapshot failed: $error")
5567
mSnapshotterMap.remove(snapshotterID)
5668
} else {
57-
val image = image.toMapboxImage()
69+
val mapboxImage = image.toMapboxImage()
5870
var result: String? = null
5971
result = if (jsOptions.getBoolean("writeToDisk")) {
60-
BitmapUtils.createImgTempFile(mContext, image)
72+
BitmapUtils.createImgTempFile(mContext, mapboxImage)
6173
} else {
62-
BitmapUtils.createImgBase64(image)
74+
BitmapUtils.createImgBase64(mapboxImage)
6375
}
6476
if (result == null) {
6577
promise.reject(
6678
REACT_CLASS,
6779
"Could not generate snapshot, please check Android logs for more info."
6880
)
69-
return@startV11
81+
return@start
7082
}
7183
promise.resolve(result)
7284
mSnapshotterMap.remove(snapshotterID)
@@ -79,17 +91,44 @@ class RNMBXSnapshotModule(private val mContext: ReactApplicationContext) :
7991
}
8092
}
8193

82-
private fun getCameraOptions(jsOptions: ReadableMap): CameraOptions {
83-
val centerPoint =
84-
Feature.fromJson(jsOptions.getString("centerCoordinate")!!)
85-
val point = centerPoint.geometry() as Point?
86-
val cameraOptionsBuilder = CameraOptions.Builder()
87-
return cameraOptionsBuilder
88-
.center(point)
89-
.pitch(jsOptions.getDouble("pitch"))
90-
.bearing(jsOptions.getDouble("heading"))
91-
.zoom(jsOptions.getDouble("zoomLevel"))
92-
.build()
94+
private fun getCameraOptions(jsOptions: ReadableMap, snapshotter: Snapshotter): CameraOptions {
95+
val pitch = jsOptions.getDouble("pitch")
96+
val heading = jsOptions.getDouble("heading")
97+
val zoomLevel = jsOptions.getDouble("zoomLevel")
98+
99+
// Check if centerCoordinate is provided
100+
if (jsOptions.hasKey("centerCoordinate") && !jsOptions.isNull("centerCoordinate")) {
101+
val centerPoint = Feature.fromJson(jsOptions.getString("centerCoordinate")!!)
102+
val point = centerPoint.geometry() as Point?
103+
return CameraOptions.Builder()
104+
.center(point)
105+
.pitch(pitch)
106+
.bearing(heading)
107+
.zoom(zoomLevel)
108+
.build()
109+
}
110+
111+
// Check if bounds is provided
112+
if (jsOptions.hasKey("bounds") && !jsOptions.isNull("bounds")) {
113+
val boundsJson = jsOptions.getString("bounds")!!
114+
val featureCollection = FeatureCollection.fromJson(boundsJson)
115+
val coords = featureCollection.features()?.mapNotNull { feature ->
116+
feature.geometry() as? Point
117+
} ?: emptyList()
118+
119+
if (coords.isEmpty()) {
120+
throw IllegalArgumentException("bounds contains no valid coordinates")
121+
}
122+
123+
return snapshotter.cameraForCoordinates(
124+
coords,
125+
EdgeInsets(0.0, 0.0, 0.0, 0.0),
126+
heading,
127+
pitch
128+
)
129+
}
130+
131+
throw IllegalArgumentException("neither centerCoordinate nor bounds provided")
93132
}
94133

95134
private fun getOptions(jsOptions: ReadableMap): MapSnapshotOptions {

example/src/examples/Camera/TakeSnapshot.js

Lines changed: 141 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
Dimensions,
88
Text,
99
ActivityIndicator,
10+
TouchableOpacity,
11+
ScrollView,
1012
} from 'react-native';
1113

1214
import BaseExamplePropTypes from '../common/BaseExamplePropTypes';
@@ -17,9 +19,31 @@ const styles = StyleSheet.create({
1719
padding: 16,
1820
},
1921
snapshot: {
20-
flex: 1,
22+
width: '100%',
23+
height: 200,
24+
marginBottom: 16,
2125
},
2226
spinnerContainer: { alignItems: 'center', flex: 1, justifyContent: 'center' },
27+
label: {
28+
fontSize: 14,
29+
fontWeight: 'bold',
30+
marginBottom: 8,
31+
color: '#333',
32+
},
33+
button: {
34+
backgroundColor: '#4264fb',
35+
padding: 12,
36+
borderRadius: 8,
37+
marginBottom: 16,
38+
},
39+
buttonText: {
40+
color: 'white',
41+
textAlign: 'center',
42+
fontWeight: 'bold',
43+
},
44+
section: {
45+
marginBottom: 24,
46+
},
2347
});
2448

2549
class TakeSnapshot extends React.Component {
@@ -31,54 +55,137 @@ class TakeSnapshot extends React.Component {
3155
super(props);
3256

3357
this.state = {
34-
snapshotURI: null,
58+
withLogoURI: null,
59+
withoutLogoURI: null,
60+
boundsURI: null,
61+
loading: true,
3562
};
3663
}
3764

3865
componentDidMount() {
39-
this.takeSnapshot();
66+
this.takeAllSnapshots();
4067
}
4168

42-
async takeSnapshot() {
43-
const { width, height } = Dimensions.get('window');
44-
45-
const uri = await snapshotManager.takeSnap({
46-
centerCoordinate: [-74.12641, 40.797968],
47-
width,
48-
height,
49-
zoomLevel: 12,
50-
pitch: 30,
51-
heading: 20,
52-
styleURL: StyleURL.Dark,
53-
writeToDisk: true,
54-
});
55-
56-
this.setState({ snapshotURI: uri });
69+
async takeAllSnapshots() {
70+
const { width } = Dimensions.get('window');
71+
const snapshotWidth = width - 32;
72+
const snapshotHeight = 200;
73+
74+
try {
75+
// Snapshot with logo (default)
76+
const withLogoURI = await snapshotManager.takeSnap({
77+
centerCoordinate: [-74.12641, 40.797968],
78+
width: snapshotWidth,
79+
height: snapshotHeight,
80+
zoomLevel: 12,
81+
pitch: 30,
82+
heading: 20,
83+
styleURL: StyleURL.Dark,
84+
writeToDisk: true,
85+
withLogo: true,
86+
});
87+
88+
// Snapshot without logo
89+
const withoutLogoURI = await snapshotManager.takeSnap({
90+
centerCoordinate: [-74.12641, 40.797968],
91+
width: snapshotWidth,
92+
height: snapshotHeight,
93+
zoomLevel: 12,
94+
pitch: 30,
95+
heading: 20,
96+
styleURL: StyleURL.Dark,
97+
writeToDisk: true,
98+
withLogo: false,
99+
});
100+
101+
// Snapshot using bounds instead of centerCoordinate
102+
const boundsURI = await snapshotManager.takeSnap({
103+
bounds: [
104+
[-74.2, 40.7],
105+
[-74.0, 40.9],
106+
],
107+
width: snapshotWidth,
108+
height: snapshotHeight,
109+
zoomLevel: 10,
110+
pitch: 0,
111+
heading: 0,
112+
styleURL: StyleURL.Street,
113+
writeToDisk: true,
114+
withLogo: true,
115+
});
116+
117+
this.setState({
118+
withLogoURI,
119+
withoutLogoURI,
120+
boundsURI,
121+
loading: false,
122+
});
123+
} catch (error) {
124+
console.error('Snapshot error:', error);
125+
this.setState({ loading: false });
126+
}
57127
}
58128

59129
render() {
60-
let childView = null;
130+
const { loading, withLogoURI, withoutLogoURI, boundsURI } = this.state;
61131

62-
if (!this.state.snapshotURI) {
63-
childView = (
132+
if (loading) {
133+
return (
64134
<View style={styles.spinnerContainer}>
65-
<ActivityIndicator size="large" color="#0000ff" />
66-
<Text>Generating Snapshot</Text>
67-
</View>
68-
);
69-
} else {
70-
childView = (
71-
<View style={styles.container}>
72-
<Image
73-
source={{ uri: this.state.snapshotURI }}
74-
resizeMode="contain"
75-
style={styles.snapshot}
76-
/>
135+
<ActivityIndicator size="large" color="#4264fb" />
136+
<Text>Generating Snapshots...</Text>
77137
</View>
78138
);
79139
}
80140

81-
return childView;
141+
return (
142+
<ScrollView style={styles.container}>
143+
<View style={styles.section}>
144+
<Text style={styles.label}>With Logo (withLogo: true)</Text>
145+
{withLogoURI && (
146+
<Image
147+
source={{ uri: withLogoURI }}
148+
resizeMode="contain"
149+
style={styles.snapshot}
150+
/>
151+
)}
152+
</View>
153+
154+
<View style={styles.section}>
155+
<Text style={styles.label}>Without Logo (withLogo: false)</Text>
156+
{withoutLogoURI && (
157+
<Image
158+
source={{ uri: withoutLogoURI }}
159+
resizeMode="contain"
160+
style={styles.snapshot}
161+
/>
162+
)}
163+
</View>
164+
165+
<View style={styles.section}>
166+
<Text style={styles.label}>
167+
Using Bounds (instead of centerCoordinate)
168+
</Text>
169+
{boundsURI && (
170+
<Image
171+
source={{ uri: boundsURI }}
172+
resizeMode="contain"
173+
style={styles.snapshot}
174+
/>
175+
)}
176+
</View>
177+
178+
<TouchableOpacity
179+
style={styles.button}
180+
onPress={() => {
181+
this.setState({ loading: true });
182+
this.takeAllSnapshots();
183+
}}
184+
>
185+
<Text style={styles.buttonText}>Retake Snapshots</Text>
186+
</TouchableOpacity>
187+
</ScrollView>
188+
);
82189
}
83190
}
84191

ios/RNMBX/RNMBXSnapshotModule.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,12 @@ class RNMBXSnapshotModule : NSObject {
9393
let height = jsOptions["height"] as? NSNumber else {
9494
throw RNMBXError.paramError("width, height: is not a number")
9595
}
96-
let mapSnapshotOptions = MapSnapshotOptions(
96+
let showsLogo = jsOptions["withLogo"] as? Bool ?? true
97+
var mapSnapshotOptions = MapSnapshotOptions(
9798
size: CGSize(width: width.doubleValue, height: height.doubleValue),
9899
pixelRatio: 1.0
99100
)
101+
mapSnapshotOptions.showsLogo = showsLogo
100102

101103
return mapSnapshotOptions
102104
}

0 commit comments

Comments
 (0)