Skip to content

Commit 93ae675

Browse files
jackashtonmfazekas
andauthored
fix(snapshotter): add withLogo support and Android bounds parity (#4154)
* feat(snapshotter): add withLogo support and Android bounds parity * Update android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXSnapshotModule.kt Co-authored-by: Miklós Fazekas <mfazekas@szemafor.com> --------- Co-authored-by: Miklós Fazekas <mfazekas@szemafor.com>
1 parent 61fdd58 commit 93ae675

File tree

3 files changed

+200
-53
lines changed

3 files changed

+200
-53
lines changed

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

Lines changed: 56 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,38 @@ 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+
return@runOnUiQueueThread
58+
}
4959
mSnapshotterMap[snapshotterID] = snapshotter
50-
snapshotter.startV11 { image,error ->
60+
61+
snapshotter.start(null) { image, error ->
5162
try {
5263
if (image == null) {
5364
Log.w(REACT_CLASS, "Snapshot failed: $error")
5465
promise.reject(REACT_CLASS, "Snapshot failed: $error")
5566
mSnapshotterMap.remove(snapshotterID)
5667
} else {
57-
val image = image.toMapboxImage()
68+
val mapboxImage = image.toMapboxImage()
5869
var result: String? = null
5970
result = if (jsOptions.getBoolean("writeToDisk")) {
60-
BitmapUtils.createImgTempFile(mContext, image)
71+
BitmapUtils.createImgTempFile(mContext, mapboxImage)
6172
} else {
62-
BitmapUtils.createImgBase64(image)
73+
BitmapUtils.createImgBase64(mapboxImage)
6374
}
6475
if (result == null) {
6576
promise.reject(
6677
REACT_CLASS,
6778
"Could not generate snapshot, please check Android logs for more info."
6879
)
69-
return@startV11
80+
return@start
7081
}
7182
promise.resolve(result)
7283
mSnapshotterMap.remove(snapshotterID)
@@ -79,17 +90,44 @@ class RNMBXSnapshotModule(private val mContext: ReactApplicationContext) :
7990
}
8091
}
8192

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

95133
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)