diff --git a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXSnapshotModule.kt b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXSnapshotModule.kt index 415763509..771884c6d 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXSnapshotModule.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXSnapshotModule.kt @@ -8,10 +8,13 @@ import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableMap import com.facebook.react.module.annotations.ReactModule import com.mapbox.geojson.Feature +import com.mapbox.geojson.FeatureCollection import com.mapbox.geojson.Point import com.mapbox.maps.CameraOptions +import com.mapbox.maps.EdgeInsets import com.mapbox.maps.MapSnapshotOptions import com.mapbox.maps.Size +import com.mapbox.maps.SnapshotOverlayOptions import com.mapbox.maps.Snapshotter import com.rnmapbox.rnmbx.modules.RNMBXModule.Companion.getAccessToken import com.rnmapbox.rnmbx.modules.RNMBXSnapshotModule @@ -43,30 +46,38 @@ class RNMBXSnapshotModule(private val mContext: ReactApplicationContext) : // FileSource.getInstance(mContext).activate(); mContext.runOnUiQueueThread { val snapshotterID = UUID.randomUUID().toString() - val snapshotter = Snapshotter(mContext, getOptions(jsOptions)) + val showLogo = if (jsOptions.hasKey("withLogo")) jsOptions.getBoolean("withLogo") else true + val overlayOptions = SnapshotOverlayOptions(showLogo = showLogo) + val snapshotter = Snapshotter(mContext, getOptions(jsOptions), overlayOptions) snapshotter.setStyleUri(jsOptions.getString("styleURL")!!) - snapshotter.setCamera(getCameraOptions(jsOptions)) + try { + snapshotter.setCamera(getCameraOptions(jsOptions, snapshotter)) + } catch (e: IllegalArgumentException) { + promise.reject(REACT_CLASS, e.message, e) + return@runOnUiQueueThread + } mSnapshotterMap[snapshotterID] = snapshotter - snapshotter.startV11 { image,error -> + + snapshotter.start(null) { image, error -> try { if (image == null) { Log.w(REACT_CLASS, "Snapshot failed: $error") promise.reject(REACT_CLASS, "Snapshot failed: $error") mSnapshotterMap.remove(snapshotterID) } else { - val image = image.toMapboxImage() + val mapboxImage = image.toMapboxImage() var result: String? = null result = if (jsOptions.getBoolean("writeToDisk")) { - BitmapUtils.createImgTempFile(mContext, image) + BitmapUtils.createImgTempFile(mContext, mapboxImage) } else { - BitmapUtils.createImgBase64(image) + BitmapUtils.createImgBase64(mapboxImage) } if (result == null) { promise.reject( REACT_CLASS, "Could not generate snapshot, please check Android logs for more info." ) - return@startV11 + return@start } promise.resolve(result) mSnapshotterMap.remove(snapshotterID) @@ -79,17 +90,44 @@ class RNMBXSnapshotModule(private val mContext: ReactApplicationContext) : } } - private fun getCameraOptions(jsOptions: ReadableMap): CameraOptions { - val centerPoint = - Feature.fromJson(jsOptions.getString("centerCoordinate")!!) - val point = centerPoint.geometry() as Point? - val cameraOptionsBuilder = CameraOptions.Builder() - return cameraOptionsBuilder - .center(point) - .pitch(jsOptions.getDouble("pitch")) - .bearing(jsOptions.getDouble("heading")) - .zoom(jsOptions.getDouble("zoomLevel")) - .build() + private fun getCameraOptions(jsOptions: ReadableMap, snapshotter: Snapshotter): CameraOptions { + val pitch = jsOptions.getDouble("pitch") + val heading = jsOptions.getDouble("heading") + val zoomLevel = jsOptions.getDouble("zoomLevel") + + // Check if centerCoordinate is provided + if (jsOptions.hasKey("centerCoordinate") && !jsOptions.isNull("centerCoordinate")) { + val centerPoint = Feature.fromJson(jsOptions.getString("centerCoordinate")!!) + val point = centerPoint.geometry() as Point? + return CameraOptions.Builder() + .center(point) + .pitch(pitch) + .bearing(heading) + .zoom(zoomLevel) + .build() + } + + // Check if bounds is provided + if (jsOptions.hasKey("bounds") && !jsOptions.isNull("bounds")) { + val boundsJson = jsOptions.getString("bounds")!! + val featureCollection = FeatureCollection.fromJson(boundsJson) + val coords = featureCollection.features()?.mapNotNull { feature -> + feature.geometry() as? Point + } ?: emptyList() + + if (coords.isEmpty()) { + throw IllegalArgumentException("bounds contains no valid coordinates") + } + + return snapshotter.cameraForCoordinates( + coords, + EdgeInsets(0.0, 0.0, 0.0, 0.0), + heading, + pitch + ) + } + + throw IllegalArgumentException("neither centerCoordinate nor bounds provided") } private fun getOptions(jsOptions: ReadableMap): MapSnapshotOptions { diff --git a/example/src/examples/Camera/TakeSnapshot.js b/example/src/examples/Camera/TakeSnapshot.js index 22c2976d8..2208d55d6 100755 --- a/example/src/examples/Camera/TakeSnapshot.js +++ b/example/src/examples/Camera/TakeSnapshot.js @@ -7,6 +7,8 @@ import { Dimensions, Text, ActivityIndicator, + TouchableOpacity, + ScrollView, } from 'react-native'; import BaseExamplePropTypes from '../common/BaseExamplePropTypes'; @@ -17,9 +19,31 @@ const styles = StyleSheet.create({ padding: 16, }, snapshot: { - flex: 1, + width: '100%', + height: 200, + marginBottom: 16, }, spinnerContainer: { alignItems: 'center', flex: 1, justifyContent: 'center' }, + label: { + fontSize: 14, + fontWeight: 'bold', + marginBottom: 8, + color: '#333', + }, + button: { + backgroundColor: '#4264fb', + padding: 12, + borderRadius: 8, + marginBottom: 16, + }, + buttonText: { + color: 'white', + textAlign: 'center', + fontWeight: 'bold', + }, + section: { + marginBottom: 24, + }, }); class TakeSnapshot extends React.Component { @@ -31,54 +55,137 @@ class TakeSnapshot extends React.Component { super(props); this.state = { - snapshotURI: null, + withLogoURI: null, + withoutLogoURI: null, + boundsURI: null, + loading: true, }; } componentDidMount() { - this.takeSnapshot(); + this.takeAllSnapshots(); } - async takeSnapshot() { - const { width, height } = Dimensions.get('window'); - - const uri = await snapshotManager.takeSnap({ - centerCoordinate: [-74.12641, 40.797968], - width, - height, - zoomLevel: 12, - pitch: 30, - heading: 20, - styleURL: StyleURL.Dark, - writeToDisk: true, - }); - - this.setState({ snapshotURI: uri }); + async takeAllSnapshots() { + const { width } = Dimensions.get('window'); + const snapshotWidth = width - 32; + const snapshotHeight = 200; + + try { + // Snapshot with logo (default) + const withLogoURI = await snapshotManager.takeSnap({ + centerCoordinate: [-74.12641, 40.797968], + width: snapshotWidth, + height: snapshotHeight, + zoomLevel: 12, + pitch: 30, + heading: 20, + styleURL: StyleURL.Dark, + writeToDisk: true, + withLogo: true, + }); + + // Snapshot without logo + const withoutLogoURI = await snapshotManager.takeSnap({ + centerCoordinate: [-74.12641, 40.797968], + width: snapshotWidth, + height: snapshotHeight, + zoomLevel: 12, + pitch: 30, + heading: 20, + styleURL: StyleURL.Dark, + writeToDisk: true, + withLogo: false, + }); + + // Snapshot using bounds instead of centerCoordinate + const boundsURI = await snapshotManager.takeSnap({ + bounds: [ + [-74.2, 40.7], + [-74.0, 40.9], + ], + width: snapshotWidth, + height: snapshotHeight, + zoomLevel: 10, + pitch: 0, + heading: 0, + styleURL: StyleURL.Street, + writeToDisk: true, + withLogo: true, + }); + + this.setState({ + withLogoURI, + withoutLogoURI, + boundsURI, + loading: false, + }); + } catch (error) { + console.error('Snapshot error:', error); + this.setState({ loading: false }); + } } render() { - let childView = null; + const { loading, withLogoURI, withoutLogoURI, boundsURI } = this.state; - if (!this.state.snapshotURI) { - childView = ( + if (loading) { + return ( - - Generating Snapshot - - ); - } else { - childView = ( - - + + Generating Snapshots... ); } - return childView; + return ( + + + With Logo (withLogo: true) + {withLogoURI && ( + + )} + + + + Without Logo (withLogo: false) + {withoutLogoURI && ( + + )} + + + + + Using Bounds (instead of centerCoordinate) + + {boundsURI && ( + + )} + + + { + this.setState({ loading: true }); + this.takeAllSnapshots(); + }} + > + Retake Snapshots + + + ); } } diff --git a/ios/RNMBX/RNMBXSnapshotModule.swift b/ios/RNMBX/RNMBXSnapshotModule.swift index 84c8e583e..1d1e2869b 100644 --- a/ios/RNMBX/RNMBXSnapshotModule.swift +++ b/ios/RNMBX/RNMBXSnapshotModule.swift @@ -93,10 +93,12 @@ class RNMBXSnapshotModule : NSObject { let height = jsOptions["height"] as? NSNumber else { throw RNMBXError.paramError("width, height: is not a number") } - let mapSnapshotOptions = MapSnapshotOptions( + let showsLogo = jsOptions["withLogo"] as? Bool ?? true + var mapSnapshotOptions = MapSnapshotOptions( size: CGSize(width: width.doubleValue, height: height.doubleValue), pixelRatio: 1.0 ) + mapSnapshotOptions.showsLogo = showsLogo return mapSnapshotOptions }