From 64073d432d48a8898282c246385b7a5ee7843080 Mon Sep 17 00:00:00 2001 From: pinpong Date: Fri, 10 Oct 2025 14:58:16 +0700 Subject: [PATCH] feat: add indoor building focus and level activation listener support --- .../rngooglemapsplus/GoogleMapsViewImpl.kt | 21 ++++++++++++ .../rngooglemapsplus/RNGoogleMapsPlusView.kt | 10 ++++++ .../extensions/IndoorBuildingExtension.kt | 33 +++++++++++++++++++ example/src/App.tsx | 6 ++++ example/src/components/MapWrapper.tsx | 18 +++++++++- example/src/screens/HomeScreen.tsx | 1 + example/src/screens/IndoorLevelMapScreen.tsx | 26 +++++++++++++++ example/src/types/navigation.ts | 1 + ios/GoogleMapViewImpl.swift | 30 +++++++++++++++-- ios/RNGoogleMapsPlusView.swift | 6 ++++ ios/extensions/IndoorBuilding+Extension.swift | 33 +++++++++++++++++++ src/RNGoogleMapsPlusView.nitro.ts | 4 +++ src/types.ts | 14 ++++++++ 13 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 android/src/main/java/com/rngooglemapsplus/extensions/IndoorBuildingExtension.kt create mode 100644 example/src/screens/IndoorLevelMapScreen.tsx create mode 100644 ios/extensions/IndoorBuilding+Extension.swift diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index 29e51e0..bdf02dd 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -15,6 +15,7 @@ import com.google.android.gms.maps.MapView import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.Circle import com.google.android.gms.maps.model.CircleOptions +import com.google.android.gms.maps.model.IndoorBuilding import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds import com.google.android.gms.maps.model.MapColorScheme @@ -30,6 +31,8 @@ import com.google.android.gms.maps.model.TileOverlayOptions import com.google.maps.android.data.kml.KmlLayer 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.nio.charset.StandardCharsets @@ -48,6 +51,7 @@ class GoogleMapsViewImpl( GoogleMap.OnPolygonClickListener, GoogleMap.OnCircleClickListener, GoogleMap.OnMarkerDragListener, + GoogleMap.OnIndoorStateChangeListener, LifecycleEventListener { private var initialized = false private var mapReady = false @@ -490,6 +494,8 @@ class GoogleMapsViewImpl( var onMarkerDragStart: ((String?, RNLatLng) -> Unit)? = null var onMarkerDrag: ((String?, RNLatLng) -> Unit)? = null var onMarkerDragEnd: ((String?, RNLatLng) -> Unit)? = null + var onIndoorBuildingFocused: ((RNIndoorBuilding) -> Unit)? = null + var onIndoorLevelActivated: ((RNIndoorLevel) -> Unit)? = null var onCameraChangeStart: ((RNRegion, RNCamera, Boolean) -> Unit)? = null var onCameraChange: ((RNRegion, RNCamera, Boolean) -> Unit)? = null var onCameraChangeComplete: ((RNRegion, RNCamera, Boolean) -> Unit)? = null @@ -1003,6 +1009,21 @@ class GoogleMapsViewImpl( RNLatLng(marker.position.latitude, marker.position.longitude), ) } + + override fun onIndoorBuildingFocused() { + val building = googleMap?.focusedBuilding ?: return + onIndoorBuildingFocused?.invoke(building.toRNIndoorBuilding()) + } + + override fun onIndoorLevelActivated(indoorBuilding: IndoorBuilding) { + val activeLevel = indoorBuilding.levels.getOrNull(indoorBuilding.activeLevelIndex) ?: return + onIndoorLevelActivated?.invoke( + activeLevel.toRNIndoorLevel( + indoorBuilding.activeLevelIndex, + true, + ), + ) + } } private inline fun onUi(crossinline block: () -> Unit) { diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index 3173f46..7bbf97b 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -315,6 +315,16 @@ class RNGoogleMapsPlusView( view.onMarkerDragEnd = cb } + override var onIndoorBuildingFocused: ((RNIndoorBuilding) -> Unit)? = null + set(cb) { + view.onIndoorBuildingFocused = cb + } + + override var onIndoorLevelActivated: ((RNIndoorLevel) -> Unit)? = null + set(cb) { + view.onIndoorLevelActivated = cb + } + override var onCameraChangeStart: ((RNRegion, RNCamera, Boolean) -> Unit)? = null set(cb) { view.onCameraChangeStart = cb diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/IndoorBuildingExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/IndoorBuildingExtension.kt new file mode 100644 index 0000000..eace541 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/IndoorBuildingExtension.kt @@ -0,0 +1,33 @@ +package com.rngooglemapsplus.extensions + +import com.google.android.gms.maps.model.IndoorBuilding +import com.google.android.gms.maps.model.IndoorLevel +import com.rngooglemapsplus.RNIndoorBuilding +import com.rngooglemapsplus.RNIndoorLevel + +fun IndoorBuilding.toRNIndoorBuilding(): RNIndoorBuilding { + val mappedLevels = + levels + .mapIndexed { index, level -> + val active = index == activeLevelIndex + level.toRNIndoorLevel(index, active) + }.toTypedArray() + + return RNIndoorBuilding( + activeLevelIndex = activeLevelIndex.toDouble(), + defaultLevelIndex = defaultLevelIndex.toDouble(), + levels = mappedLevels, + underground = isUnderground, + ) +} + +fun IndoorLevel.toRNIndoorLevel( + index: Int, + active: Boolean, +): RNIndoorLevel = + RNIndoorLevel( + index = index.toDouble(), + name = name, + shortName = shortName, + active = active, + ) diff --git a/example/src/App.tsx b/example/src/App.tsx index 6add98f..fe469a5 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -20,6 +20,7 @@ import StressTestScreen from './screens/StressTestScreen'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { useColorScheme } from 'react-native'; import BlankScreen from './screens/BlankScreen'; +import IndoorLevelMapScreen from './screens/IndoorLevelMapScreen'; import type { RootStackParamList } from './types/navigation'; const Stack = createStackNavigator(); @@ -94,6 +95,11 @@ export default function App() { component={CustomStyleScreen} options={{ title: 'Custom Map Style' }} /> + + console.log('Indoor building focused', building), + } + )} + onIndoorLevelActivated={callback( + props.onIndoorLevelActivated ?? { + f: (level: RNIndoorLevel) => + console.log('Indoor level activated', level), + } + )} onCameraChangeStart={callback( props.onCameraChangeStart ?? { f: (r: RNRegion, cam: RNCamera, g: boolean) => diff --git a/example/src/screens/HomeScreen.tsx b/example/src/screens/HomeScreen.tsx index fb721b1..66333cc 100644 --- a/example/src/screens/HomeScreen.tsx +++ b/example/src/screens/HomeScreen.tsx @@ -14,6 +14,7 @@ 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' }, ]; diff --git a/example/src/screens/IndoorLevelMapScreen.tsx b/example/src/screens/IndoorLevelMapScreen.tsx new file mode 100644 index 0000000..6e09919 --- /dev/null +++ b/example/src/screens/IndoorLevelMapScreen.tsx @@ -0,0 +1,26 @@ +import React, { useRef } from 'react'; +import MapWrapper from '../components/MapWrapper'; +import type { GoogleMapsViewRef } from 'react-native-google-maps-plus'; +import ControlPanel from '../components/ControlPanel'; + +export default function IndoorLevelMapScreen() { + const mapRef = useRef(null); + return ( + + + + ); +} diff --git a/example/src/types/navigation.ts b/example/src/types/navigation.ts index 052a36a..32d67ee 100644 --- a/example/src/types/navigation.ts +++ b/example/src/types/navigation.ts @@ -10,6 +10,7 @@ export type RootStackParamList = { KmlLayer: undefined; Location: undefined; CustomStyle: undefined; + IndoorLevelMap: undefined; StressTest: undefined; }; diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index 3080a84..500be6b 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -3,7 +3,8 @@ import GoogleMaps import GoogleMapsUtils import UIKit -final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { +final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate, +GMSIndoorDisplayDelegate { private let locationHandler: LocationHandler private let markerBuilder: MapMarkerBuilder @@ -136,7 +137,10 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { myLocationEnabled.map { mapView?.isMyLocationEnabled = $0 } buildingEnabled.map { mapView?.isBuildingsEnabled = $0 } trafficEnabled.map { mapView?.isTrafficEnabled = $0 } - indoorEnabled.map { mapView?.isIndoorEnabled = $0 } + indoorEnabled.map { + mapView?.isIndoorEnabled = $0 + mapView?.indoorDisplay.delegate = $0 == true ? self : nil + } customMapStyle.map { mapView?.mapStyle = $0 } mapType.map { mapView?.mapType = $0 } userInterfaceStyle.map { mapView?.overrideUserInterfaceStyle = $0 } @@ -236,6 +240,7 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { var indoorEnabled: Bool? { didSet { mapView?.isIndoorEnabled = indoorEnabled ?? false + mapView?.indoorDisplay.delegate = indoorEnabled == true ? self : nil } } @@ -304,6 +309,8 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { var onMarkerDragStart: ((String?, RNLatLng) -> Void)? var onMarkerDrag: ((String?, RNLatLng) -> Void)? var onMarkerDragEnd: ((String?, RNLatLng) -> Void)? + var onIndoorBuildingFocused: ((RNIndoorBuilding) -> Void)? + var onIndoorLevelActivated: ((RNIndoorLevel) -> Void)? var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? var onCameraChange: ((RNRegion, RNCamera, Bool) -> Void)? var onCameraChangeComplete: ((RNRegion, RNCamera, Bool) -> Void)? @@ -766,4 +773,23 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { 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)) + } + + 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)) + } + } diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index a39c764..1cb8c16 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -319,6 +319,12 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { var onMarkerDragEnd: ((String?, RNLatLng) -> Void)? { didSet { impl.onMarkerDragEnd = onMarkerDragEnd } } + var onIndoorBuildingFocused: ((RNIndoorBuilding) -> Void)? { + didSet { impl.onIndoorBuildingFocused = onIndoorBuildingFocused } + } + var onIndoorLevelActivated: ((RNIndoorLevel) -> Void)? { + didSet { impl.onIndoorLevelActivated = onIndoorLevelActivated } + } var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? { didSet { impl.onCameraChangeStart = onCameraChangeStart } } diff --git a/ios/extensions/IndoorBuilding+Extension.swift b/ios/extensions/IndoorBuilding+Extension.swift new file mode 100644 index 0000000..87620b5 --- /dev/null +++ b/ios/extensions/IndoorBuilding+Extension.swift @@ -0,0 +1,33 @@ +import GoogleMaps +import NitroModules + +extension GMSIndoorLevel { + func toRNIndoorLevel(index: Int, active: Bool) -> RNIndoorLevel { + RNIndoorLevel( + index: Double(index), + name: self.name, + shortName: self.shortName, + active: active + ) + } +} + +extension GMSIndoorBuilding { + func toRNIndoorBuilding(from indoorDisplay: GMSIndoorDisplay) + -> RNIndoorBuilding { + let activeLevel = indoorDisplay.activeLevel + let levels = self.levels.enumerated().map { + (index, level) -> RNIndoorLevel in + let isActive = (level == activeLevel) + return level.toRNIndoorLevel(index: index, active: isActive) + } + let activeIndex = self.levels.firstIndex(where: { $0 == activeLevel }) + + return RNIndoorBuilding( + activeLevelIndex: activeIndex.map { Double($0) } ?? nil, + defaultLevelIndex: nil, + levels: levels, + underground: self.isUnderground + ) + } +} diff --git a/src/RNGoogleMapsPlusView.nitro.ts b/src/RNGoogleMapsPlusView.nitro.ts index f3b7dda..fbc41e4 100644 --- a/src/RNGoogleMapsPlusView.nitro.ts +++ b/src/RNGoogleMapsPlusView.nitro.ts @@ -24,6 +24,8 @@ import type { RNMapZoomConfig, RNHeatmap, RNKMLayer, + RNIndoorBuilding, + RNIndoorLevel, } from './types'; export interface RNGoogleMapsPlusViewProps extends HybridViewProps { @@ -57,6 +59,8 @@ export interface RNGoogleMapsPlusViewProps extends HybridViewProps { onMarkerDragStart?: (id: string | undefined, location: RNLatLng) => void; onMarkerDrag?: (id: string | undefined, location: RNLatLng) => void; onMarkerDragEnd?: (id: string | undefined, location: RNLatLng) => void; + onIndoorBuildingFocused?: (indoorBuilding: RNIndoorBuilding) => void; + onIndoorLevelActivated?: (indoorLevel: RNIndoorLevel) => void; onCameraChangeStart?: ( region: RNRegion, camera: RNCamera, diff --git a/src/types.ts b/src/types.ts index 7fdf3ec..ab856ad 100644 --- a/src/types.ts +++ b/src/types.ts @@ -210,6 +210,20 @@ export type RNKMLayer = { kmlString: string; }; +export type RNIndoorBuilding = { + activeLevelIndex?: number; + defaultLevelIndex?: number; + levels: RNIndoorLevel[]; + underground?: boolean; +}; + +export type RNIndoorLevel = { + index: number; + name?: string; + shortName?: string; + active?: boolean; +}; + export type RNLocationConfig = { android?: RNAndroidLocationConfig; ios?: RNIOSLocationConfig;