diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 2eecd11..311e216 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -105,17 +105,7 @@ jobs: with: xcode-version: ${{ env.XCODE_VERSION }} - - name: Cache Pods - id: pods-cache - uses: actions/cache@v4.2.4 - with: - path: example/ios/Pods - key: ${{ runner.os }}-pods-${{ hashFiles('example/ios/Podfile', 'example/ios/Podfile.lock', 'example/package.json') }} - restore-keys: | - ${{ runner.os }}-pods- - - name: Install cocoapods - if: steps.pods-cache.outputs.cache-hit != 'true' working-directory: example run: yarn ios:pods diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index bdf02dd..46ab2ec 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -1,8 +1,12 @@ package com.rngooglemapsplus import android.annotation.SuppressLint +import android.graphics.Bitmap import android.location.Location +import android.util.Base64 +import android.util.Size import android.widget.FrameLayout +import androidx.core.graphics.scale import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.uimanager.PixelUtil.dpToPx @@ -29,11 +33,15 @@ import com.google.android.gms.maps.model.PolylineOptions import com.google.android.gms.maps.model.TileOverlay import com.google.android.gms.maps.model.TileOverlayOptions import com.google.maps.android.data.kml.KmlLayer +import com.margelo.nitro.core.Promise import com.rngooglemapsplus.extensions.toGooglePriority import com.rngooglemapsplus.extensions.toLocationErrorCode import com.rngooglemapsplus.extensions.toRNIndoorBuilding import com.rngooglemapsplus.extensions.toRNIndoorLevel import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream import java.nio.charset.StandardCharsets class GoogleMapsViewImpl( @@ -188,6 +196,8 @@ class GoogleMapsViewImpl( if (cameraPosition == lastSubmittedCameraPosition) { return } + lastSubmittedCameraPosition = cameraPosition + val isGesture = GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE == cameraMoveReason val latDelta = bounds.northeast.latitude - bounds.southwest.latitude @@ -207,7 +217,6 @@ class GoogleMapsViewImpl( ), isGesture, ) - lastSubmittedCameraPosition = cameraPosition } override fun onCameraIdle() { @@ -503,7 +512,7 @@ class GoogleMapsViewImpl( fun setCamera( cameraPosition: CameraPosition, animated: Boolean, - durationMS: Int, + durationMs: Int, ) { onUi { val current = googleMap?.cameraPosition @@ -514,7 +523,7 @@ class GoogleMapsViewImpl( val update = CameraUpdateFactory.newCameraPosition(cameraPosition) if (animated) { - googleMap?.animateCamera(update, durationMS, null) + googleMap?.animateCamera(update, durationMs, null) } else { googleMap?.moveCamera(update) } @@ -525,7 +534,7 @@ class GoogleMapsViewImpl( coordinates: Array, padding: RNMapPadding, animated: Boolean, - durationMS: Int, + durationMs: Int, ) { if (coordinates.isEmpty()) { return @@ -583,13 +592,85 @@ class GoogleMapsViewImpl( 0, ) if (animated) { - googleMap?.animateCamera(update, durationMS, null) + googleMap?.animateCamera(update, durationMs, null) } else { googleMap?.moveCamera(update) } } } + fun setCameraBounds(bounds: LatLngBounds?) { + onUi { + googleMap?.setLatLngBoundsForCameraTarget(bounds) + } + } + + fun animateToBounds( + bounds: LatLngBounds, + padding: Int, + durationMs: Int, + lockBounds: Boolean, + ) { + onUi { + if (lockBounds) { + googleMap?.setLatLngBoundsForCameraTarget(bounds) + } + val update = + CameraUpdateFactory.newLatLngBounds( + bounds, + padding, + ) + googleMap?.animateCamera(update, durationMs, null) + } + } + + fun snapshot( + size: Size?, + format: String, + compressFormat: Bitmap.CompressFormat, + quality: Double, + resultIsFile: Boolean, + ): Promise { + val promise = Promise() + onUi { + googleMap?.snapshot { bitmap -> + try { + if (bitmap == null) { + promise.resolve(null) + return@snapshot + } + + val scaledBitmap = + size?.let { + bitmap.scale(it.width, it.height) + } ?: bitmap + + val output = ByteArrayOutputStream() + scaledBitmap.compress(compressFormat, (quality * 100).toInt().coerceIn(0, 100), output) + val bytes = output.toByteArray() + + if (resultIsFile) { + val file = File(context.cacheDir, "map_snapshot_${System.currentTimeMillis()}.$format") + FileOutputStream(file).use { it.write(bytes) } + promise.resolve(file.absolutePath) + } else { + val base64 = Base64.encodeToString(bytes, Base64.NO_WRAP) + promise.resolve("data:image/$format;base64,$base64") + } + + if (scaledBitmap != bitmap) { + scaledBitmap.recycle() + } + bitmap.recycle() + } catch (e: Exception) { + promise.resolve(null) + } + } + } + + return promise + } + fun addMarker( id: String, opts: MarkerOptions, diff --git a/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt b/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt index 42313db..c03c8b5 100644 --- a/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt +++ b/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt @@ -131,7 +131,6 @@ class LocationHandler( private fun restartLocationUpdates() { stop() - // 4) Google Play Services checken – früh zurückmelden val playServicesStatus = GoogleApiAvailability .getInstance() diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index 7bbf97b..845421f 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -6,11 +6,16 @@ import com.facebook.react.uimanager.ThemedReactContext import com.google.android.gms.maps.model.MapStyleOptions import com.margelo.nitro.core.Promise import com.rngooglemapsplus.extensions.circleEquals +import com.rngooglemapsplus.extensions.isFileResult import com.rngooglemapsplus.extensions.markerEquals import com.rngooglemapsplus.extensions.polygonEquals import com.rngooglemapsplus.extensions.polylineEquals import com.rngooglemapsplus.extensions.toCameraPosition +import com.rngooglemapsplus.extensions.toCompressFormat +import com.rngooglemapsplus.extensions.toFileExtension +import com.rngooglemapsplus.extensions.toLatLngBounds import com.rngooglemapsplus.extensions.toMapColorScheme +import com.rngooglemapsplus.extensions.toSize @DoNotStrip class RNGoogleMapsPlusView( @@ -343,25 +348,54 @@ class RNGoogleMapsPlusView( override fun setCamera( camera: RNCamera, animated: Boolean?, - durationMS: Double?, + durationMs: Double?, ) { - view.setCamera(camera.toCameraPosition(), animated == true, durationMS?.toInt() ?: 3000) + view.setCamera(camera.toCameraPosition(), animated == true, durationMs?.toInt() ?: 3000) } override fun setCameraToCoordinates( coordinates: Array, padding: RNMapPadding?, animated: Boolean?, - durationMS: Double?, + durationMs: Double?, ) { view.setCameraToCoordinates( coordinates, padding = padding ?: RNMapPadding(0.0, 0.0, 0.0, 0.0), animated == true, - durationMS?.toInt() ?: 3000, + durationMs?.toInt() ?: 3000, ) } + override fun setCameraBounds(bounds: RNLatLngBounds?) { + view.setCameraBounds( + bounds?.toLatLngBounds(), + ) + } + + override fun animateToBounds( + bounds: RNLatLngBounds, + padding: Double?, + durationMs: Double?, + lockBounds: Boolean?, + ) { + view.animateToBounds( + bounds.toLatLngBounds(), + padding = padding?.toInt() ?: 0, + durationMs?.toInt() ?: 3000, + lockBounds = false, + ) + } + + override fun snapshot(options: RNSnapshotOptions): Promise = + view.snapshot( + size = options.size.toSize(), + format = options.format.toFileExtension(), + compressFormat = options.format.toCompressFormat(), + quality = options.quality, + resultIsFile = options.resultType.isFileResult(), + ) + override fun showLocationDialog() { locationHandler.showLocationDialog() } diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNLatLngBoundsExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNLatLngBoundsExtension.kt new file mode 100644 index 0000000..b073d0f --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNLatLngBoundsExtension.kt @@ -0,0 +1,17 @@ +package com.rngooglemapsplus.extensions + +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import com.rngooglemapsplus.RNLatLngBounds + +fun RNLatLngBounds.toLatLngBounds(): LatLngBounds = + LatLngBounds( + LatLng( + southWest.latitude, + southWest.longitude, + ), + LatLng( + northEast.latitude, + northEast.longitude, + ), + ) diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNSize.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNSize.kt new file mode 100644 index 0000000..132fd8a --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNSize.kt @@ -0,0 +1,7 @@ +package com.rngooglemapsplus.extensions + +import android.util.Size +import com.facebook.react.uimanager.PixelUtil.dpToPx +import com.rngooglemapsplus.RNSize + +fun RNSize?.toSize(): Size? = this?.let { Size(width.dpToPx().toInt(), height.dpToPx().toInt()) } diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotFormat.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotFormat.kt new file mode 100644 index 0000000..da71976 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotFormat.kt @@ -0,0 +1,16 @@ +package com.rngooglemapsplus.extensions + +import android.graphics.Bitmap +import com.rngooglemapsplus.RNSnapshotFormat + +fun RNSnapshotFormat?.toCompressFormat(): Bitmap.CompressFormat = + when (this) { + RNSnapshotFormat.JPG, RNSnapshotFormat.JPEG -> Bitmap.CompressFormat.JPEG + RNSnapshotFormat.PNG, null -> Bitmap.CompressFormat.PNG + } + +fun RNSnapshotFormat?.toFileExtension(): String = + when (this) { + RNSnapshotFormat.JPG, RNSnapshotFormat.JPEG -> "jpg" + RNSnapshotFormat.PNG, null -> "png" + } diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotResultType.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotResultType.kt new file mode 100644 index 0000000..112d333 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotResultType.kt @@ -0,0 +1,9 @@ +package com.rngooglemapsplus.extensions + +import com.rngooglemapsplus.RNSnapshotResultType + +fun RNSnapshotResultType?.isFileResult(): Boolean = + when (this) { + RNSnapshotResultType.FILE -> true + RNSnapshotResultType.BASE64, null -> false + } diff --git a/example/index.js b/example/index.js index 117ddca..470a3ba 100644 --- a/example/index.js +++ b/example/index.js @@ -1,5 +1,8 @@ import { AppRegistry } from 'react-native'; import App from './src/App'; import { name as appName } from './app.json'; +import { LogBox } from 'react-native'; + +LogBox.ignoreLogs(['InteractionManager has been deprecated']); AppRegistry.registerComponent(appName, () => App); diff --git a/example/src/App.tsx b/example/src/App.tsx index fe469a5..05f1756 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -21,7 +21,9 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { useColorScheme } from 'react-native'; import BlankScreen from './screens/BlankScreen'; import IndoorLevelMapScreen from './screens/IndoorLevelMapScreen'; +import CameraTestScreen from './screens/CameraTestScreen'; import type { RootStackParamList } from './types/navigation'; +import SnapshotTestScreen from './screens/SnaptshotTestScreen'; const Stack = createStackNavigator(); @@ -100,10 +102,20 @@ export default function App() { component={IndoorLevelMapScreen} options={{ title: 'Indoor level map' }} /> + + diff --git a/example/src/components/ControlPanel.tsx b/example/src/components/ControlPanel.tsx index 88bb30c..1df87fa 100644 --- a/example/src/components/ControlPanel.tsx +++ b/example/src/components/ControlPanel.tsx @@ -29,6 +29,7 @@ export default function ControlPanel({ mapRef, buttons }: Props) { const theme = useAppTheme(); const navigation = useNavigation(); const progress = useSharedValue(0); + const styles = getThemedStyles(theme); const toggle = () => { progress.value = withTiming(progress.value === 1 ? 0 : 1, { @@ -90,22 +91,16 @@ export default function ControlPanel({ mapRef, buttons }: Props) { return ( - - Controls - - - ▼ - + Controls + @@ -113,16 +108,11 @@ export default function ControlPanel({ mapRef, buttons }: Props) { {finalButtons.map((btn, i) => ( - - {btn.title} - + {btn.title} ))} @@ -131,53 +121,61 @@ export default function ControlPanel({ mapRef, buttons }: Props) { ); } -const styles = StyleSheet.create({ - scrollView: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - paddingHorizontal: 12, - paddingTop: 12, - }, - scrollContent: { - paddingBottom: 40, - }, - header: { - borderRadius: 10, - paddingVertical: 12, - alignItems: 'center', - marginBottom: 10, - flexDirection: 'row', - justifyContent: 'center', - }, - headerText: { - fontWeight: '600', - fontSize: 16, - marginRight: 6, - }, - arrow: { - fontSize: 14, - fontWeight: '600', - }, - animatedContainer: { - overflow: 'hidden', - }, - buttonList: { - gap: 8, - }, - button: { - paddingVertical: 12, - borderRadius: 10, - alignItems: 'center', - justifyContent: 'center', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.25, - shadowRadius: 4, - elevation: 1, - }, - buttonText: { - fontWeight: '600', - fontSize: 15, - }, -}); +const getThemedStyles = (theme: any) => + StyleSheet.create({ + scrollView: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + paddingHorizontal: 12, + paddingTop: 12, + backgroundColor: theme.bgPrimary, + }, + scrollContent: { + paddingBottom: 40, + }, + header: { + borderRadius: 10, + paddingVertical: 12, + alignItems: 'center', + marginBottom: 10, + flexDirection: 'row', + justifyContent: 'center', + backgroundColor: theme.bgHeader, + }, + headerText: { + fontWeight: '600', + fontSize: 16, + marginRight: 6, + color: theme.textPrimary, + }, + arrow: { + fontSize: 14, + fontWeight: '600', + color: theme.textPrimary, + }, + animatedContainer: { + overflow: 'hidden', + }, + buttonList: { + gap: 8, + }, + button: { + backgroundColor: theme.bgAccent, + paddingVertical: 12, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + shadowColor: theme.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 1, + }, + buttonText: { + fontWeight: '600', + fontSize: 15, + color: theme.textOnAccent, + }, + }); diff --git a/example/src/components/MapWrapper.tsx b/example/src/components/MapWrapper.tsx index 3fa12d0..a9cfb80 100644 --- a/example/src/components/MapWrapper.tsx +++ b/example/src/components/MapWrapper.tsx @@ -4,6 +4,8 @@ import { GoogleMapsView, type RNIndoorBuilding, type RNIndoorLevel, + RNLocationErrorCode, + RNMapErrorCode, } from 'react-native-google-maps-plus'; import type { GoogleMapsViewRef, @@ -58,10 +60,11 @@ export default function MapWrapper(props: Props) { [] ); - const mapPadding = useMemo( - () => ({ top: 20, left: 20, bottom: layout.bottom + 80, right: 20 }), - [layout.bottom] - ); + const mapPadding = useMemo(() => { + return props.children + ? { top: 20, left: 20, bottom: layout.bottom + 80, right: 20 } + : undefined; + }, [layout.bottom, props.children]); const mapZoomConfig = useMemo(() => ({ min: 0, max: 20 }), []); @@ -93,7 +96,7 @@ export default function MapWrapper(props: Props) { uiSettings={props.uiSettings ?? uiSettings} style={[styles.map, props.style]} userInterfaceStyle={ - (props.userInterfaceStyle ?? scheme === 'dark') ? 'dark' : 'light' + props.userInterfaceStyle ?? (scheme === 'dark' ? 'dark' : 'light') } mapType={props.mapType ?? 'normal'} mapZoomConfig={props.mapZoomConfig ?? mapZoomConfig} @@ -104,6 +107,11 @@ export default function MapWrapper(props: Props) { f: (ready: boolean) => console.log('Map is ready! ' + ready), } )} + onMapError={callback( + props.onMapError ?? { + f: (error: RNMapErrorCode) => console.log('Map error:', error), + } + )} onMapPress={callback( props.onMapPress ?? { f: (c: RNLatLng) => console.log('Map press:', c), @@ -184,7 +192,7 @@ export default function MapWrapper(props: Props) { )} onLocationError={callback( props.onLocationError ?? { - f: (e: any) => console.log('Location error', e), + f: (e: RNLocationErrorCode) => console.log('Location error', e), } )} /> diff --git a/example/src/screens/CameraTestScreen.tsx b/example/src/screens/CameraTestScreen.tsx new file mode 100644 index 0000000..3610685 --- /dev/null +++ b/example/src/screens/CameraTestScreen.tsx @@ -0,0 +1,81 @@ +import React, { useMemo, useRef, useState } from 'react'; +import MapWrapper from '../components/MapWrapper'; +import ControlPanel from '../components/ControlPanel'; +import type { + GoogleMapsViewRef, + RNLatLngBounds, + RNCamera, + RNLatLng, +} from 'react-native-google-maps-plus'; + +export default function CameraTestScreen() { + const mapRef = useRef(null); + const [boundsActive, setBoundsActive] = useState(false); + + const coordinates = useMemo( + () => [ + { latitude: 37.7749, longitude: -122.4194 }, + { latitude: 37.7849, longitude: -122.4094 }, + { latitude: 37.7649, longitude: -122.4294 }, + ], + [] + ); + + const bounds = useMemo( + () => ({ + southWest: { latitude: 37.703, longitude: -122.527 }, + northEast: { latitude: 37.833, longitude: -122.356 }, + }), + [] + ); + + const buttons = useMemo( + () => [ + { + title: 'Set Camera to SF', + onPress: () => { + const camera: RNCamera = { + center: { latitude: 37.7749, longitude: -122.4194 }, + zoom: 12, + bearing: 0, + tilt: 0, + }; + mapRef.current?.setCamera(camera, true, 1000); + }, + }, + { + title: 'Fit Coordinates', + onPress: () => + mapRef.current?.setCameraToCoordinates( + coordinates, + { top: 50, bottom: 50, left: 50, right: 50 }, + true, + 1000 + ), + }, + { + title: boundsActive ? 'Clear Camera Bounds' : 'Set Camera Bounds', + onPress: () => { + if (boundsActive) { + mapRef.current?.setCameraBounds(undefined); + setBoundsActive(false); + } else { + mapRef.current?.setCameraBounds(bounds); + setBoundsActive(true); + } + }, + }, + { + title: 'Animate To Bounds', + onPress: () => mapRef.current?.animateToBounds(bounds, 50, 1200), + }, + ], + [bounds, boundsActive, coordinates] + ); + + return ( + + + + ); +} diff --git a/example/src/screens/HomeScreen.tsx b/example/src/screens/HomeScreen.tsx index 66333cc..5c4455a 100644 --- a/example/src/screens/HomeScreen.tsx +++ b/example/src/screens/HomeScreen.tsx @@ -14,62 +14,68 @@ const screens = [ { name: 'KmlLayer', title: 'KML Layer' }, { name: 'Location', title: 'Location & Permissions' }, { name: 'CustomStyle', title: 'Custom Map Style' }, - { name: 'IndoorLevelMap', title: 'Indoor level map' }, - { name: 'StressTest', title: 'Stress Test' }, + { name: 'IndoorLevelMap', title: 'Indoor Level Map' }, + { name: 'Camera', title: 'Camera Test' }, + { name: 'Snapshot', title: 'Snapshot Test' }, + { name: 'Stress', title: 'Stress Test' }, ]; export default function HomeScreen() { const navigation = useNavigation>(); const theme = useAppTheme(); + const styles = getThemedStyles(theme); return ( - - - React Native Google Maps Plus Examples - + + React Native Google Maps Plus Examples {screens.map((s) => ( navigation.navigate(s.name)} activeOpacity={0.85} > - - {s.title} - + {s.title} ))} ); } -const styles = StyleSheet.create({ - container: { - flexGrow: 1, - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 40, - }, - title: { - fontSize: 18, - fontWeight: '600', - marginBottom: 20, - }, - button: { - paddingVertical: 14, - paddingHorizontal: 24, - borderRadius: 10, - marginVertical: 6, - width: '80%', - alignItems: 'center', - }, - buttonText: { - fontSize: 16, - fontWeight: '600', - }, -}); +const getThemedStyles = (theme: any) => + StyleSheet.create({ + container: { + flexGrow: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 40, + backgroundColor: theme.bgPrimary, + }, + title: { + fontSize: 18, + fontWeight: '600', + marginBottom: 24, + color: theme.textPrimary, + textAlign: 'center', + }, + button: { + backgroundColor: theme.bgAccent, + paddingVertical: 14, + paddingHorizontal: 24, + borderRadius: 10, + marginVertical: 6, + width: '80%', + alignItems: 'center', + justifyContent: 'center', + shadowColor: theme.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 2, + }, + buttonText: { + fontSize: 16, + fontWeight: '600', + color: theme.textOnAccent, + }, + }); diff --git a/example/src/screens/MarkersScreen.tsx b/example/src/screens/MarkersScreen.tsx index bccaf57..6e5c303 100644 --- a/example/src/screens/MarkersScreen.tsx +++ b/example/src/screens/MarkersScreen.tsx @@ -29,7 +29,7 @@ export default function MarkersScreen() { const coords = markers.map((m) => m.coordinate); mapRef.current?.setCameraToCoordinates( coords, - { top: 0, left: 0, bottom: 0, right: 0 }, + { top: 50, left: 50, bottom: 50, right: 50 }, true, 300 ); diff --git a/example/src/screens/SnaptshotTestScreen.tsx b/example/src/screens/SnaptshotTestScreen.tsx new file mode 100644 index 0000000..142baf6 --- /dev/null +++ b/example/src/screens/SnaptshotTestScreen.tsx @@ -0,0 +1,148 @@ +import React, { useMemo, useRef, useState } from 'react'; +import { Image, Modal, Pressable, StyleSheet, Text, View } from 'react-native'; +import MapWrapper from '../components/MapWrapper'; +import ControlPanel from '../components/ControlPanel'; +import { useAppTheme } from '../theme'; +import type { GoogleMapsViewRef } from 'react-native-google-maps-plus'; + +export default function SnapshotTestScreen() { + const mapRef = useRef(null); + const [snapshotUri, setSnapshotUri] = useState(null); + const [visible, setVisible] = useState(false); + + const theme = useAppTheme(); + + const buttons = useMemo( + () => [ + { + title: 'Take Snapshot (Base64)', + onPress: async () => { + try { + const result = await mapRef.current?.snapshot({ + format: 'jpg', + quality: 0.9, + resultType: 'base64', + }); + if (result) { + setSnapshotUri(result); + setVisible(true); + } + } catch (e) { + console.warn('Snapshot failed:', e); + } + }, + }, + { + title: 'Take Snapshot (File)', + onPress: async () => { + try { + const result = await mapRef.current?.snapshot({ + format: 'jpg', + quality: 0.9, + resultType: 'file', + }); + if (result) { + const uri = result.startsWith('file://') + ? result + : `file://${result}`; + setSnapshotUri(uri); + setVisible(true); + } + } catch (e) { + console.warn('Snapshot failed:', e); + } + }, + }, + ], + [] + ); + + const styles = getThemedStyles(theme); + + return ( + + + + + + + Map Snapshot + + {snapshotUri ? ( + + ) : ( + No image + )} + + setVisible(false)} + style={styles.closeButton} + > + Close + + + + + + ); +} + +const getThemedStyles = (theme: any) => + StyleSheet.create({ + backdrop: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.6)', + alignItems: 'center', + justifyContent: 'center', + }, + dialog: { + backgroundColor: theme.bgPrimary, + borderColor: theme.border, + padding: 20, + borderRadius: 14, + alignItems: 'center', + width: '80%', + shadowColor: theme.shadow, + shadowOpacity: 0.2, + shadowRadius: 10, + shadowOffset: { width: 0, height: 4 }, + elevation: 3, + }, + title: { + fontSize: 18, + fontWeight: '600', + marginBottom: 14, + color: theme.textPrimary, + }, + image: { + width: 260, + height: 260, + borderRadius: 10, + marginBottom: 20, + backgroundColor: theme.bgHeader, + }, + noImage: { + color: theme.textSecondary, + marginBottom: 20, + }, + closeButton: { + backgroundColor: theme.bgAccent, + borderRadius: 10, + paddingVertical: 10, + paddingHorizontal: 20, + shadowColor: theme.shadow, + shadowOpacity: 0.25, + shadowRadius: 6, + shadowOffset: { width: 0, height: 3 }, + elevation: 2, + }, + closeText: { + color: theme.textOnAccent, + fontWeight: '600', + fontSize: 15, + }, + }); diff --git a/example/src/screens/StressTestScreen.tsx b/example/src/screens/StressTestScreen.tsx index f33ecc6..27ea360 100644 --- a/example/src/screens/StressTestScreen.tsx +++ b/example/src/screens/StressTestScreen.tsx @@ -38,7 +38,7 @@ export default function StressTestScreen() { if (coords.length) mapRef.current?.setCameraToCoordinates( coords, - { top: 0, left: 0, bottom: 0, right: 0 }, + { top: 50, left: 50, bottom: 50, right: 50 }, true, 300 ); diff --git a/example/src/types/navigation.ts b/example/src/types/navigation.ts index 32d67ee..0cdaa46 100644 --- a/example/src/types/navigation.ts +++ b/example/src/types/navigation.ts @@ -11,6 +11,8 @@ export type RootStackParamList = { Location: undefined; CustomStyle: undefined; IndoorLevelMap: undefined; + Camera: undefined; + Snapshot: undefined; StressTest: undefined; }; diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index 500be6b..11b4f29 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -41,6 +41,7 @@ GMSIndoorDisplayDelegate { setupAppLifecycleObservers() } + @MainActor private func setupAppLifecycleObservers() { NotificationCenter.default.addObserver( self, @@ -82,6 +83,7 @@ GMSIndoorDisplayDelegate { mapReady = true } + @MainActor private func initLocationCallbacks() { locationHandler.onUpdate = { [weak self] loc in guard let self = self else { return } @@ -191,6 +193,7 @@ GMSIndoorDisplayDelegate { } } + @MainActor var currentCamera: GMSCameraPosition? { mapView?.camera } @@ -268,7 +271,8 @@ GMSIndoorDisplayDelegate { } } - @MainActor var mapPadding: RNMapPadding? { + @MainActor + var mapPadding: RNMapPadding? { didSet { mapView?.padding = mapPadding.map { @@ -282,13 +286,15 @@ GMSIndoorDisplayDelegate { } } - @MainActor var mapType: GMSMapViewType? { + @MainActor + var mapType: GMSMapViewType? { didSet { mapView?.mapType = mapType ?? .normal } } - @MainActor var locationConfig: RNLocationConfig? { + @MainActor + var locationConfig: RNLocationConfig? { didSet { locationHandler.desiredAccuracy = locationConfig?.ios?.desiredAccuracy?.toCLLocationAccuracy @@ -315,11 +321,12 @@ GMSIndoorDisplayDelegate { var onCameraChange: ((RNRegion, RNCamera, Bool) -> Void)? var onCameraChangeComplete: ((RNRegion, RNCamera, Bool) -> Void)? - func setCamera(camera: GMSCameraPosition, animated: Bool, durationMS: Double) { + @MainActor + func setCamera(camera: GMSCameraPosition, animated: Bool, durationMs: Double) { if animated { withCATransaction( disableActions: false, - duration: durationMS / 1000.0 + duration: durationMs / 1000.0 ) { mapView?.animate(to: camera) } @@ -329,11 +336,12 @@ GMSIndoorDisplayDelegate { } } + @MainActor func setCameraToCoordinates( coordinates: [RNLatLng], padding: RNMapPadding, animated: Bool, - durationMS: Double + durationMs: Double ) { if coordinates.isEmpty { return @@ -369,7 +377,7 @@ GMSIndoorDisplayDelegate { if animated { withCATransaction( disableActions: false, - duration: durationMS / 1000.0 + duration: durationMs / 1000.0 ) { mapView?.animate(with: update) } @@ -378,6 +386,90 @@ GMSIndoorDisplayDelegate { } } + @MainActor + func setCameraBounds(_ bounds: GMSCoordinateBounds?) { + mapView?.cameraTargetBounds = bounds + } + + @MainActor + func animateToBounds( + _ bounds: GMSCoordinateBounds, + padding: Double, + durationMs: Double, + lockBounds: Bool + ) { + if lockBounds { + mapView?.cameraTargetBounds = bounds + } + + let update = GMSCameraUpdate.fit(bounds, withPadding: CGFloat(padding)) + mapView?.animate(with: update) + } + + @MainActor + func snapshot( + size: CGSize?, + format: String, + imageFormat: ImageFormat, + quality: CGFloat, + resultIsFile: Bool + ) -> NitroModules.Promise { + let promise = Promise() + + DispatchQueue.main.async { + guard let mapView = self.mapView else { + promise.resolve(withResult: nil) + return + } + + let renderer = UIGraphicsImageRenderer(bounds: mapView.bounds) + let image = renderer.image { ctx in + mapView.layer.render(in: ctx.cgContext) + } + + var finalImage = image + + size.map { + UIGraphicsBeginImageContextWithOptions($0, false, 0.0) + image.draw(in: CGRect(origin: .zero, size: $0)) + finalImage = UIGraphicsGetImageFromCurrentImageContext() ?? image + UIGraphicsEndImageContext() + } + + let data: Data? + switch imageFormat { + case .jpeg: + data = finalImage.jpegData(compressionQuality: quality) + case .png: + data = finalImage.pngData() + } + + guard let imageData = data else { + promise.resolve(withResult: nil) + return + } + + // Rückgabe + if resultIsFile { + let filename = + "map_snapshot_\(Int(Date().timeIntervalSince1970)).\(format)" + let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent(filename) + do { + try imageData.write(to: fileURL) + promise.resolve(withResult: fileURL.path) + } catch { + promise.resolve(withResult: nil) + } + } else { + let base64 = imageData.base64EncodedString() + promise.resolve(withResult: "data:image/\(format);base64,\(base64)") + } + } + + return promise + } + @MainActor func addMarker(id: String, marker: GMSMarker) { if mapView == nil { @@ -619,177 +711,215 @@ GMSIndoorDisplayDelegate { } func mapView(_ mapView: GMSMapView, willMove gesture: Bool) { - let visibleRegion = mapView.projection.visibleRegion() - let bounds = GMSCoordinateBounds(region: visibleRegion) - - let center = CLLocationCoordinate2D( - latitude: (bounds.northEast.latitude + bounds.southWest.latitude) / 2.0, - longitude: (bounds.northEast.longitude + bounds.southWest.longitude) / 2.0 - ) + onMain { + let visibleRegion = mapView.projection.visibleRegion() + let bounds = GMSCoordinateBounds(region: visibleRegion) + + let center = CLLocationCoordinate2D( + latitude: (bounds.northEast.latitude + bounds.southWest.latitude) / 2.0, + longitude: (bounds.northEast.longitude + bounds.southWest.longitude) + / 2.0 + ) - let latDelta = bounds.northEast.latitude - bounds.southWest.latitude - let lngDelta = bounds.northEast.longitude - bounds.southWest.longitude + let latDelta = bounds.northEast.latitude - bounds.southWest.latitude + let lngDelta = bounds.northEast.longitude - bounds.southWest.longitude - let cp = mapView.camera - let region = RNRegion( - center: RNLatLng(center.latitude, center.longitude), - latitudeDelta: latDelta, - longitudeDelta: lngDelta - ) - let cam = RNCamera( - center: RNLatLng( - latitude: cp.target.latitude, - longitude: cp.target.longitude - ), - zoom: Double(cp.zoom), - bearing: cp.bearing, - tilt: cp.viewingAngle - ) - cameraMoveReasonIsGesture = gesture + let cp = mapView.camera + let region = RNRegion( + center: RNLatLng(center.latitude, center.longitude), + latitudeDelta: latDelta, + longitudeDelta: lngDelta + ) + let cam = RNCamera( + center: RNLatLng( + latitude: cp.target.latitude, + longitude: cp.target.longitude + ), + zoom: Double(cp.zoom), + bearing: cp.bearing, + tilt: cp.viewingAngle + ) + self.cameraMoveReasonIsGesture = gesture - onCameraChangeStart?(region, cam, gesture) + self.onCameraChangeStart?(region, cam, gesture) + } } func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition) { - if let last = lastSubmittedCameraPosition, - last.target.latitude == position.target.latitude, - last.target.longitude == position.target.longitude, - last.zoom == position.zoom, - last.bearing == position.bearing, - last.viewingAngle == position.viewingAngle { - return - } - let visibleRegion = mapView.projection.visibleRegion() - let bounds = GMSCoordinateBounds(region: visibleRegion) + onMain { + if let last = self.lastSubmittedCameraPosition, + last.target.latitude == position.target.latitude, + last.target.longitude == position.target.longitude, + last.zoom == position.zoom, + last.bearing == position.bearing, + last.viewingAngle == position.viewingAngle { + return + } - let center = CLLocationCoordinate2D( - latitude: (bounds.northEast.latitude + bounds.southWest.latitude) / 2.0, - longitude: (bounds.northEast.longitude + bounds.southWest.longitude) / 2.0 - ) + self.lastSubmittedCameraPosition = position + let visibleRegion = mapView.projection.visibleRegion() + let bounds = GMSCoordinateBounds(region: visibleRegion) - let latDelta = bounds.northEast.latitude - bounds.southWest.latitude - let lngDelta = bounds.northEast.longitude - bounds.southWest.longitude + let center = CLLocationCoordinate2D( + latitude: (bounds.northEast.latitude + bounds.southWest.latitude) / 2.0, + longitude: (bounds.northEast.longitude + bounds.southWest.longitude) + / 2.0 + ) - let cp = mapView.camera - let region = RNRegion( - center: RNLatLng(center.latitude, center.longitude), - latitudeDelta: latDelta, - longitudeDelta: lngDelta - ) - let cam = RNCamera( - center: RNLatLng( - latitude: cp.target.latitude, - longitude: cp.target.longitude - ), - zoom: Double(cp.zoom), - bearing: cp.bearing, - tilt: cp.viewingAngle - ) - onCameraChange?(region, cam, cameraMoveReasonIsGesture) - lastSubmittedCameraPosition = position + let latDelta = bounds.northEast.latitude - bounds.southWest.latitude + let lngDelta = bounds.northEast.longitude - bounds.southWest.longitude + + let cp = mapView.camera + let region = RNRegion( + center: RNLatLng(center.latitude, center.longitude), + latitudeDelta: latDelta, + longitudeDelta: lngDelta + ) + let cam = RNCamera( + center: RNLatLng( + latitude: cp.target.latitude, + longitude: cp.target.longitude + ), + zoom: Double(cp.zoom), + bearing: cp.bearing, + tilt: cp.viewingAngle + ) + self.onCameraChange?(region, cam, self.cameraMoveReasonIsGesture) + } } func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition) { - let visibleRegion = mapView.projection.visibleRegion() - let bounds = GMSCoordinateBounds(region: visibleRegion) - - let center = CLLocationCoordinate2D( - latitude: (bounds.northEast.latitude + bounds.southWest.latitude) / 2.0, - longitude: (bounds.northEast.longitude + bounds.southWest.longitude) / 2.0 - ) + onMain { + let visibleRegion = mapView.projection.visibleRegion() + let bounds = GMSCoordinateBounds(region: visibleRegion) + + let center = CLLocationCoordinate2D( + latitude: (bounds.northEast.latitude + bounds.southWest.latitude) / 2.0, + longitude: (bounds.northEast.longitude + bounds.southWest.longitude) + / 2.0 + ) - let latDelta = bounds.northEast.latitude - bounds.southWest.latitude - let lngDelta = bounds.northEast.longitude - bounds.southWest.longitude + let latDelta = bounds.northEast.latitude - bounds.southWest.latitude + let lngDelta = bounds.northEast.longitude - bounds.southWest.longitude - let cp = mapView.camera - let region = RNRegion( - center: RNLatLng(center.latitude, center.longitude), - latitudeDelta: latDelta, - longitudeDelta: lngDelta - ) - let cam = RNCamera( - center: RNLatLng( - latitude: cp.target.latitude, - longitude: cp.target.longitude - ), - zoom: Double(cp.zoom), - bearing: cp.bearing, - tilt: cp.viewingAngle - ) - onCameraChangeComplete?(region, cam, cameraMoveReasonIsGesture) + let cp = mapView.camera + let region = RNRegion( + center: RNLatLng(center.latitude, center.longitude), + latitudeDelta: latDelta, + longitudeDelta: lngDelta + ) + let cam = RNCamera( + center: RNLatLng( + latitude: cp.target.latitude, + longitude: cp.target.longitude + ), + zoom: Double(cp.zoom), + bearing: cp.bearing, + tilt: cp.viewingAngle + ) + self.onCameraChangeComplete?(region, cam, self.cameraMoveReasonIsGesture) + } } func mapView( _ mapView: GMSMapView, didTapAt coordinate: CLLocationCoordinate2D ) { - onMapPress?( - RNLatLng( - latitude: coordinate.latitude, - longitude: coordinate.longitude + onMain { + self.onMapPress?( + RNLatLng( + latitude: coordinate.latitude, + longitude: coordinate.longitude + ) ) - ) + } } func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool { - mapView.selectedMarker = marker - onMarkerPress?(marker.userData as? String, ) + onMain { + mapView.selectedMarker = marker + self.onMarkerPress?(marker.userData as? String, ) + } return true } func mapView(_ mapView: GMSMapView, didTap overlay: GMSOverlay) { - switch overlay { - case let circle as GMSCircle: - onCirclePress?(circle.userData as? String, ) + onMain { + switch overlay { + case let circle as GMSCircle: + self.onCirclePress?(circle.userData as? String, ) - case let polygon as GMSPolygon: - onPolygonPress?(polygon.userData as? String, ) + case let polygon as GMSPolygon: + self.onPolygonPress?(polygon.userData as? String, ) - case let polyline as GMSPolyline: - onPolylinePress?(polyline.userData as? String, ) + case let polyline as GMSPolyline: + self.onPolylinePress?(polyline.userData as? String, ) - default: - break + default: + break + } } } func mapView(_ mapView: GMSMapView, didBeginDragging marker: GMSMarker) { - onMarkerDragStart?( - marker.userData as? String, - RNLatLng(marker.position.latitude, marker.position.longitude) - ) + onMain { + self.onMarkerDragStart?( + marker.userData as? String, + RNLatLng(marker.position.latitude, marker.position.longitude) + ) + } } func mapView(_ mapView: GMSMapView, didDrag marker: GMSMarker) { - onMarkerDrag?( - marker.userData as? String, - RNLatLng(marker.position.latitude, marker.position.longitude) - ) + onMain { + self.onMarkerDrag?( + marker.userData as? String, + RNLatLng(marker.position.latitude, marker.position.longitude) + ) + } } func mapView(_ mapView: GMSMapView, didEndDragging marker: GMSMarker) { - onMarkerDragEnd?( - marker.userData as? String, - RNLatLng(marker.position.latitude, marker.position.longitude) - ) + onMain { + self.onMarkerDragEnd?( + marker.userData as? String, + RNLatLng(marker.position.latitude, marker.position.longitude) + ) + } } func didChangeActiveBuilding(_ building: GMSIndoorBuilding?) { - guard let display = mapView?.indoorDisplay, let building else { return } - onIndoorBuildingFocused?(building.toRNIndoorBuilding(from: display)) + onMain { + guard let display = self.mapView?.indoorDisplay, let building else { + return + } + self.onIndoorBuildingFocused?(building.toRNIndoorBuilding(from: display)) + } } func didChangeActiveLevel(_ level: GMSIndoorLevel?) { - guard - let display = mapView?.indoorDisplay, - let building = display.activeBuilding, - let level, - let index = building.levels.firstIndex(where: { - $0.name == level.name && $0.shortName == level.shortName - }) - else { return } - - onIndoorLevelActivated?(level.toRNIndoorLevel(index: index, active: true)) + onMain { + guard + let display = self.mapView?.indoorDisplay, + let building = display.activeBuilding, + let level, + let index = building.levels.firstIndex(where: { + $0.name == level.name && $0.shortName == level.shortName + }) + else { return } + + self.onIndoorLevelActivated?( + level.toRNIndoorLevel(index: index, active: true) + ) + } } +} +@inline(__always) +func onMain(_ block: @escaping () -> Void) { + if Thread.isMainThread { + block() + } else { + DispatchQueue.main.async { block() } + } } diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index 1cb8c16..cf7c0db 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -277,115 +277,160 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } - @MainActor var locationConfig: RNLocationConfig? { + @MainActor + var locationConfig: RNLocationConfig? { didSet { impl.locationConfig = locationConfig } } + @MainActor var onMapError: ((RNMapErrorCode) -> Void)? { didSet { impl.onMapError = onMapError } } + @MainActor var onMapReady: ((Bool) -> Void)? { didSet { impl.onMapReady = onMapReady } } + @MainActor var onLocationUpdate: ((RNLocation) -> Void)? { didSet { impl.onLocationUpdate = onLocationUpdate } } + @MainActor var onLocationError: ((_ error: RNLocationErrorCode) -> Void)? { didSet { impl.onLocationError = onLocationError } } + @MainActor var onMapPress: ((RNLatLng) -> Void)? { didSet { impl.onMapPress = onMapPress } } + @MainActor var onMarkerPress: ((String?) -> Void)? { didSet { impl.onMarkerPress = onMarkerPress } } + @MainActor var onPolylinePress: ((String?) -> Void)? { didSet { impl.onPolylinePress = onPolylinePress } } + @MainActor var onPolygonPress: ((String?) -> Void)? { didSet { impl.onPolygonPress = onPolygonPress } } + @MainActor var onCirclePress: ((String?) -> Void)? { didSet { impl.onCirclePress = onCirclePress } } + @MainActor var onMarkerDragStart: ((String?, RNLatLng) -> Void)? { didSet { impl.onMarkerDragStart = onMarkerDragStart } } + @MainActor var onMarkerDrag: ((String?, RNLatLng) -> Void)? { didSet { impl.onMarkerDrag = onMarkerDrag } } + @MainActor var onMarkerDragEnd: ((String?, RNLatLng) -> Void)? { didSet { impl.onMarkerDragEnd = onMarkerDragEnd } } + @MainActor var onIndoorBuildingFocused: ((RNIndoorBuilding) -> Void)? { didSet { impl.onIndoorBuildingFocused = onIndoorBuildingFocused } } + @MainActor var onIndoorLevelActivated: ((RNIndoorLevel) -> Void)? { didSet { impl.onIndoorLevelActivated = onIndoorLevelActivated } } + @MainActor var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? { didSet { impl.onCameraChangeStart = onCameraChangeStart } } + @MainActor var onCameraChange: ((RNRegion, RNCamera, Bool) -> Void)? { didSet { impl.onCameraChange = onCameraChange } } + @MainActor var onCameraChangeComplete: ((RNRegion, RNCamera, Bool) -> Void)? { didSet { impl.onCameraChangeComplete = onCameraChangeComplete } } - func setCamera(camera: RNCamera, animated: Bool?, durationMS: Double?) { + @MainActor + func setCamera(camera: RNCamera, animated: Bool?, durationMs: Double?) { let cam = camera.toGMSCameraPosition(current: impl.currentCamera) - onMain { - self.impl.setCamera( - camera: cam, - animated: animated ?? true, - durationMS: durationMS ?? 3000 - ) - } + impl.setCamera( + camera: cam, + animated: animated ?? true, + durationMs: durationMs ?? 3000 + ) } + @MainActor func setCameraToCoordinates( coordinates: [RNLatLng], padding: RNMapPadding?, animated: Bool?, - durationMS: Double? + durationMs: Double? ) { - onMain { - self.impl.setCameraToCoordinates( - coordinates: coordinates, - padding: padding ?? RNMapPadding(0, 0, 0, 0), - animated: animated ?? true, - durationMS: durationMS ?? 3000 - ) - } + impl.setCameraToCoordinates( + coordinates: coordinates, + padding: padding ?? RNMapPadding(0, 0, 0, 0), + animated: animated ?? true, + durationMs: durationMs ?? 3000 + ) + } + + @MainActor + func setCameraBounds(bounds: RNLatLngBounds?) { + impl.setCameraBounds(bounds?.toCoordinateBounds()) } + @MainActor + func animateToBounds( + bounds: RNLatLngBounds, + padding: Double?, + durationMs: Double?, + lockBounds: Bool? + ) { + impl.animateToBounds( + bounds.toCoordinateBounds(), + padding: padding ?? 0, + durationMs: durationMs ?? 3000, + lockBounds: false + ) + } + + @MainActor + func snapshot( + options: RNSnapshotOptions, + ) -> NitroModules.Promise { + return impl.snapshot( + size: options.size?.toCGSize(), + format: options.format.toFileExtension(), + imageFormat: options.format.toImageFormat(), + quality: CGFloat(options.quality), + resultIsFile: options.resultType.isFileResult() + ) + + } + + @MainActor func showLocationDialog() { locationHandler.showLocationDialog() } + @MainActor func openLocationSettings() { locationHandler.openLocationSettings() } + @MainActor func requestLocationPermission() -> NitroModules.Promise { return permissionHandler.requestLocationPermission() } + @MainActor func isGooglePlayServicesAvailable() -> Bool { /// not supported return true } } - -@inline(__always) -func onMain(_ block: @escaping () -> Void) { - if Thread.isMainThread { - block() - } else { - DispatchQueue.main.async { block() } - } -} diff --git a/ios/extensions/RNLatLngBounds+Extension.swift b/ios/extensions/RNLatLngBounds+Extension.swift new file mode 100644 index 0000000..2d5ca16 --- /dev/null +++ b/ios/extensions/RNLatLngBounds+Extension.swift @@ -0,0 +1,16 @@ +import GoogleMaps + +extension RNLatLngBounds { + func toCoordinateBounds() -> GMSCoordinateBounds { + return GMSCoordinateBounds( + coordinate: CLLocationCoordinate2D( + latitude: southWest.latitude, + longitude: southWest.longitude + ), + coordinate: CLLocationCoordinate2D( + latitude: northEast.latitude, + longitude: northEast.longitude + ) + ) + } +} diff --git a/ios/extensions/RNSize+Extension.swift b/ios/extensions/RNSize+Extension.swift new file mode 100644 index 0000000..723509e --- /dev/null +++ b/ios/extensions/RNSize+Extension.swift @@ -0,0 +1,7 @@ +import UIKit + +extension RNSize { + func toCGSize() -> CGSize? { + CGSize(width: width, height: height) + } +} diff --git a/ios/extensions/RNSnapshotFormat+Extension.swift b/ios/extensions/RNSnapshotFormat+Extension.swift new file mode 100644 index 0000000..a6afda7 --- /dev/null +++ b/ios/extensions/RNSnapshotFormat+Extension.swift @@ -0,0 +1,28 @@ +enum ImageFormat { + case png + case jpeg +} + +extension RNSnapshotFormat { + func toImageFormat() -> ImageFormat { + switch self { + case .jpg, .jpeg: + return .jpeg + case .png: + return .png + @unknown default: + return .png + } + } + + func toFileExtension() -> String { + switch self { + case .jpg, .jpeg: + return "jpg" + case .png: + return "png" + @unknown default: + return "png" + } + } +} diff --git a/ios/extensions/RNSnapshotResultType+Extension.swift b/ios/extensions/RNSnapshotResultType+Extension.swift new file mode 100644 index 0000000..61a3e88 --- /dev/null +++ b/ios/extensions/RNSnapshotResultType+Extension.swift @@ -0,0 +1,12 @@ +extension RNSnapshotResultType { + func isFileResult() -> Bool { + switch self { + case .file: + return true + case .base64: + return false + @unknown default: + return false + } + } +} diff --git a/src/RNGoogleMapsPlusView.nitro.ts b/src/RNGoogleMapsPlusView.nitro.ts index fbc41e4..24e6cda 100644 --- a/src/RNGoogleMapsPlusView.nitro.ts +++ b/src/RNGoogleMapsPlusView.nitro.ts @@ -26,6 +26,8 @@ import type { RNKMLayer, RNIndoorBuilding, RNIndoorLevel, + RNLatLngBounds, + RNSnapshotOptions, } from './types'; export interface RNGoogleMapsPlusViewProps extends HybridViewProps { @@ -79,15 +81,26 @@ export interface RNGoogleMapsPlusViewProps extends HybridViewProps { } export interface RNGoogleMapsPlusViewMethods extends HybridViewMethods { - setCamera(camera: RNCamera, animated?: boolean, durationMS?: number): void; + setCamera(camera: RNCamera, animated?: boolean, durationMs?: number): void; setCameraToCoordinates( coordinates: RNLatLng[], padding?: RNMapPadding, animated?: boolean, - durationMS?: number + durationMs?: number ): void; + setCameraBounds(bounds?: RNLatLngBounds): void; + + animateToBounds( + bounds: RNLatLngBounds, + padding?: number, + durationMs?: number, + lockBounds?: boolean + ): void; + + snapshot(options: RNSnapshotOptions): Promise; + showLocationDialog(): void; openLocationSettings(): void; diff --git a/src/types.ts b/src/types.ts index ab856ad..3746c6e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,9 +23,31 @@ export type RNMapUiSettings = { zoomGesturesEnabled?: boolean; }; -export type RNLatLng = { latitude: number; longitude: number }; +export type RNLatLng = { + latitude: number; + longitude: number; +}; + +export type RNLatLngBounds = { + northEast: RNLatLng; + southWest: RNLatLng; +}; + +export type RNSnapshotOptions = { + size?: RNSize; + format: RNSnapshotFormat; + quality: number; + resultType: RNSnapshotResultType; +}; + +export type RNSize = { + width: number; + height: number; +}; + +export type RNSnapshotFormat = 'png' | 'jpg' | 'jpeg'; -export type RNBoundingBox = { northEast: RNLatLng; southWest: RNLatLng }; +export type RNSnapshotResultType = 'base64' | 'file'; export type RNMapPadding = { top: number;