@react-native-kakao/map 2.2.6 → 3.1.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +5 -38
- package/RNCKakaoMap.podspec +16 -52
- package/android/build.gradle +12 -107
- package/android/consumer-rules.pro +2 -0
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/net/mjstudio/rnkakao/map/CameraOptions.kt +61 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapModule.kt +131 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapView.kt +1257 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapCameraCommands.kt +306 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapDimScreen.kt +431 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapGui.kt +899 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapImageLoader.kt +232 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapLabels.kt +1572 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapOptions.kt +360 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapRoutes.kt +805 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapShapes.kt +1096 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapUtilities.kt +495 -0
- package/expo-module.config.json +5 -0
- package/ios/CameraOptions.swift +41 -0
- package/ios/KakaoMapModule.swift +204 -0
- package/ios/KakaoMapView.swift +1018 -0
- package/ios/MapCameraCommands.swift +451 -0
- package/ios/MapDimScreen.swift +463 -0
- package/ios/MapGui.swift +1099 -0
- package/ios/MapImageLoader.swift +177 -0
- package/ios/MapLabels.swift +1676 -0
- package/ios/MapOptions.swift +374 -0
- package/ios/MapRoutes.swift +903 -0
- package/ios/MapShapes.swift +1294 -0
- package/ios/MapUnits.swift +20 -0
- package/ios/MapUtilities.swift +365 -0
- package/lib/commonjs/index.js +180 -29
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +18 -0
- package/lib/commonjs/index.web.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types-assets.js +2 -0
- package/lib/commonjs/types-assets.js.map +1 -0
- package/lib/commonjs/types-camera.js +6 -0
- package/lib/commonjs/types-camera.js.map +1 -0
- package/lib/commonjs/types-dim.js +6 -0
- package/lib/commonjs/types-dim.js.map +1 -0
- package/lib/commonjs/types-gui.js +6 -0
- package/lib/commonjs/types-gui.js.map +1 -0
- package/lib/commonjs/types-labels.js +6 -0
- package/lib/commonjs/types-labels.js.map +1 -0
- package/lib/commonjs/types-routes.js +6 -0
- package/lib/commonjs/types-routes.js.map +1 -0
- package/lib/commonjs/types-shapes.js +6 -0
- package/lib/commonjs/types-shapes.js.map +1 -0
- package/lib/commonjs/types-utilities.js +6 -0
- package/lib/commonjs/types-utilities.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/index.js +180 -22
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +13 -0
- package/lib/module/index.web.js.map +1 -0
- package/lib/module/types-assets.js +2 -0
- package/lib/module/types-assets.js.map +1 -0
- package/lib/module/types-camera.js +4 -0
- package/lib/module/types-camera.js.map +1 -0
- package/lib/module/types-dim.js +4 -0
- package/lib/module/types-dim.js.map +1 -0
- package/lib/module/types-gui.js +4 -0
- package/lib/module/types-gui.js.map +1 -0
- package/lib/module/types-labels.js +4 -0
- package/lib/module/types-labels.js.map +1 -0
- package/lib/module/types-routes.js +4 -0
- package/lib/module/types-routes.js.map +1 -0
- package/lib/module/types-shapes.js +4 -0
- package/lib/module/types-shapes.js.map +1 -0
- package/lib/module/types-utilities.js +4 -0
- package/lib/module/types-utilities.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/src/index.d.ts +59 -9
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types-assets.d.ts +9 -0
- package/lib/typescript/src/types-assets.d.ts.map +1 -0
- package/lib/typescript/src/types-camera.d.ts +123 -0
- package/lib/typescript/src/types-camera.d.ts.map +1 -0
- package/lib/typescript/src/types-dim.d.ts +54 -0
- package/lib/typescript/src/types-dim.d.ts.map +1 -0
- package/lib/typescript/src/types-gui.d.ts +189 -0
- package/lib/typescript/src/types-gui.d.ts.map +1 -0
- package/lib/typescript/src/types-labels.d.ts +246 -0
- package/lib/typescript/src/types-labels.d.ts.map +1 -0
- package/lib/typescript/src/types-routes.d.ts +162 -0
- package/lib/typescript/src/types-routes.d.ts.map +1 -0
- package/lib/typescript/src/types-shapes.d.ts +253 -0
- package/lib/typescript/src/types-shapes.d.ts.map +1 -0
- package/lib/typescript/src/types-utilities.d.ts +130 -0
- package/lib/typescript/src/types-utilities.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +222 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +24 -77
- package/src/index.tsx +322 -0
- package/src/index.web.tsx +46 -0
- package/src/types-assets.ts +8 -0
- package/src/types-camera.ts +97 -0
- package/src/types-dim.ts +54 -0
- package/src/types-gui.ts +171 -0
- package/src/types-labels.ts +207 -0
- package/src/types-routes.ts +118 -0
- package/src/types-shapes.ts +244 -0
- package/src/types-utilities.ts +115 -0
- package/src/types.ts +223 -0
- package/android/gradle.properties +0 -1
- package/android/src/main/AndroidManifestNew.xml +0 -2
- package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapModule.kt +0 -25
- package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapPackage.kt +0 -35
- package/android/src/newarch/KakaoMapSpec.kt +0 -7
- package/android/src/oldarch/KakaoMapSpec.kt +0 -14
- package/ios/Component/Map/RNCKakaoMapView.h +0 -23
- package/ios/Component/Map/RNCKakaoMapView.mm +0 -209
- package/ios/Component/Map/RNCKakaoMapViewManager.mm +0 -19
- package/ios/NativeModule/RNCKakaoMap.h +0 -7
- package/ios/NativeModule/RNCKakaoMap.mm +0 -19
- package/ios/NativeModule/RNCKakaoMapManager.swift +0 -7
- package/ios/Util/Util.h +0 -97
- package/lib/commonjs/component/KakaoMapView.js +0 -54
- package/lib/commonjs/component/KakaoMapView.js.map +0 -1
- package/lib/commonjs/spec/NativeKakaoMap.js +0 -9
- package/lib/commonjs/spec/NativeKakaoMap.js.map +0 -1
- package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js +0 -15
- package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
- package/lib/commonjs/type/type.js +0 -2
- package/lib/commonjs/type/type.js.map +0 -1
- package/lib/commonjs/util/Const.js +0 -14
- package/lib/commonjs/util/Const.js.map +0 -1
- package/lib/commonjs/util/Util.js +0 -15
- package/lib/commonjs/util/Util.js.map +0 -1
- package/lib/module/component/KakaoMapView.js +0 -48
- package/lib/module/component/KakaoMapView.js.map +0 -1
- package/lib/module/spec/NativeKakaoMap.js +0 -3
- package/lib/module/spec/NativeKakaoMap.js.map +0 -1
- package/lib/module/spec/RNCKakaoMapViewNativeComponent.js +0 -10
- package/lib/module/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
- package/lib/module/type/type.js +0 -2
- package/lib/module/type/type.js.map +0 -1
- package/lib/module/util/Const.js +0 -8
- package/lib/module/util/Const.js.map +0 -1
- package/lib/module/util/Util.js +0 -9
- package/lib/module/util/Util.js.map +0 -1
- package/lib/typescript/src/component/KakaoMapView.d.ts +0 -25
- package/lib/typescript/src/component/KakaoMapView.d.ts.map +0 -1
- package/lib/typescript/src/spec/NativeKakaoMap.d.ts +0 -7
- package/lib/typescript/src/spec/NativeKakaoMap.d.ts.map +0 -1
- package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts +0 -32
- package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts.map +0 -1
- package/lib/typescript/src/type/type.d.ts +0 -8
- package/lib/typescript/src/type/type.d.ts.map +0 -1
- package/lib/typescript/src/util/Const.d.ts +0 -8
- package/lib/typescript/src/util/Const.d.ts.map +0 -1
- package/lib/typescript/src/util/Util.d.ts +0 -3
- package/lib/typescript/src/util/Util.d.ts.map +0 -1
- package/src/component/KakaoMapView.tsx +0 -82
- package/src/index.ts +0 -43
- package/src/spec/NativeKakaoMap.ts +0 -8
- package/src/spec/RNCKakaoMapViewNativeComponent.ts +0 -38
- package/src/type/type.ts +0 -7
- package/src/util/Const.ts +0 -7
- package/src/util/Util.ts +0 -11
|
@@ -0,0 +1,1257 @@
|
|
|
1
|
+
package net.mjstudio.rnkakao.map
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
7
|
+
import com.facebook.react.bridge.ReactContext
|
|
8
|
+
import com.facebook.react.common.LifecycleState
|
|
9
|
+
import com.kakao.vectormap.GestureType
|
|
10
|
+
import com.kakao.vectormap.KakaoMap
|
|
11
|
+
import com.kakao.vectormap.KakaoMapReadyCallback
|
|
12
|
+
import com.kakao.vectormap.LatLng
|
|
13
|
+
import com.kakao.vectormap.MapAuthException
|
|
14
|
+
import com.kakao.vectormap.MapLifeCycleCallback
|
|
15
|
+
import com.kakao.vectormap.MapView
|
|
16
|
+
import com.kakao.vectormap.MapViewInfo
|
|
17
|
+
import com.kakao.vectormap.camera.CameraAnimation
|
|
18
|
+
import com.kakao.vectormap.camera.CameraPosition
|
|
19
|
+
import com.kakao.vectormap.camera.CameraUpdate
|
|
20
|
+
import com.kakao.vectormap.camera.CameraUpdateFactory
|
|
21
|
+
import expo.modules.kotlin.AppContext
|
|
22
|
+
import expo.modules.kotlin.Promise
|
|
23
|
+
import expo.modules.kotlin.exception.CodedException
|
|
24
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
25
|
+
import expo.modules.kotlin.views.ExpoView
|
|
26
|
+
import java.lang.ref.WeakReference
|
|
27
|
+
|
|
28
|
+
private const val CAMERA_REQUEST_TIMEOUT_MILLIS = 3_000L
|
|
29
|
+
private const val CAMERA_TIMEOUT = "ERR_KAKAO_MAP_CAMERA_TIMEOUT"
|
|
30
|
+
private const val CAMERA_SUPERSEDED = "ERR_KAKAO_MAP_CAMERA_SUPERSEDED"
|
|
31
|
+
private const val SHAPE_INVALID_ARGUMENT = "ERR_KAKAO_MAP_SHAPE_INVALID_ARGUMENT"
|
|
32
|
+
private const val ROUTE_INVALID_ARGUMENT = "ERR_KAKAO_MAP_ROUTE_INVALID_ARGUMENT"
|
|
33
|
+
private const val VIEW_DESTROYED = "ERR_KAKAO_MAP_VIEW_DESTROYED"
|
|
34
|
+
private const val VIEW_DESTROYED_MESSAGE = "The map view was destroyed."
|
|
35
|
+
|
|
36
|
+
class KakaoMapView(
|
|
37
|
+
context: Context,
|
|
38
|
+
appContext: AppContext,
|
|
39
|
+
) : ExpoView(context, appContext),
|
|
40
|
+
LifecycleEventListener {
|
|
41
|
+
override val shouldUseAndroidLayout = true
|
|
42
|
+
|
|
43
|
+
private val onMapReady by EventDispatcher()
|
|
44
|
+
private val onError by EventDispatcher()
|
|
45
|
+
private val onCameraMoveStart by EventDispatcher()
|
|
46
|
+
private val onCameraIdle by EventDispatcher()
|
|
47
|
+
private val onMapClick by EventDispatcher()
|
|
48
|
+
private val onMapLongClick by EventDispatcher()
|
|
49
|
+
private val onTerrainClick by EventDispatcher()
|
|
50
|
+
private val onViewportClick by EventDispatcher()
|
|
51
|
+
private val onVisibilityChanged by EventDispatcher()
|
|
52
|
+
private val onFocusChanged by EventDispatcher()
|
|
53
|
+
private val onPoiClick by EventDispatcher()
|
|
54
|
+
private val onCompassClick by EventDispatcher()
|
|
55
|
+
private val onMapTypeChanged by EventDispatcher()
|
|
56
|
+
private val onPaddingChanged by EventDispatcher()
|
|
57
|
+
private val onViewportChanged by EventDispatcher()
|
|
58
|
+
private val onLabelClick by EventDispatcher()
|
|
59
|
+
private val onLabelAnimationStopped by EventDispatcher()
|
|
60
|
+
private val onLabelsChanged by EventDispatcher()
|
|
61
|
+
private val onShapeAnimationStop by EventDispatcher()
|
|
62
|
+
private val onShapesChanged by EventDispatcher()
|
|
63
|
+
private val onRoutesChanged by EventDispatcher()
|
|
64
|
+
private val onRouteProgress by EventDispatcher()
|
|
65
|
+
private val onRouteAnimationStop by EventDispatcher()
|
|
66
|
+
private val onGuiEvent by EventDispatcher()
|
|
67
|
+
private val onDimScreenEvent by EventDispatcher()
|
|
68
|
+
private var labels: MapLabels? = null
|
|
69
|
+
private var shapes: MapShapes? = null
|
|
70
|
+
private var routes: MapRoutes? = null
|
|
71
|
+
private var gui: MapGui? = null
|
|
72
|
+
private var dimScreen: MapDimScreen? = null
|
|
73
|
+
private var utilities: MapUtilities? = null
|
|
74
|
+
private var cameraCommands: MapCameraCommands? = null
|
|
75
|
+
private var imageLoader: MapImageLoader? = null
|
|
76
|
+
private var pendingLabels: MapLabelSceneRecord? = null
|
|
77
|
+
private var pendingShapes: Map<String, Any?>? = null
|
|
78
|
+
private var pendingRoutes: Map<String, Any?>? = null
|
|
79
|
+
private var hasUsedRoutes = false
|
|
80
|
+
private var routeGeneration = 0L
|
|
81
|
+
private var labelGeneration = 0L
|
|
82
|
+
private var mapView: MapView? = null
|
|
83
|
+
private var map: KakaoMap? = null
|
|
84
|
+
private var isDestroyed = false
|
|
85
|
+
private var isStarted = false
|
|
86
|
+
private var hasInitialProps = false
|
|
87
|
+
private var initialCamera: CameraOptions? = null
|
|
88
|
+
private var lastCameraValues: List<Double>? = null
|
|
89
|
+
private var queuedOptions: MapOptionsRecord? = null
|
|
90
|
+
private val appliedOptions = MapOptionsRecord()
|
|
91
|
+
private val activeOverlays = mutableSetOf<String>()
|
|
92
|
+
private val activeCustomOverlays = mutableSetOf<String>()
|
|
93
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
94
|
+
private val pendingCameraRequests = mutableMapOf<Any, PendingCameraRequest>()
|
|
95
|
+
private val pendingCameraDispatches = mutableMapOf<Any, PendingCameraRequest>()
|
|
96
|
+
private var cameraCommandGeneration = 0L
|
|
97
|
+
private val reactContext = appContext.reactContext as? ReactContext
|
|
98
|
+
var pendingInitialCamera: CameraOptions? = null
|
|
99
|
+
var camera: CameraOptions? = null
|
|
100
|
+
var pendingOptions: MapOptionsRecord? = null
|
|
101
|
+
|
|
102
|
+
init {
|
|
103
|
+
reactContext?.addLifecycleEventListener(this)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fun updateProps() {
|
|
107
|
+
if (!hasInitialProps) {
|
|
108
|
+
hasInitialProps = true
|
|
109
|
+
initialCamera = pendingInitialCamera
|
|
110
|
+
}
|
|
111
|
+
if (map != null) {
|
|
112
|
+
val values = camera?.values()
|
|
113
|
+
if (values != lastCameraValues) {
|
|
114
|
+
camera?.let { applyCameraProp(it) }
|
|
115
|
+
lastCameraValues = values
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
pendingOptions?.let { options ->
|
|
119
|
+
if (options.mapType != null && options.viewInfo != null) {
|
|
120
|
+
reportError(CodedException(INVALID_ARGUMENT, "Map type and view info are mutually exclusive.", null))
|
|
121
|
+
} else if (map == null) {
|
|
122
|
+
queuedOptions = mergeMapOptions(queuedOptions, options)
|
|
123
|
+
} else {
|
|
124
|
+
applyOptionsSafely(options)
|
|
125
|
+
}
|
|
126
|
+
pendingOptions = null
|
|
127
|
+
}
|
|
128
|
+
applyPendingShapesIfReady()
|
|
129
|
+
applyPendingRoutesIfReady()
|
|
130
|
+
applyPendingLabelsIfReady()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fun queueLabels(scene: MapLabelSceneRecord?) {
|
|
134
|
+
if (scene == null && labels == null) {
|
|
135
|
+
pendingLabels = null
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
pendingLabels = scene ?: MapLabelSceneRecord()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fun queueShapes(scene: Map<String, Any?>?) {
|
|
142
|
+
if (scene == null && shapes == null) {
|
|
143
|
+
pendingShapes = null
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
pendingShapes = normalizeShapeObject(scene ?: emptyMap<String, Any?>(), "shapes")
|
|
148
|
+
} catch (error: Exception) {
|
|
149
|
+
pendingShapes = null
|
|
150
|
+
if (!isDestroyed) reportError(error)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fun queueRoutes(scene: Map<String, Any?>?) {
|
|
155
|
+
if (scene == null && !hasUsedRoutes) return
|
|
156
|
+
hasUsedRoutes = true
|
|
157
|
+
try {
|
|
158
|
+
pendingRoutes = normalizeShapeObject(scene ?: emptyMap<String, Any?>(), "routes", ROUTE_INVALID_ARGUMENT)
|
|
159
|
+
} catch (error: Exception) {
|
|
160
|
+
pendingRoutes = null
|
|
161
|
+
if (!isDestroyed) reportError(error)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
override fun onAttachedToWindow() {
|
|
166
|
+
super.onAttachedToWindow()
|
|
167
|
+
startIfNeeded()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private fun startIfNeeded() {
|
|
171
|
+
if (isDestroyed || !isAttachedToWindow || width == 0 || height == 0) return
|
|
172
|
+
if (isStarted) {
|
|
173
|
+
resume()
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
isStarted = true
|
|
177
|
+
if (KakaoMapModule.appKey == null) {
|
|
178
|
+
onError(mapOf("code" to NOT_INITIALIZED, "message" to "Call initializeKakaoMapSDK before mounting the map."))
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
val view = MapView(context)
|
|
182
|
+
mapView = view
|
|
183
|
+
addView(view, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
184
|
+
view.start(
|
|
185
|
+
object : MapLifeCycleCallback() {
|
|
186
|
+
override fun onMapDestroy() {
|
|
187
|
+
releaseMapObjects()
|
|
188
|
+
map = null
|
|
189
|
+
rejectPendingCameraRequests(NOT_READY, "The map was destroyed before returning its camera position.")
|
|
190
|
+
rejectPendingCameraDispatches(NOT_READY, "The map was destroyed before dispatching the camera update.")
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
override fun onMapError(error: Exception) {
|
|
194
|
+
if (isDestroyed) return
|
|
195
|
+
releaseMapObjects()
|
|
196
|
+
map = null
|
|
197
|
+
val code = if (error is MapAuthException) error.errorCode.toString() else error.javaClass.simpleName
|
|
198
|
+
val message = error.message ?: "Map error"
|
|
199
|
+
rejectPendingCameraRequests(code, message)
|
|
200
|
+
rejectPendingCameraDispatches(code, message)
|
|
201
|
+
onError(
|
|
202
|
+
mapOf(
|
|
203
|
+
"code" to code,
|
|
204
|
+
"message" to message,
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
object : KakaoMapReadyCallback() {
|
|
210
|
+
override fun onMapReady(readyMap: KakaoMap) {
|
|
211
|
+
if (isDestroyed) return
|
|
212
|
+
map = readyMap
|
|
213
|
+
readyMap.setOnCameraMoveStartListener { _, gesture ->
|
|
214
|
+
if (!isDestroyed) onCameraMoveStart(mapOf("reason" to reason(gesture)))
|
|
215
|
+
}
|
|
216
|
+
readyMap.setOnCameraMoveEndListener { _, position, gesture ->
|
|
217
|
+
if (!isDestroyed) onCameraIdle(mapOf("reason" to reason(gesture), "camera" to position.toPayload()))
|
|
218
|
+
}
|
|
219
|
+
readyMap.setOnMapClickListener { _, position, point, _ ->
|
|
220
|
+
if (!isDestroyed) {
|
|
221
|
+
onMapClick(
|
|
222
|
+
mapOf(
|
|
223
|
+
"latitude" to position.latitude,
|
|
224
|
+
"longitude" to position.longitude,
|
|
225
|
+
"x" to point.x / resources.displayMetrics.density,
|
|
226
|
+
"y" to point.y / resources.displayMetrics.density,
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
readyMap.setOnTerrainClickListener { _, position, _ ->
|
|
232
|
+
if (!isDestroyed) onTerrainClick(mapOf("latitude" to position.latitude, "longitude" to position.longitude))
|
|
233
|
+
}
|
|
234
|
+
readyMap.setOnViewportClickListener { _, position, point ->
|
|
235
|
+
if (!isDestroyed) {
|
|
236
|
+
val density = resources.displayMetrics.density
|
|
237
|
+
onViewportClick(
|
|
238
|
+
mapOf(
|
|
239
|
+
"latitude" to position.latitude,
|
|
240
|
+
"longitude" to position.longitude,
|
|
241
|
+
"x" to point.x / density,
|
|
242
|
+
"y" to point.y / density,
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
readyMap.setOnVisibleChangeListener { _, visible ->
|
|
248
|
+
if (!isDestroyed) onVisibilityChanged(mapOf("visible" to visible))
|
|
249
|
+
}
|
|
250
|
+
readyMap.setOnTerrainLongClickListener { _, position, _ ->
|
|
251
|
+
if (!isDestroyed) {
|
|
252
|
+
onMapLongClick(mapOf("latitude" to position.latitude, "longitude" to position.longitude))
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
readyMap.setOnPoiClickListener { _, position, layerId, id ->
|
|
256
|
+
if (!isDestroyed) {
|
|
257
|
+
onPoiClick(
|
|
258
|
+
mapOf(
|
|
259
|
+
"latitude" to position.latitude,
|
|
260
|
+
"longitude" to position.longitude,
|
|
261
|
+
"id" to id,
|
|
262
|
+
"layerId" to layerId,
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
readyMap.setOnCompassClickListener { _, _ ->
|
|
268
|
+
if (!isDestroyed) onCompassClick(emptyMap<String, Any>())
|
|
269
|
+
}
|
|
270
|
+
readyMap.setOnMapViewInfoChangeListener(
|
|
271
|
+
object : KakaoMap.OnMapViewInfoChangeListener {
|
|
272
|
+
override fun onMapViewInfoChanged(mapViewInfo: MapViewInfo) {
|
|
273
|
+
if (!isDestroyed) onMapTypeChanged(mapOf("viewInfoName" to mapViewInfo.mapType))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
override fun onMapViewInfoChangeFailed() {
|
|
277
|
+
if (!isDestroyed) {
|
|
278
|
+
onError(
|
|
279
|
+
mapOf(
|
|
280
|
+
"code" to "ERR_KAKAO_MAP_VIEW_INFO_CHANGE_FAILED",
|
|
281
|
+
"message" to "Kakao Maps could not change the map view information.",
|
|
282
|
+
),
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
readyMap.setOnPaddingChangeListener { changedMap ->
|
|
289
|
+
if (!isDestroyed) {
|
|
290
|
+
val density = resources.displayMetrics.density
|
|
291
|
+
val padding = changedMap.padding
|
|
292
|
+
onPaddingChanged(
|
|
293
|
+
mapOf(
|
|
294
|
+
"top" to padding.top / density,
|
|
295
|
+
"right" to padding.right / density,
|
|
296
|
+
"bottom" to padding.bottom / density,
|
|
297
|
+
"left" to padding.left / density,
|
|
298
|
+
),
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
readyMap.setOnViewportChangeListener { _, viewport ->
|
|
303
|
+
if (!isDestroyed) {
|
|
304
|
+
val density = resources.displayMetrics.density
|
|
305
|
+
onViewportChanged(
|
|
306
|
+
mapOf(
|
|
307
|
+
"width" to viewport.width() / density,
|
|
308
|
+
"height" to viewport.height() / density,
|
|
309
|
+
"x" to viewport.left / density,
|
|
310
|
+
"y" to viewport.top / density,
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
queuedOptions?.let(::applyOptionsSafely)
|
|
316
|
+
queuedOptions = null
|
|
317
|
+
(camera ?: initialCamera)?.let { applyCameraProp(it) }
|
|
318
|
+
lastCameraValues = camera?.values()
|
|
319
|
+
applyPendingShapesIfReady()
|
|
320
|
+
applyPendingRoutesIfReady()
|
|
321
|
+
applyPendingLabelsIfReady()
|
|
322
|
+
onMapReady(emptyMap<String, Any>())
|
|
323
|
+
if (!isAttachedToWindow || reactContext?.lifecycleState != LifecycleState.RESUMED) view.pause()
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
view.setFinishManually(true)
|
|
328
|
+
if (reactContext?.lifecycleState == LifecycleState.RESUMED && isAttachedToWindow) {
|
|
329
|
+
view.resume()
|
|
330
|
+
} else {
|
|
331
|
+
view.pause()
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
fun moveCamera(
|
|
336
|
+
camera: CameraOptions,
|
|
337
|
+
duration: Double,
|
|
338
|
+
) {
|
|
339
|
+
val map = requireMap()
|
|
340
|
+
val durationMillis = validateDuration(duration)
|
|
341
|
+
val update = CameraUpdateFactory.newCameraPosition(camera.position(map.cameraMinLevel, map.cameraMaxLevel))
|
|
342
|
+
beginCameraCommand()
|
|
343
|
+
dispatchCameraUpdate(map, update, durationMillis)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
fun updateCamera(
|
|
347
|
+
update: CameraUpdateRecord,
|
|
348
|
+
duration: Double,
|
|
349
|
+
promise: Promise,
|
|
350
|
+
) {
|
|
351
|
+
try {
|
|
352
|
+
val map = requireMap()
|
|
353
|
+
val durationMillis = validateDuration(duration)
|
|
354
|
+
if (update.type == "orientation") {
|
|
355
|
+
updateOrientation(map, update, durationMillis, promise)
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
val cameraUpdate =
|
|
359
|
+
when (update.type) {
|
|
360
|
+
"position" -> {
|
|
361
|
+
val position = update.position ?: throw invalidCameraUpdate("position")
|
|
362
|
+
position.validate("Camera position")
|
|
363
|
+
requireOnlyCameraFields(update, "position")
|
|
364
|
+
CameraUpdateFactory.newCenterPosition(LatLng.from(position.latitude, position.longitude))
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
"zoom" -> {
|
|
368
|
+
val zoomLevel = update.zoomLevel ?: throw invalidCameraUpdate("zoomLevel")
|
|
369
|
+
if (!zoomLevel.isFinite() || zoomLevel % 1.0 != 0.0 || zoomLevel < map.cameraMinLevel || zoomLevel > map.cameraMaxLevel) {
|
|
370
|
+
throw CodedException(INVALID_ARGUMENT, "Camera zoom level is outside the configured zoom range.", null)
|
|
371
|
+
}
|
|
372
|
+
requireOnlyCameraFields(update, "zoom")
|
|
373
|
+
CameraUpdateFactory.zoomTo(zoomLevel.toInt())
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
"fit" -> {
|
|
377
|
+
val coordinates = update.coordinates ?: throw invalidCameraUpdate("coordinates")
|
|
378
|
+
if (coordinates.isEmpty()) {
|
|
379
|
+
throw CodedException(INVALID_ARGUMENT, "Camera fit requires at least one coordinate.", null)
|
|
380
|
+
}
|
|
381
|
+
coordinates.forEachIndexed { index, coordinate -> coordinate.validate("Camera fit coordinate $index") }
|
|
382
|
+
requireOnlyCameraFields(update, "fit")
|
|
383
|
+
CameraUpdateFactory.fitMapPoints(
|
|
384
|
+
coordinates.map { LatLng.from(it.latitude, it.longitude) }.toTypedArray(),
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
else -> {
|
|
389
|
+
throw CodedException(INVALID_ARGUMENT, "Unsupported camera update type: ${update.type}.", null)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
beginCameraCommand()
|
|
393
|
+
dispatchCameraUpdate(map, cameraUpdate, durationMillis)
|
|
394
|
+
promise.resolve(null)
|
|
395
|
+
} catch (error: CodedException) {
|
|
396
|
+
promise.reject(error)
|
|
397
|
+
} catch (error: Exception) {
|
|
398
|
+
promise.reject(error.javaClass.simpleName, error.message ?: "Camera update failed.", error)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
fun getMapInfo(): Map<String, Any> {
|
|
403
|
+
val map = requireMap()
|
|
404
|
+
val density = resources.displayMetrics.density
|
|
405
|
+
val padding = map.padding
|
|
406
|
+
val viewport = map.viewport
|
|
407
|
+
val mapViewInfo =
|
|
408
|
+
map.mapViewInfo
|
|
409
|
+
?: throw CodedException("ERR_KAKAO_MAP_VIEW_INFO_UNAVAILABLE", "Kakao Maps did not return map view information.", null)
|
|
410
|
+
return mapOf(
|
|
411
|
+
"mapType" to mapTypePayload(mapViewInfo.mapType),
|
|
412
|
+
"viewInfoName" to mapViewInfo.mapType,
|
|
413
|
+
"zoomRange" to mapOf("min" to map.cameraMinLevel, "max" to map.cameraMaxLevel),
|
|
414
|
+
"padding" to
|
|
415
|
+
mapOf(
|
|
416
|
+
"top" to padding.top / density,
|
|
417
|
+
"right" to padding.right / density,
|
|
418
|
+
"bottom" to padding.bottom / density,
|
|
419
|
+
"left" to padding.left / density,
|
|
420
|
+
),
|
|
421
|
+
"size" to mapOf("width" to viewport.width() / density, "height" to viewport.height() / density),
|
|
422
|
+
"viewport" to
|
|
423
|
+
mapOf(
|
|
424
|
+
"x" to viewport.left / density,
|
|
425
|
+
"y" to viewport.top / density,
|
|
426
|
+
"width" to viewport.width() / density,
|
|
427
|
+
"height" to viewport.height() / density,
|
|
428
|
+
),
|
|
429
|
+
"language" to (map.poiLanguage ?: ""),
|
|
430
|
+
"supportedLanguages" to (map.supportedLanguages?.toList() ?: emptyList<String>()),
|
|
431
|
+
"capabilities" to mapOf("coordinateToScreen" to true),
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
fun screenToCoordinate(point: ScreenPointRecord): Map<String, Double> {
|
|
436
|
+
val map = requireMap()
|
|
437
|
+
point.validate()
|
|
438
|
+
validatePixelValue(point.x)
|
|
439
|
+
validatePixelValue(point.y)
|
|
440
|
+
val density = resources.displayMetrics.density
|
|
441
|
+
val x = (point.x * density).toInt()
|
|
442
|
+
val y = (point.y * density).toInt()
|
|
443
|
+
if (!map.viewport.contains(x, y)) {
|
|
444
|
+
throw CodedException(INVALID_ARGUMENT, "Screen point must be inside the map viewport.", null)
|
|
445
|
+
}
|
|
446
|
+
val coordinate =
|
|
447
|
+
map.fromScreenPoint(x, y)
|
|
448
|
+
?: throw CodedException("ERR_KAKAO_MAP_PROJECTION_FAILED", "The screen point could not be projected to a coordinate.", null)
|
|
449
|
+
return mapOf("latitude" to coordinate.latitude, "longitude" to coordinate.longitude)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
fun coordinateToScreen(coordinate: CoordinateRecord): Map<String, Double> {
|
|
453
|
+
val map = requireMap()
|
|
454
|
+
coordinate.validate()
|
|
455
|
+
val density = resources.displayMetrics.density
|
|
456
|
+
val point =
|
|
457
|
+
map.toScreenPoint(LatLng.from(coordinate.latitude, coordinate.longitude))
|
|
458
|
+
?: throw CodedException("ERR_KAKAO_MAP_PROJECTION_FAILED", "The coordinate could not be projected to a screen point.", null)
|
|
459
|
+
return mapOf("x" to point.x / density.toDouble(), "y" to point.y / density.toDouble())
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
fun clearCache() {
|
|
463
|
+
requireUtilities().command(mapOf("type" to "clearCache", "kind" to "all"))
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
internal fun requireUtilities(): MapUtilities {
|
|
467
|
+
val readyMap = requireMap()
|
|
468
|
+
return utilities ?: MapUtilities(readyMap, context).also { utilities = it }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
fun executeCameraCommand(
|
|
472
|
+
command: Map<String, Any?>,
|
|
473
|
+
promise: Promise,
|
|
474
|
+
) {
|
|
475
|
+
if (command["type"] == "getState") {
|
|
476
|
+
getCamera(promise, isAdvancedState = true)
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
val result = requireCameraCommands().command(command)
|
|
480
|
+
if (result["type"] == "dispatched") beginCameraCommand()
|
|
481
|
+
promise.resolve(result)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private fun requireCameraCommands(): MapCameraCommands {
|
|
485
|
+
val readyMap = requireMap()
|
|
486
|
+
return cameraCommands ?: MapCameraCommands(readyMap, resources.displayMetrics.density).also { cameraCommands = it }
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
fun runLabelCommand(command: MapLabelCommandRecord) {
|
|
490
|
+
requireLabels().command(command)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
fun getLabelCapabilities() = requireLabels().capabilities()
|
|
494
|
+
|
|
495
|
+
fun getLabelState() = requireLabels().state()
|
|
496
|
+
|
|
497
|
+
fun applyShapes(scene: Map<String, Any?>): Map<String, Any> {
|
|
498
|
+
pendingShapes = null
|
|
499
|
+
val counts = requireShapes().apply(normalizeShapeObject(scene, "scene"))
|
|
500
|
+
if (!isDestroyed) onShapesChanged(counts)
|
|
501
|
+
return counts
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
fun executeShapeCommand(command: Map<String, Any?>): Map<String, Any?> = requireShapes().command(normalizeShapeObject(command, "command"))
|
|
505
|
+
|
|
506
|
+
private fun requireShapes(): MapShapes {
|
|
507
|
+
val readyMap = requireMap()
|
|
508
|
+
shapes?.let { return it }
|
|
509
|
+
val viewReference = WeakReference(this)
|
|
510
|
+
lateinit var manager: MapShapes
|
|
511
|
+
manager =
|
|
512
|
+
MapShapes(readyMap, resources.displayMetrics.density) { event, payload ->
|
|
513
|
+
viewReference.get()?.let { view ->
|
|
514
|
+
if (!view.isDestroyed && view.shapes === manager && event == "onShapeAnimationStop") {
|
|
515
|
+
(payload["animationId"] as? String)?.let { animationId ->
|
|
516
|
+
view.onShapeAnimationStop(mapOf("animationId" to animationId))
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
shapes = manager
|
|
522
|
+
return manager
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private fun requireLabels(): MapLabels {
|
|
526
|
+
val readyMap = requireMap()
|
|
527
|
+
labels?.let { return it }
|
|
528
|
+
val loader = requireImageLoader()
|
|
529
|
+
val viewReference = WeakReference(this)
|
|
530
|
+
val manager =
|
|
531
|
+
MapLabels(
|
|
532
|
+
readyMap,
|
|
533
|
+
loader,
|
|
534
|
+
emitClick = { event ->
|
|
535
|
+
viewReference.get()?.let { view -> if (!view.isDestroyed) view.onLabelClick(event) }
|
|
536
|
+
},
|
|
537
|
+
emitAnimationStopped = { event ->
|
|
538
|
+
viewReference.get()?.let { view -> if (!view.isDestroyed) view.onLabelAnimationStopped(event) }
|
|
539
|
+
},
|
|
540
|
+
resolvePolygon = { id -> viewReference.get()?.shapes?.resolvePolygon(id) },
|
|
541
|
+
density = resources.displayMetrics.density,
|
|
542
|
+
)
|
|
543
|
+
labels = manager
|
|
544
|
+
readyMap.setOnLabelClickListener { _, _, label ->
|
|
545
|
+
viewReference.get()?.labels?.handleLabelClick(label)
|
|
546
|
+
true
|
|
547
|
+
}
|
|
548
|
+
readyMap.setOnLodLabelClickListener { _, _, label ->
|
|
549
|
+
viewReference.get()?.labels?.handleLodLabelClick(label)
|
|
550
|
+
true
|
|
551
|
+
}
|
|
552
|
+
return manager
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
fun applyRoutes(
|
|
556
|
+
scene: Map<String, Any?>,
|
|
557
|
+
promise: Promise,
|
|
558
|
+
) {
|
|
559
|
+
pendingRoutes = null
|
|
560
|
+
hasUsedRoutes = true
|
|
561
|
+
val generation = ++routeGeneration
|
|
562
|
+
val viewReference = WeakReference(this)
|
|
563
|
+
requireRoutes().apply(normalizeShapeObject(scene, "scene", ROUTE_INVALID_ARGUMENT)) { result ->
|
|
564
|
+
result.fold(
|
|
565
|
+
onSuccess = { counts ->
|
|
566
|
+
viewReference.get()?.let { view ->
|
|
567
|
+
if (!view.isDestroyed && generation == view.routeGeneration) view.onRoutesChanged(counts)
|
|
568
|
+
}
|
|
569
|
+
promise.resolve(counts)
|
|
570
|
+
},
|
|
571
|
+
onFailure = { error ->
|
|
572
|
+
if (error is CodedException) {
|
|
573
|
+
promise.reject(error)
|
|
574
|
+
} else {
|
|
575
|
+
promise.reject("ERR_KAKAO_MAP_ROUTE", error.message, error)
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
fun executeRouteCommand(command: Map<String, Any?>): Map<String, Any?> =
|
|
583
|
+
requireRoutes().command(normalizeShapeObject(command, "command", ROUTE_INVALID_ARGUMENT))
|
|
584
|
+
|
|
585
|
+
private fun requireRoutes(): MapRoutes {
|
|
586
|
+
val readyMap = requireMap()
|
|
587
|
+
routes?.let { return it }
|
|
588
|
+
val viewReference = WeakReference(this)
|
|
589
|
+
val manager =
|
|
590
|
+
MapRoutes(readyMap, resources.displayMetrics.density, requireImageLoader()) { event, payload ->
|
|
591
|
+
viewReference.get()?.let { view ->
|
|
592
|
+
if (!view.isDestroyed) {
|
|
593
|
+
when (event) {
|
|
594
|
+
"onRouteProgress" -> view.onRouteProgress(payload)
|
|
595
|
+
"onRouteAnimationStop" -> view.onRouteAnimationStop(payload)
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
routes = manager
|
|
601
|
+
return manager
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
internal fun requireGui(): MapGui {
|
|
605
|
+
val readyMap = requireMap()
|
|
606
|
+
gui?.let { return it }
|
|
607
|
+
val viewReference = WeakReference(this)
|
|
608
|
+
val manager =
|
|
609
|
+
MapGui(readyMap, resources.displayMetrics.density, requireImageLoader()) { payload ->
|
|
610
|
+
viewReference.get()?.let { view -> if (!view.isDestroyed) view.onGuiEvent(payload) }
|
|
611
|
+
}
|
|
612
|
+
gui = manager
|
|
613
|
+
return manager
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
internal fun requireDimScreen(): MapDimScreen {
|
|
617
|
+
val readyMap = requireMap()
|
|
618
|
+
dimScreen?.let { return it }
|
|
619
|
+
val viewReference = WeakReference(this)
|
|
620
|
+
val manager =
|
|
621
|
+
MapDimScreen(readyMap, resources.displayMetrics.density) { payload ->
|
|
622
|
+
viewReference.get()?.let { view -> if (!view.isDestroyed) view.onDimScreenEvent(payload) }
|
|
623
|
+
}
|
|
624
|
+
dimScreen = manager
|
|
625
|
+
return manager
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private fun requireImageLoader(): MapImageLoader = imageLoader ?: MapImageLoader(appContext).also { imageLoader = it }
|
|
629
|
+
|
|
630
|
+
private fun applyPendingRoutesIfReady() {
|
|
631
|
+
if (map == null) return
|
|
632
|
+
val scene = pendingRoutes ?: return
|
|
633
|
+
pendingRoutes = null
|
|
634
|
+
val generation = ++routeGeneration
|
|
635
|
+
val viewReference = WeakReference(this)
|
|
636
|
+
try {
|
|
637
|
+
requireRoutes().apply(scene) { result ->
|
|
638
|
+
viewReference.get()?.let { view ->
|
|
639
|
+
if (!view.isDestroyed && generation == view.routeGeneration) {
|
|
640
|
+
result.fold(
|
|
641
|
+
onSuccess = { view.onRoutesChanged(it) },
|
|
642
|
+
onFailure = { view.reportError(it as? Exception ?: Exception(it)) },
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
} catch (error: Exception) {
|
|
648
|
+
reportError(error)
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private fun applyPendingLabelsIfReady() {
|
|
653
|
+
if (map == null) return
|
|
654
|
+
val scene = pendingLabels ?: return
|
|
655
|
+
pendingLabels = null
|
|
656
|
+
val previousGeneration = labelGeneration
|
|
657
|
+
val generation = ++labelGeneration
|
|
658
|
+
try {
|
|
659
|
+
val viewReference = WeakReference(this)
|
|
660
|
+
requireLabels().apply(scene) { result ->
|
|
661
|
+
viewReference.get()?.let { view ->
|
|
662
|
+
if (!view.isDestroyed && generation == view.labelGeneration) {
|
|
663
|
+
result.fold(
|
|
664
|
+
onSuccess = { view.labels?.let { view.onLabelsChanged(it.state()) } },
|
|
665
|
+
onFailure = { error -> view.reportError(error as? Exception ?: Exception(error)) },
|
|
666
|
+
)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
} catch (error: Exception) {
|
|
671
|
+
if (labelGeneration == generation) labelGeneration = previousGeneration
|
|
672
|
+
reportError(error)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private fun applyPendingShapesIfReady() {
|
|
677
|
+
if (map == null) return
|
|
678
|
+
val scene = pendingShapes ?: return
|
|
679
|
+
pendingShapes = null
|
|
680
|
+
try {
|
|
681
|
+
val counts = requireShapes().apply(scene)
|
|
682
|
+
if (!isDestroyed) onShapesChanged(counts)
|
|
683
|
+
} catch (error: Exception) {
|
|
684
|
+
reportError(error)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private fun releaseMapObjects() {
|
|
689
|
+
labelGeneration++
|
|
690
|
+
routeGeneration++
|
|
691
|
+
utilities?.dispose()
|
|
692
|
+
utilities = null
|
|
693
|
+
cameraCommands?.dispose()
|
|
694
|
+
cameraCommands = null
|
|
695
|
+
val manager = labels
|
|
696
|
+
labels = null
|
|
697
|
+
try {
|
|
698
|
+
manager?.dispose()
|
|
699
|
+
} catch (error: Exception) {
|
|
700
|
+
if (!isDestroyed) reportError(error)
|
|
701
|
+
}
|
|
702
|
+
val guiManager = gui
|
|
703
|
+
gui = null
|
|
704
|
+
try {
|
|
705
|
+
guiManager?.dispose()
|
|
706
|
+
} catch (error: Exception) {
|
|
707
|
+
if (!isDestroyed) reportError(error)
|
|
708
|
+
}
|
|
709
|
+
val dimManager = dimScreen
|
|
710
|
+
dimScreen = null
|
|
711
|
+
try {
|
|
712
|
+
dimManager?.dispose()
|
|
713
|
+
} catch (error: Exception) {
|
|
714
|
+
if (!isDestroyed) reportError(error)
|
|
715
|
+
}
|
|
716
|
+
val routeManager = routes
|
|
717
|
+
routes = null
|
|
718
|
+
try {
|
|
719
|
+
routeManager?.dispose()
|
|
720
|
+
} catch (error: Exception) {
|
|
721
|
+
if (!isDestroyed) reportError(error)
|
|
722
|
+
}
|
|
723
|
+
val shapeManager = shapes
|
|
724
|
+
shapes = null
|
|
725
|
+
try {
|
|
726
|
+
shapeManager?.dispose()
|
|
727
|
+
} catch (error: Exception) {
|
|
728
|
+
if (!isDestroyed) reportError(error)
|
|
729
|
+
}
|
|
730
|
+
val loader = imageLoader
|
|
731
|
+
imageLoader = null
|
|
732
|
+
try {
|
|
733
|
+
loader?.dispose()
|
|
734
|
+
} catch (error: Exception) {
|
|
735
|
+
if (!isDestroyed) reportError(error)
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private fun applyOptionsSafely(options: MapOptionsRecord) {
|
|
740
|
+
try {
|
|
741
|
+
val map = requireMap()
|
|
742
|
+
validateOptions(map, options)
|
|
743
|
+
val changed = changedMapOptions(appliedOptions, options)
|
|
744
|
+
if (changed.isEmpty()) return
|
|
745
|
+
applyOptions(map, changed)
|
|
746
|
+
mergeMapOptions(appliedOptions, changed)
|
|
747
|
+
} catch (error: Exception) {
|
|
748
|
+
if (!isDestroyed) reportError(error)
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
private fun applyOptions(
|
|
753
|
+
map: KakaoMap,
|
|
754
|
+
options: MapOptionsRecord,
|
|
755
|
+
) {
|
|
756
|
+
options.viewInfo?.let { viewInfo ->
|
|
757
|
+
val nativeViewInfo =
|
|
758
|
+
options.mapType?.let { MapViewInfo.from(viewInfo.appName ?: "openmap", nativeMapType(it)) }
|
|
759
|
+
?: MapViewInfo.from(viewInfo.appName ?: "openmap", viewInfo.name)
|
|
760
|
+
map.changeMapViewInfo(nativeViewInfo)
|
|
761
|
+
} ?: options.mapType?.let { map.changeMapType(nativeMapType(it)) }
|
|
762
|
+
|
|
763
|
+
options.overlays?.let { overlays ->
|
|
764
|
+
(activeOverlays - overlays.toSet()).forEach { map.hideOverlay(nativeMapOverlay(it)) }
|
|
765
|
+
(overlays.toSet() - activeOverlays).forEach { map.showOverlay(nativeMapOverlay(it)) }
|
|
766
|
+
activeOverlays.clear()
|
|
767
|
+
activeOverlays += overlays
|
|
768
|
+
}
|
|
769
|
+
options.customOverlays?.let { overlays ->
|
|
770
|
+
(activeCustomOverlays - overlays.toSet()).forEach(map::hideOverlay)
|
|
771
|
+
(overlays.toSet() - activeCustomOverlays).forEach(map::showOverlay)
|
|
772
|
+
activeCustomOverlays.clear()
|
|
773
|
+
activeCustomOverlays += overlays
|
|
774
|
+
}
|
|
775
|
+
options.language?.let { language ->
|
|
776
|
+
if (!map.setPoiLanguage(language)) {
|
|
777
|
+
throw CodedException("ERR_KAKAO_MAP_LANGUAGE_CHANGE_FAILED", "Kakao Maps could not change the POI language to $language.", null)
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
options.poi?.let { poi ->
|
|
781
|
+
poi.visible?.let(map::setPoiVisible)
|
|
782
|
+
poi.clickable?.let(map::setPoiClickable)
|
|
783
|
+
poi.scale?.let { map.poiScale = nativePoiScale(it) }
|
|
784
|
+
}
|
|
785
|
+
options.buildingScale?.let { map.buildingHeightScale = it.toFloat() }
|
|
786
|
+
options.padding?.let { padding ->
|
|
787
|
+
map.setPadding(toPixels(padding.left), toPixels(padding.top), toPixels(padding.right), toPixels(padding.bottom))
|
|
788
|
+
}
|
|
789
|
+
options.zoomRange?.let { range ->
|
|
790
|
+
val min = range.min.toInt()
|
|
791
|
+
val max = range.max.toInt()
|
|
792
|
+
if (max < map.cameraMinLevel) {
|
|
793
|
+
map.cameraMinLevel = min
|
|
794
|
+
map.cameraMaxLevel = max
|
|
795
|
+
} else {
|
|
796
|
+
map.cameraMaxLevel = max
|
|
797
|
+
map.cameraMinLevel = min
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
options.gestures?.forEach { (gesture, enabled) -> map.setGestureEnable(nativeGesture(gesture), enabled) }
|
|
801
|
+
options.compass?.let { compassOptions ->
|
|
802
|
+
val compass =
|
|
803
|
+
map.compass
|
|
804
|
+
?: throw CodedException("ERR_KAKAO_MAP_WIDGET_UNAVAILABLE", "The compass is unavailable.", null)
|
|
805
|
+
compassOptions.visible?.let { if (it) compass.show() else compass.hide() }
|
|
806
|
+
compassOptions.position?.let { compass.setPosition(it.nativeGravity(), toPixelsFloat(it.x), toPixelsFloat(it.y)) }
|
|
807
|
+
}
|
|
808
|
+
options.scaleBar?.let { scaleBarOptions ->
|
|
809
|
+
val scaleBar =
|
|
810
|
+
map.scaleBar
|
|
811
|
+
?: throw CodedException("ERR_KAKAO_MAP_WIDGET_UNAVAILABLE", "The scale bar is unavailable.", null)
|
|
812
|
+
scaleBarOptions.visible?.let { if (it) scaleBar.show() else scaleBar.hide() }
|
|
813
|
+
scaleBarOptions.position?.let { scaleBar.setPosition(it.nativeGravity(), toPixelsFloat(it.x), toPixelsFloat(it.y)) }
|
|
814
|
+
scaleBarOptions.autoHide?.let { scaleBar.isAutoHide = it }
|
|
815
|
+
scaleBarOptions.fade?.let { fade ->
|
|
816
|
+
scaleBar.setFadeInOutTime(
|
|
817
|
+
fade.inDuration.toInt(),
|
|
818
|
+
fade.outDuration.toInt(),
|
|
819
|
+
fade.retentionDuration.toInt(),
|
|
820
|
+
)
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
options.logo?.let { position ->
|
|
824
|
+
val logo = map.logo ?: throw CodedException("ERR_KAKAO_MAP_WIDGET_UNAVAILABLE", "The Kakao logo is unavailable.", null)
|
|
825
|
+
logo.setPosition(position.nativeGravity(), toPixelsFloat(position.x), toPixelsFloat(position.y))
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
private fun validateOptions(
|
|
830
|
+
map: KakaoMap,
|
|
831
|
+
options: MapOptionsRecord,
|
|
832
|
+
) {
|
|
833
|
+
if (options.mapType != null && options.viewInfo != null) {
|
|
834
|
+
throw CodedException(INVALID_ARGUMENT, "Map type and view info are mutually exclusive.", null)
|
|
835
|
+
}
|
|
836
|
+
options.mapType?.let(::nativeMapType)
|
|
837
|
+
options.viewInfo?.validate()
|
|
838
|
+
options.overlays?.forEach(::nativeMapOverlay)
|
|
839
|
+
options.customOverlays?.let { overlays ->
|
|
840
|
+
if (overlays.any(String::isBlank)) {
|
|
841
|
+
throw CodedException(INVALID_ARGUMENT, "Custom overlay IDs must not be blank.", null)
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
options.language?.let { language ->
|
|
845
|
+
if (language.isBlank() || language !in (map.supportedLanguages?.toList() ?: emptyList())) {
|
|
846
|
+
throw CodedException(INVALID_ARGUMENT, "Unsupported POI language: $language.", null)
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
options.poi?.scale?.let(::nativePoiScale)
|
|
850
|
+
options.buildingScale?.let { scale ->
|
|
851
|
+
if (!scale.isFinite() || scale !in 0.0..1.0) {
|
|
852
|
+
throw CodedException(INVALID_ARGUMENT, "Building scale must be between 0 and 1.", null)
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
options.padding?.let { padding ->
|
|
856
|
+
padding.validate()
|
|
857
|
+
listOf(padding.top, padding.right, padding.bottom, padding.left).forEach(::validatePixelValue)
|
|
858
|
+
}
|
|
859
|
+
options.zoomRange?.let { range ->
|
|
860
|
+
if (
|
|
861
|
+
!range.min.isFinite() ||
|
|
862
|
+
!range.max.isFinite() ||
|
|
863
|
+
range.min % 1.0 != 0.0 ||
|
|
864
|
+
range.max % 1.0 != 0.0 ||
|
|
865
|
+
range.min > range.max ||
|
|
866
|
+
range.min < map.minZoomLevel ||
|
|
867
|
+
range.max > map.maxZoomLevel
|
|
868
|
+
) {
|
|
869
|
+
throw CodedException(INVALID_ARGUMENT, "Zoom range must contain integer levels within the SDK limits.", null)
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
options.gestures?.keys?.forEach(::nativeGesture)
|
|
873
|
+
options.compass?.position?.validate("Compass position")
|
|
874
|
+
options.scaleBar?.let { scaleBar ->
|
|
875
|
+
scaleBar.position?.validate("Scale bar position")
|
|
876
|
+
scaleBar.fade?.let { fade ->
|
|
877
|
+
validateDuration(fade.inDuration)
|
|
878
|
+
validateDuration(fade.outDuration)
|
|
879
|
+
validateDuration(fade.retentionDuration)
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
options.logo?.validate("Logo position")
|
|
883
|
+
listOfNotNull(options.compass?.position, options.scaleBar?.position, options.logo).forEach { position ->
|
|
884
|
+
validatePixelValue(position.x)
|
|
885
|
+
validatePixelValue(position.y)
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
private fun dispatchCameraUpdate(
|
|
890
|
+
map: KakaoMap,
|
|
891
|
+
update: CameraUpdate,
|
|
892
|
+
durationMillis: Int,
|
|
893
|
+
) {
|
|
894
|
+
if (durationMillis == 0) map.moveCamera(update) else map.moveCamera(update, CameraAnimation.from(durationMillis, false, false))
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
private fun updateOrientation(
|
|
898
|
+
map: KakaoMap,
|
|
899
|
+
update: CameraUpdateRecord,
|
|
900
|
+
durationMillis: Int,
|
|
901
|
+
promise: Promise,
|
|
902
|
+
) {
|
|
903
|
+
val rotation = update.rotation ?: throw invalidCameraUpdate("rotation")
|
|
904
|
+
val tilt = update.tilt ?: throw invalidCameraUpdate("tilt")
|
|
905
|
+
if (!rotation.isFinite() || !tilt.isFinite() || tilt !in 0.0..90.0) {
|
|
906
|
+
throw CodedException(INVALID_ARGUMENT, "Camera orientation must contain a finite rotation and tilt from 0 to 90 degrees.", null)
|
|
907
|
+
}
|
|
908
|
+
requireOnlyCameraFields(update, "orientation")
|
|
909
|
+
val commandGeneration = beginCameraCommand()
|
|
910
|
+
val requestId = Any()
|
|
911
|
+
val viewReference = WeakReference(this)
|
|
912
|
+
val timeout =
|
|
913
|
+
Runnable {
|
|
914
|
+
viewReference.get()?.rejectCameraDispatch(
|
|
915
|
+
requestId,
|
|
916
|
+
CAMERA_TIMEOUT,
|
|
917
|
+
"Kakao Maps did not return a settled camera position within $CAMERA_REQUEST_TIMEOUT_MILLIS milliseconds.",
|
|
918
|
+
)
|
|
919
|
+
}
|
|
920
|
+
pendingCameraDispatches[requestId] = PendingCameraRequest(promise, timeout)
|
|
921
|
+
mainHandler.postDelayed(timeout, CAMERA_REQUEST_TIMEOUT_MILLIS)
|
|
922
|
+
val callbackHandler = mainHandler
|
|
923
|
+
val mapReference = WeakReference(map)
|
|
924
|
+
try {
|
|
925
|
+
map.getCameraPosition { position ->
|
|
926
|
+
callbackHandler.post {
|
|
927
|
+
viewReference.get()?.resolveCameraDispatch(
|
|
928
|
+
requestId,
|
|
929
|
+
mapReference.get(),
|
|
930
|
+
position,
|
|
931
|
+
rotation,
|
|
932
|
+
tilt,
|
|
933
|
+
durationMillis,
|
|
934
|
+
commandGeneration,
|
|
935
|
+
)
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
} catch (error: Exception) {
|
|
939
|
+
val code = if (error is CodedException) error.code else error.javaClass.simpleName
|
|
940
|
+
rejectCameraDispatch(requestId, code, error.message ?: "Camera update failed.")
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
private fun resolveCameraDispatch(
|
|
945
|
+
requestId: Any,
|
|
946
|
+
expectedMap: KakaoMap?,
|
|
947
|
+
position: CameraPosition,
|
|
948
|
+
rotation: Double,
|
|
949
|
+
tilt: Double,
|
|
950
|
+
durationMillis: Int,
|
|
951
|
+
commandGeneration: Long,
|
|
952
|
+
) {
|
|
953
|
+
val request = pendingCameraDispatches.remove(requestId) ?: return
|
|
954
|
+
mainHandler.removeCallbacks(request.timeout)
|
|
955
|
+
if (isDestroyed || expectedMap == null || map !== expectedMap) {
|
|
956
|
+
request.promise.reject(VIEW_DESTROYED, VIEW_DESTROYED_MESSAGE, null)
|
|
957
|
+
return
|
|
958
|
+
}
|
|
959
|
+
if (commandGeneration != cameraCommandGeneration) {
|
|
960
|
+
request.promise.reject(
|
|
961
|
+
CAMERA_SUPERSEDED,
|
|
962
|
+
"The orientation update was superseded by a newer camera command before dispatch.",
|
|
963
|
+
null,
|
|
964
|
+
)
|
|
965
|
+
return
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
val cameraUpdate =
|
|
969
|
+
CameraUpdateFactory.newCameraPosition(
|
|
970
|
+
CameraPosition.from(
|
|
971
|
+
CameraPosition
|
|
972
|
+
.Builder()
|
|
973
|
+
.setPosition(position.position)
|
|
974
|
+
.setZoomLevel(position.zoomLevel)
|
|
975
|
+
.setTiltAngle(Math.toRadians(tilt))
|
|
976
|
+
.setRotationAngle(rotation.toNativeCameraRotation()),
|
|
977
|
+
),
|
|
978
|
+
)
|
|
979
|
+
dispatchCameraUpdate(expectedMap, cameraUpdate, durationMillis)
|
|
980
|
+
request.promise.resolve(null)
|
|
981
|
+
} catch (error: Exception) {
|
|
982
|
+
val code = if (error is CodedException) error.code else error.javaClass.simpleName
|
|
983
|
+
request.promise.reject(code, error.message ?: "Camera update failed.", error)
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
private fun rejectCameraDispatch(
|
|
988
|
+
requestId: Any,
|
|
989
|
+
code: String,
|
|
990
|
+
message: String,
|
|
991
|
+
) {
|
|
992
|
+
val request = pendingCameraDispatches.remove(requestId) ?: return
|
|
993
|
+
mainHandler.removeCallbacks(request.timeout)
|
|
994
|
+
request.promise.reject(code, message, null)
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
private fun rejectPendingCameraDispatches(
|
|
998
|
+
code: String,
|
|
999
|
+
message: String,
|
|
1000
|
+
) {
|
|
1001
|
+
pendingCameraDispatches.keys.toList().forEach { rejectCameraDispatch(it, code, message) }
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
private fun validateDuration(duration: Double): Int {
|
|
1005
|
+
if (!duration.isFinite() || duration < 0 || duration > Int.MAX_VALUE || duration % 1.0 != 0.0) {
|
|
1006
|
+
throw CodedException(INVALID_ARGUMENT, "Duration must be an integer between 0 and 2147483647 milliseconds.", null)
|
|
1007
|
+
}
|
|
1008
|
+
return duration.toInt()
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
private fun beginCameraCommand(): Long {
|
|
1012
|
+
cameraCommandGeneration += 1
|
|
1013
|
+
return cameraCommandGeneration
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
private fun validatePixelValue(value: Double) {
|
|
1017
|
+
val pixels = value * resources.displayMetrics.density
|
|
1018
|
+
if (!pixels.isFinite() || pixels < Int.MIN_VALUE || pixels > Int.MAX_VALUE) {
|
|
1019
|
+
throw CodedException(INVALID_ARGUMENT, "Screen point values exceed the supported pixel range.", null)
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
private fun toPixels(value: Double) = (value * resources.displayMetrics.density).toInt()
|
|
1024
|
+
|
|
1025
|
+
private fun toPixelsFloat(value: Double) = (value * resources.displayMetrics.density).toFloat()
|
|
1026
|
+
|
|
1027
|
+
private fun requireOnlyCameraFields(
|
|
1028
|
+
update: CameraUpdateRecord,
|
|
1029
|
+
type: String,
|
|
1030
|
+
) {
|
|
1031
|
+
val isValid =
|
|
1032
|
+
when (type) {
|
|
1033
|
+
"position" -> update.zoomLevel == null && update.rotation == null && update.tilt == null && update.coordinates == null
|
|
1034
|
+
"zoom" -> update.position == null && update.rotation == null && update.tilt == null && update.coordinates == null
|
|
1035
|
+
"orientation" -> update.position == null && update.zoomLevel == null && update.coordinates == null
|
|
1036
|
+
"fit" -> update.position == null && update.zoomLevel == null && update.rotation == null && update.tilt == null
|
|
1037
|
+
else -> false
|
|
1038
|
+
}
|
|
1039
|
+
if (!isValid) {
|
|
1040
|
+
throw CodedException(INVALID_ARGUMENT, "Camera update type $type contains fields for another update type.", null)
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
private fun invalidCameraUpdate(field: String) =
|
|
1045
|
+
CodedException(INVALID_ARGUMENT, "Camera update is missing required field: $field.", null)
|
|
1046
|
+
|
|
1047
|
+
private fun reportError(error: Exception) {
|
|
1048
|
+
val code = if (error is CodedException) error.code else error.javaClass.simpleName
|
|
1049
|
+
onError(mapOf("code" to code, "message" to (error.message ?: "Map error")))
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
fun getCamera(
|
|
1053
|
+
promise: Promise,
|
|
1054
|
+
isAdvancedState: Boolean = false,
|
|
1055
|
+
) {
|
|
1056
|
+
val readyMap =
|
|
1057
|
+
try {
|
|
1058
|
+
requireMap()
|
|
1059
|
+
} catch (error: CodedException) {
|
|
1060
|
+
promise.reject(error)
|
|
1061
|
+
return
|
|
1062
|
+
}
|
|
1063
|
+
val requestId = Any()
|
|
1064
|
+
val viewReference = WeakReference(this)
|
|
1065
|
+
val timeout =
|
|
1066
|
+
Runnable {
|
|
1067
|
+
viewReference.get()?.rejectCameraRequest(
|
|
1068
|
+
requestId,
|
|
1069
|
+
CAMERA_TIMEOUT,
|
|
1070
|
+
"Kakao Maps did not return a camera position within $CAMERA_REQUEST_TIMEOUT_MILLIS milliseconds.",
|
|
1071
|
+
)
|
|
1072
|
+
}
|
|
1073
|
+
pendingCameraRequests[requestId] = PendingCameraRequest(promise, timeout, isAdvancedState)
|
|
1074
|
+
mainHandler.postDelayed(timeout, CAMERA_REQUEST_TIMEOUT_MILLIS)
|
|
1075
|
+
val callbackHandler = mainHandler
|
|
1076
|
+
try {
|
|
1077
|
+
readyMap.getCameraPosition { position ->
|
|
1078
|
+
callbackHandler.post {
|
|
1079
|
+
viewReference.get()?.resolveCameraRequest(requestId, position)
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
} catch (error: Exception) {
|
|
1083
|
+
val code = if (error is CodedException) error.code else error.javaClass.simpleName
|
|
1084
|
+
rejectCameraRequest(requestId, code, error.message ?: "Camera query failed.")
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
private fun resolveCameraRequest(
|
|
1089
|
+
requestId: Any,
|
|
1090
|
+
position: CameraPosition,
|
|
1091
|
+
) {
|
|
1092
|
+
val request = pendingCameraRequests.remove(requestId) ?: return
|
|
1093
|
+
mainHandler.removeCallbacks(request.timeout)
|
|
1094
|
+
try {
|
|
1095
|
+
request.promise.resolve(if (request.isAdvancedState) requireCameraCommands().stateResult(position) else position.toPayload())
|
|
1096
|
+
} catch (error: Exception) {
|
|
1097
|
+
if (error is CodedException) {
|
|
1098
|
+
request.promise.reject(error)
|
|
1099
|
+
} else {
|
|
1100
|
+
request.promise.reject("ERR_KAKAO_MAP_CAMERA", error.message, error)
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
private fun rejectCameraRequest(
|
|
1106
|
+
requestId: Any,
|
|
1107
|
+
code: String,
|
|
1108
|
+
message: String,
|
|
1109
|
+
) {
|
|
1110
|
+
val request = pendingCameraRequests.remove(requestId) ?: return
|
|
1111
|
+
mainHandler.removeCallbacks(request.timeout)
|
|
1112
|
+
request.promise.reject(code, message, null)
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
private fun rejectPendingCameraRequests(
|
|
1116
|
+
code: String,
|
|
1117
|
+
message: String,
|
|
1118
|
+
) {
|
|
1119
|
+
pendingCameraRequests.keys.toList().forEach { rejectCameraRequest(it, code, message) }
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
private fun applyCameraProp(camera: CameraOptions) {
|
|
1123
|
+
try {
|
|
1124
|
+
moveCamera(camera, 0.0)
|
|
1125
|
+
} catch (error: CodedException) {
|
|
1126
|
+
onError(mapOf("code" to error.code, "message" to error.localizedMessage))
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
private fun requireMap(): KakaoMap {
|
|
1131
|
+
if (isDestroyed) throw CodedException(VIEW_DESTROYED, VIEW_DESTROYED_MESSAGE, null)
|
|
1132
|
+
if (KakaoMapModule.appKey == null) throw CodedException(NOT_INITIALIZED, "Initialize the map SDK first.", null)
|
|
1133
|
+
return map ?: throw CodedException(NOT_READY, "Wait for onMapReady before calling map methods.", null)
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
override fun onLayout(
|
|
1137
|
+
changed: Boolean,
|
|
1138
|
+
left: Int,
|
|
1139
|
+
top: Int,
|
|
1140
|
+
right: Int,
|
|
1141
|
+
bottom: Int,
|
|
1142
|
+
) {
|
|
1143
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
1144
|
+
startIfNeeded()
|
|
1145
|
+
mapView?.let { view ->
|
|
1146
|
+
// A map created during layout needs measurement before its surface can start.
|
|
1147
|
+
view.measure(
|
|
1148
|
+
MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY),
|
|
1149
|
+
MeasureSpec.makeMeasureSpec(bottom - top, MeasureSpec.EXACTLY),
|
|
1150
|
+
)
|
|
1151
|
+
view.layout(0, 0, right - left, bottom - top)
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
private fun resume() {
|
|
1156
|
+
if (!isDestroyed && isAttachedToWindow && reactContext?.lifecycleState == LifecycleState.RESUMED) mapView?.resume()
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
override fun onDetachedFromWindow() {
|
|
1160
|
+
mapView?.pause()
|
|
1161
|
+
super.onDetachedFromWindow()
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
override fun onHostResume() {
|
|
1165
|
+
resume()
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
override fun onHostPause() {
|
|
1169
|
+
mapView?.pause()
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
override fun onHostDestroy() {
|
|
1173
|
+
destroy()
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
fun destroy() {
|
|
1177
|
+
if (isDestroyed) return
|
|
1178
|
+
isDestroyed = true
|
|
1179
|
+
releaseMapObjects()
|
|
1180
|
+
beginCameraCommand()
|
|
1181
|
+
rejectPendingCameraRequests(VIEW_DESTROYED, VIEW_DESTROYED_MESSAGE)
|
|
1182
|
+
rejectPendingCameraDispatches(VIEW_DESTROYED, VIEW_DESTROYED_MESSAGE)
|
|
1183
|
+
reactContext?.removeLifecycleEventListener(this)
|
|
1184
|
+
map?.setOnCameraMoveStartListener(null)
|
|
1185
|
+
map?.setOnCameraMoveEndListener(null)
|
|
1186
|
+
map?.setOnMapClickListener(null)
|
|
1187
|
+
map?.setOnTerrainClickListener(null)
|
|
1188
|
+
map?.setOnViewportClickListener(null)
|
|
1189
|
+
map?.setOnVisibleChangeListener(null)
|
|
1190
|
+
map?.setOnTerrainLongClickListener(null)
|
|
1191
|
+
map?.setOnPoiClickListener(null)
|
|
1192
|
+
map?.setOnCompassClickListener(null)
|
|
1193
|
+
map?.setOnMapViewInfoChangeListener(null)
|
|
1194
|
+
map?.setOnPaddingChangeListener(null)
|
|
1195
|
+
map?.setOnViewportChangeListener(null)
|
|
1196
|
+
map?.setOnLabelClickListener(null)
|
|
1197
|
+
map?.setOnLodLabelClickListener(null)
|
|
1198
|
+
map = null
|
|
1199
|
+
queuedOptions = null
|
|
1200
|
+
pendingShapes = null
|
|
1201
|
+
pendingRoutes = null
|
|
1202
|
+
activeOverlays.clear()
|
|
1203
|
+
activeCustomOverlays.clear()
|
|
1204
|
+
val view = mapView
|
|
1205
|
+
mapView = null
|
|
1206
|
+
view?.finish()
|
|
1207
|
+
view?.let(::removeView)
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
private fun reason(gesture: GestureType) = if (gesture == GestureType.Unknown) "developer" else "gesture"
|
|
1211
|
+
|
|
1212
|
+
private data class PendingCameraRequest(
|
|
1213
|
+
val promise: Promise,
|
|
1214
|
+
val timeout: Runnable,
|
|
1215
|
+
val isAdvancedState: Boolean = false,
|
|
1216
|
+
)
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
private fun normalizeShapeObject(
|
|
1220
|
+
value: Map<*, *>,
|
|
1221
|
+
path: String,
|
|
1222
|
+
errorCode: String = SHAPE_INVALID_ARGUMENT,
|
|
1223
|
+
): Map<String, Any?> =
|
|
1224
|
+
value.entries.associate { (rawKey, rawValue) ->
|
|
1225
|
+
val key = rawKey as? String ?: throw CodedException(errorCode, "$path must use string object keys.", null)
|
|
1226
|
+
key to normalizeShapeValue(rawValue, "$path.$key", errorCode)
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
private fun normalizeShapeValue(
|
|
1230
|
+
value: Any?,
|
|
1231
|
+
path: String,
|
|
1232
|
+
errorCode: String,
|
|
1233
|
+
): Any? =
|
|
1234
|
+
when (value) {
|
|
1235
|
+
null, is String, is Boolean -> {
|
|
1236
|
+
value
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
is Number -> {
|
|
1240
|
+
if (!value.toDouble().isFinite()) {
|
|
1241
|
+
throw CodedException(errorCode, "$path must be a finite JSON number.", null)
|
|
1242
|
+
}
|
|
1243
|
+
value
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
is Map<*, *> -> {
|
|
1247
|
+
normalizeShapeObject(value, path, errorCode)
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
is List<*> -> {
|
|
1251
|
+
value.mapIndexed { index, item -> normalizeShapeValue(item, "$path[$index]", errorCode) }
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
else -> {
|
|
1255
|
+
throw CodedException(errorCode, "$path contains an unsupported JSON value.", null)
|
|
1256
|
+
}
|
|
1257
|
+
}
|