@react-native-kakao/map 2.2.7 → 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.
Files changed (165) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +5 -38
  3. package/RNCKakaoMap.podspec +16 -52
  4. package/android/build.gradle +12 -107
  5. package/android/consumer-rules.pro +2 -0
  6. package/android/src/main/AndroidManifest.xml +3 -1
  7. package/android/src/main/java/net/mjstudio/rnkakao/map/CameraOptions.kt +61 -0
  8. package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapModule.kt +131 -0
  9. package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapView.kt +1257 -0
  10. package/android/src/main/java/net/mjstudio/rnkakao/map/MapCameraCommands.kt +306 -0
  11. package/android/src/main/java/net/mjstudio/rnkakao/map/MapDimScreen.kt +431 -0
  12. package/android/src/main/java/net/mjstudio/rnkakao/map/MapGui.kt +899 -0
  13. package/android/src/main/java/net/mjstudio/rnkakao/map/MapImageLoader.kt +232 -0
  14. package/android/src/main/java/net/mjstudio/rnkakao/map/MapLabels.kt +1572 -0
  15. package/android/src/main/java/net/mjstudio/rnkakao/map/MapOptions.kt +360 -0
  16. package/android/src/main/java/net/mjstudio/rnkakao/map/MapRoutes.kt +805 -0
  17. package/android/src/main/java/net/mjstudio/rnkakao/map/MapShapes.kt +1096 -0
  18. package/android/src/main/java/net/mjstudio/rnkakao/map/MapUtilities.kt +495 -0
  19. package/expo-module.config.json +5 -0
  20. package/ios/CameraOptions.swift +41 -0
  21. package/ios/KakaoMapModule.swift +204 -0
  22. package/ios/KakaoMapView.swift +1018 -0
  23. package/ios/MapCameraCommands.swift +451 -0
  24. package/ios/MapDimScreen.swift +463 -0
  25. package/ios/MapGui.swift +1099 -0
  26. package/ios/MapImageLoader.swift +177 -0
  27. package/ios/MapLabels.swift +1676 -0
  28. package/ios/MapOptions.swift +374 -0
  29. package/ios/MapRoutes.swift +903 -0
  30. package/ios/MapShapes.swift +1294 -0
  31. package/ios/MapUnits.swift +20 -0
  32. package/ios/MapUtilities.swift +365 -0
  33. package/lib/commonjs/index.js +180 -29
  34. package/lib/commonjs/index.js.map +1 -1
  35. package/lib/commonjs/index.web.js +18 -0
  36. package/lib/commonjs/index.web.js.map +1 -0
  37. package/lib/commonjs/package.json +1 -0
  38. package/lib/commonjs/types-assets.js +2 -0
  39. package/lib/commonjs/types-assets.js.map +1 -0
  40. package/lib/commonjs/types-camera.js +6 -0
  41. package/lib/commonjs/types-camera.js.map +1 -0
  42. package/lib/commonjs/types-dim.js +6 -0
  43. package/lib/commonjs/types-dim.js.map +1 -0
  44. package/lib/commonjs/types-gui.js +6 -0
  45. package/lib/commonjs/types-gui.js.map +1 -0
  46. package/lib/commonjs/types-labels.js +6 -0
  47. package/lib/commonjs/types-labels.js.map +1 -0
  48. package/lib/commonjs/types-routes.js +6 -0
  49. package/lib/commonjs/types-routes.js.map +1 -0
  50. package/lib/commonjs/types-shapes.js +6 -0
  51. package/lib/commonjs/types-shapes.js.map +1 -0
  52. package/lib/commonjs/types-utilities.js +6 -0
  53. package/lib/commonjs/types-utilities.js.map +1 -0
  54. package/lib/commonjs/types.js +6 -0
  55. package/lib/commonjs/types.js.map +1 -0
  56. package/lib/module/index.js +180 -22
  57. package/lib/module/index.js.map +1 -1
  58. package/lib/module/index.web.js +13 -0
  59. package/lib/module/index.web.js.map +1 -0
  60. package/lib/module/types-assets.js +2 -0
  61. package/lib/module/types-assets.js.map +1 -0
  62. package/lib/module/types-camera.js +4 -0
  63. package/lib/module/types-camera.js.map +1 -0
  64. package/lib/module/types-dim.js +4 -0
  65. package/lib/module/types-dim.js.map +1 -0
  66. package/lib/module/types-gui.js +4 -0
  67. package/lib/module/types-gui.js.map +1 -0
  68. package/lib/module/types-labels.js +4 -0
  69. package/lib/module/types-labels.js.map +1 -0
  70. package/lib/module/types-routes.js +4 -0
  71. package/lib/module/types-routes.js.map +1 -0
  72. package/lib/module/types-shapes.js +4 -0
  73. package/lib/module/types-shapes.js.map +1 -0
  74. package/lib/module/types-utilities.js +4 -0
  75. package/lib/module/types-utilities.js.map +1 -0
  76. package/lib/module/types.js +4 -0
  77. package/lib/module/types.js.map +1 -0
  78. package/lib/typescript/src/index.d.ts +59 -9
  79. package/lib/typescript/src/index.d.ts.map +1 -1
  80. package/lib/typescript/src/types-assets.d.ts +9 -0
  81. package/lib/typescript/src/types-assets.d.ts.map +1 -0
  82. package/lib/typescript/src/types-camera.d.ts +123 -0
  83. package/lib/typescript/src/types-camera.d.ts.map +1 -0
  84. package/lib/typescript/src/types-dim.d.ts +54 -0
  85. package/lib/typescript/src/types-dim.d.ts.map +1 -0
  86. package/lib/typescript/src/types-gui.d.ts +189 -0
  87. package/lib/typescript/src/types-gui.d.ts.map +1 -0
  88. package/lib/typescript/src/types-labels.d.ts +246 -0
  89. package/lib/typescript/src/types-labels.d.ts.map +1 -0
  90. package/lib/typescript/src/types-routes.d.ts +162 -0
  91. package/lib/typescript/src/types-routes.d.ts.map +1 -0
  92. package/lib/typescript/src/types-shapes.d.ts +253 -0
  93. package/lib/typescript/src/types-shapes.d.ts.map +1 -0
  94. package/lib/typescript/src/types-utilities.d.ts +130 -0
  95. package/lib/typescript/src/types-utilities.d.ts.map +1 -0
  96. package/lib/typescript/src/types.d.ts +222 -0
  97. package/lib/typescript/src/types.d.ts.map +1 -0
  98. package/package.json +24 -77
  99. package/src/index.tsx +322 -0
  100. package/src/index.web.tsx +46 -0
  101. package/src/types-assets.ts +8 -0
  102. package/src/types-camera.ts +97 -0
  103. package/src/types-dim.ts +54 -0
  104. package/src/types-gui.ts +171 -0
  105. package/src/types-labels.ts +207 -0
  106. package/src/types-routes.ts +118 -0
  107. package/src/types-shapes.ts +244 -0
  108. package/src/types-utilities.ts +115 -0
  109. package/src/types.ts +223 -0
  110. package/android/gradle.properties +0 -1
  111. package/android/src/main/AndroidManifestNew.xml +0 -2
  112. package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapModule.kt +0 -25
  113. package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapPackage.kt +0 -35
  114. package/android/src/newarch/KakaoMapSpec.kt +0 -7
  115. package/android/src/oldarch/KakaoMapSpec.kt +0 -14
  116. package/ios/Component/Map/RNCKakaoMapView.h +0 -23
  117. package/ios/Component/Map/RNCKakaoMapView.mm +0 -209
  118. package/ios/Component/Map/RNCKakaoMapViewManager.mm +0 -19
  119. package/ios/NativeModule/RNCKakaoMap.h +0 -7
  120. package/ios/NativeModule/RNCKakaoMap.mm +0 -19
  121. package/ios/NativeModule/RNCKakaoMapManager.swift +0 -7
  122. package/ios/Util/Util.h +0 -97
  123. package/lib/commonjs/component/KakaoMapView.js +0 -54
  124. package/lib/commonjs/component/KakaoMapView.js.map +0 -1
  125. package/lib/commonjs/spec/NativeKakaoMap.js +0 -9
  126. package/lib/commonjs/spec/NativeKakaoMap.js.map +0 -1
  127. package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js +0 -15
  128. package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
  129. package/lib/commonjs/type/type.js +0 -2
  130. package/lib/commonjs/type/type.js.map +0 -1
  131. package/lib/commonjs/util/Const.js +0 -14
  132. package/lib/commonjs/util/Const.js.map +0 -1
  133. package/lib/commonjs/util/Util.js +0 -15
  134. package/lib/commonjs/util/Util.js.map +0 -1
  135. package/lib/module/component/KakaoMapView.js +0 -48
  136. package/lib/module/component/KakaoMapView.js.map +0 -1
  137. package/lib/module/spec/NativeKakaoMap.js +0 -3
  138. package/lib/module/spec/NativeKakaoMap.js.map +0 -1
  139. package/lib/module/spec/RNCKakaoMapViewNativeComponent.js +0 -10
  140. package/lib/module/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
  141. package/lib/module/type/type.js +0 -2
  142. package/lib/module/type/type.js.map +0 -1
  143. package/lib/module/util/Const.js +0 -8
  144. package/lib/module/util/Const.js.map +0 -1
  145. package/lib/module/util/Util.js +0 -9
  146. package/lib/module/util/Util.js.map +0 -1
  147. package/lib/typescript/src/component/KakaoMapView.d.ts +0 -25
  148. package/lib/typescript/src/component/KakaoMapView.d.ts.map +0 -1
  149. package/lib/typescript/src/spec/NativeKakaoMap.d.ts +0 -7
  150. package/lib/typescript/src/spec/NativeKakaoMap.d.ts.map +0 -1
  151. package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts +0 -32
  152. package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts.map +0 -1
  153. package/lib/typescript/src/type/type.d.ts +0 -8
  154. package/lib/typescript/src/type/type.d.ts.map +0 -1
  155. package/lib/typescript/src/util/Const.d.ts +0 -8
  156. package/lib/typescript/src/util/Const.d.ts.map +0 -1
  157. package/lib/typescript/src/util/Util.d.ts +0 -3
  158. package/lib/typescript/src/util/Util.d.ts.map +0 -1
  159. package/src/component/KakaoMapView.tsx +0 -82
  160. package/src/index.ts +0 -43
  161. package/src/spec/NativeKakaoMap.ts +0 -8
  162. package/src/spec/RNCKakaoMapViewNativeComponent.ts +0 -38
  163. package/src/type/type.ts +0 -7
  164. package/src/util/Const.ts +0 -7
  165. package/src/util/Util.ts +0 -11
@@ -0,0 +1,899 @@
1
+ package net.mjstudio.rnkakao.map
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Color
5
+ import android.graphics.PointF
6
+ import com.kakao.vectormap.KakaoMap
7
+ import com.kakao.vectormap.LatLng
8
+ import com.kakao.vectormap.MapGravity
9
+ import com.kakao.vectormap.mapwidget.InfoWindow
10
+ import com.kakao.vectormap.mapwidget.InfoWindowOptions
11
+ import com.kakao.vectormap.mapwidget.MapWidget
12
+ import com.kakao.vectormap.mapwidget.MapWidgetOptions
13
+ import com.kakao.vectormap.mapwidget.component.GuiImage
14
+ import com.kakao.vectormap.mapwidget.component.GuiLayout
15
+ import com.kakao.vectormap.mapwidget.component.GuiText
16
+ import com.kakao.vectormap.mapwidget.component.GuiView
17
+ import com.kakao.vectormap.mapwidget.component.Horizontal
18
+ import com.kakao.vectormap.mapwidget.component.Orientation
19
+ import com.kakao.vectormap.mapwidget.component.Vertical
20
+ import expo.modules.kotlin.exception.CodedException
21
+ import expo.modules.kotlin.records.Field
22
+ import expo.modules.kotlin.records.Record
23
+ import expo.modules.kotlin.records.Required
24
+ import java.lang.ref.WeakReference
25
+ import kotlin.math.roundToInt
26
+
27
+ private const val GUI_INVALID_ARGUMENT = "ERR_KAKAO_MAP_GUI_INVALID_ARGUMENT"
28
+ private const val GUI_NOT_FOUND = "ERR_KAKAO_MAP_GUI_NOT_FOUND"
29
+ private const val GUI_UNSUPPORTED = "ERR_KAKAO_MAP_UNSUPPORTED_PLATFORM_FEATURE"
30
+ private const val GUI_APPLY_CANCELLED = "ERR_KAKAO_MAP_GUI_APPLY_CANCELLED"
31
+ private const val GUI_NATIVE_ERROR = "ERR_KAKAO_MAP_GUI_NATIVE"
32
+
33
+ class MapGuiImageSourceRecord : Record {
34
+ @Field @Required
35
+ var uri: String = ""
36
+
37
+ @Field var width: Double? = null
38
+
39
+ @Field var height: Double? = null
40
+ }
41
+
42
+ class MapGuiSizeRecord : Record {
43
+ @Field @Required
44
+ var width: Double = Double.NaN
45
+
46
+ @Field @Required
47
+ var height: Double = Double.NaN
48
+ }
49
+
50
+ class MapGuiStrokeRecord : Record {
51
+ @Field @Required
52
+ var color: String = ""
53
+
54
+ @Field @Required
55
+ var width: Double = Double.NaN
56
+ }
57
+
58
+ class MapGuiSplitLineRecord : Record {
59
+ @Field @Required
60
+ var color: String = ""
61
+
62
+ @Field @Required
63
+ var width: Double = Double.NaN
64
+ }
65
+
66
+ class MapGuiIosLayoutRecord : Record {
67
+ @Field var backgroundColor: String? = null
68
+
69
+ @Field var splitLine: MapGuiSplitLineRecord? = null
70
+ }
71
+
72
+ class MapGuiAndroidLayoutRecord : Record {
73
+ @Field var background: MapGuiNodeRecord? = null
74
+ }
75
+
76
+ class MapGuiNodeRecord : Record {
77
+ @Field @Required
78
+ var id: String = ""
79
+
80
+ @Field @Required
81
+ var type: String = ""
82
+
83
+ @Field var platform: String? = null
84
+
85
+ @Field var clickable: Boolean = false
86
+
87
+ @Field var padding: MapPaddingRecord? = null
88
+
89
+ @Field var origin: String? = null
90
+
91
+ @Field var align: String? = null
92
+
93
+ @Field var orientation: String? = null
94
+
95
+ @Field var children: List<MapGuiNodeRecord> = emptyList()
96
+
97
+ @Field var child: MapGuiNodeRecord? = null
98
+
99
+ @Field var source: MapGuiImageSourceRecord? = null
100
+
101
+ @Field var pressedSource: MapGuiImageSourceRecord? = null
102
+
103
+ @Field var sources: List<MapGuiImageSourceRecord> = emptyList()
104
+
105
+ @Field var size: MapGuiSizeRecord? = null
106
+
107
+ @Field var pressedSize: MapGuiSizeRecord? = null
108
+
109
+ @Field var stretch: MapPaddingRecord? = null
110
+
111
+ @Field var text: String? = null
112
+
113
+ @Field var textSize: Double? = null
114
+
115
+ @Field var color: String? = null
116
+
117
+ @Field var stroke: MapGuiStrokeRecord? = null
118
+
119
+ @Field var duration: Double? = null
120
+
121
+ @Field var repeat: Double? = null
122
+
123
+ @Field var autoStart: Boolean = false
124
+
125
+ @Field var ios: MapGuiIosLayoutRecord? = null
126
+
127
+ @Field var android: MapGuiAndroidLayoutRecord? = null
128
+ }
129
+
130
+ class MapGuiAutoMoveRecord : Record {
131
+ @Field @Required
132
+ var enabled: Boolean = false
133
+
134
+ @Field var duration: Double? = null
135
+ }
136
+
137
+ class MapGuiInfoWindowRecord : Record {
138
+ @Field @Required
139
+ var id: String = ""
140
+
141
+ @Field @Required
142
+ var position: CoordinateRecord = CoordinateRecord()
143
+
144
+ @Field @Required
145
+ var body: MapGuiNodeRecord = MapGuiNodeRecord()
146
+
147
+ @Field var tail: MapGuiNodeRecord? = null
148
+
149
+ @Field var bodyOffset: ScreenPointRecord? = null
150
+
151
+ @Field var tailOffset: ScreenPointRecord? = null
152
+
153
+ @Field var visible: Boolean = true
154
+
155
+ @Field var zIndex: Int = 0
156
+
157
+ @Field var autoMove: MapGuiAutoMoveRecord? = null
158
+ }
159
+
160
+ class MapGuiWidgetRecord : Record {
161
+ @Field @Required
162
+ var id: String = ""
163
+
164
+ @Field @Required
165
+ var position: MapWidgetPositionRecord = MapWidgetPositionRecord()
166
+
167
+ @Field @Required
168
+ var root: MapGuiNodeRecord = MapGuiNodeRecord()
169
+
170
+ @Field var visible: Boolean = true
171
+
172
+ @Field var zIndex: Int = 0
173
+
174
+ @Field var rotation: Double = 0.0
175
+
176
+ @Field var ios: MapGuiIosLayoutRecord? = null
177
+ }
178
+
179
+ class MapGuiDropAnimationRecord : Record {
180
+ @Field @Required
181
+ var id: String = ""
182
+
183
+ @Field @Required
184
+ var platform: String = "ios"
185
+
186
+ @Field @Required
187
+ var infoWindowIds: List<String> = emptyList()
188
+
189
+ @Field @Required
190
+ var duration: Double = Double.NaN
191
+
192
+ @Field @Required
193
+ var pixelHeight: Double = Double.NaN
194
+
195
+ @Field var interpolation: String = "linear"
196
+
197
+ @Field var repeat: Double = 1.0
198
+
199
+ @Field var hideAtStop: Boolean = false
200
+
201
+ @Field var removeAtStop: Boolean = false
202
+ }
203
+
204
+ class MapGuiSceneRecord : Record {
205
+ @Field var infoWindows: List<MapGuiInfoWindowRecord> = emptyList()
206
+
207
+ @Field var widgets: List<MapGuiWidgetRecord> = emptyList()
208
+
209
+ @Field var animations: List<MapGuiDropAnimationRecord> = emptyList()
210
+ }
211
+
212
+ internal class MapGui(
213
+ private val map: KakaoMap,
214
+ private val density: Float,
215
+ private val imageLoader: MapImageLoader,
216
+ private val emit: (Map<String, Any>) -> Unit,
217
+ ) {
218
+ private val infoWindows = mutableMapOf<String, InfoWindow>()
219
+ private val widgets = mutableMapOf<String, MapWidget>()
220
+ private val imageTokens = mutableListOf<MapImageLoadToken>()
221
+ private var applyGeneration = 0
222
+ private var pendingApply: ((CodedException?) -> Unit)? = null
223
+ private var isDisposed = false
224
+
225
+ init {
226
+ val owner = WeakReference(this)
227
+ map.setOnInfoWindowClickListener { _, infoWindow, componentId ->
228
+ owner.get()?.takeUnless { it.isDisposed }?.run {
229
+ if (infoWindows[infoWindow.id] !== infoWindow) return@run
230
+ emit(
231
+ mapOf(
232
+ "type" to "tap",
233
+ "ownerType" to "infoWindow",
234
+ "ownerId" to infoWindow.id,
235
+ "componentId" to componentId,
236
+ ),
237
+ )
238
+ }
239
+ }
240
+ map.setOnMapWidgetClickListener { _, widget, componentId ->
241
+ owner.get()?.takeUnless { it.isDisposed }?.run {
242
+ if (widgets[widget.id] !== widget) return@run
243
+ emit(
244
+ mapOf(
245
+ "type" to "tap",
246
+ "ownerType" to "widget",
247
+ "ownerId" to widget.id,
248
+ "componentId" to componentId,
249
+ ),
250
+ )
251
+ }
252
+ }
253
+ }
254
+
255
+ fun apply(
256
+ scene: MapGuiSceneRecord,
257
+ completion: (CodedException?) -> Unit,
258
+ ) {
259
+ if (isDisposed) {
260
+ completion(CodedException(GUI_NOT_FOUND, "The GUI manager was disposed.", null))
261
+ return
262
+ }
263
+ try {
264
+ validate(scene)
265
+ } catch (error: CodedException) {
266
+ completion(error)
267
+ return
268
+ }
269
+ applyGeneration += 1
270
+ pendingApply?.invoke(CodedException(GUI_APPLY_CANCELLED, "A newer GUI scene replaced this apply request.", null))
271
+ pendingApply = completion
272
+ val generation = applyGeneration
273
+ imageTokens.forEach(MapImageLoadToken::cancel)
274
+ imageTokens.clear()
275
+ loadImages(scene, generation) { result ->
276
+ if (isDisposed || generation != applyGeneration) return@loadImages
277
+ result.fold(
278
+ onSuccess = { images ->
279
+ try {
280
+ replaceScene(scene, images)
281
+ pendingApply = null
282
+ completion(null)
283
+ } catch (error: CodedException) {
284
+ runCatching(::clearScene)
285
+ pendingApply = null
286
+ completion(error)
287
+ } catch (error: Throwable) {
288
+ runCatching(::clearScene)
289
+ pendingApply = null
290
+ completion(CodedException(GUI_NATIVE_ERROR, error.message ?: "The native GUI operation failed.", error))
291
+ }
292
+ },
293
+ onFailure = { error ->
294
+ pendingApply = null
295
+ completion(CodedException("ERR_KAKAO_MAP_IMAGE_LOAD", error.message ?: "Image load failed.", error))
296
+ },
297
+ )
298
+ }
299
+ }
300
+
301
+ fun showInfoWindow(
302
+ id: String,
303
+ autoMove: Boolean,
304
+ duration: Double?,
305
+ ) {
306
+ val infoWindow = requireInfoWindow(id)
307
+ val nativeDuration = duration?.let(::duration) ?: 0
308
+ when {
309
+ autoMove && nativeDuration > 0 -> infoWindow.show(true, nativeDuration)
310
+ autoMove -> infoWindow.show(true)
311
+ else -> infoWindow.show()
312
+ }
313
+ }
314
+
315
+ fun hideInfoWindow(id: String) = requireInfoWindow(id).hide()
316
+
317
+ fun moveInfoWindow(
318
+ id: String,
319
+ position: CoordinateRecord,
320
+ duration: Double,
321
+ ) {
322
+ position.validate()
323
+ requireInfoWindow(id).moveTo(position.latLng(), duration(duration).toLong())
324
+ }
325
+
326
+ fun showWidget(id: String) = requireWidget(id).show()
327
+
328
+ fun hideWidget(id: String) = requireWidget(id).hide()
329
+
330
+ fun positionWidget(
331
+ id: String,
332
+ position: MapWidgetPositionRecord,
333
+ ) {
334
+ position.validate("Widget position")
335
+ requireWidget(id).setPosition(position.nativeGravity(), position.x.toDpPixels(), position.y.toDpPixels())
336
+ }
337
+
338
+ fun rotateWidget(
339
+ id: String,
340
+ rotation: Double,
341
+ ) {
342
+ if (!rotation.isFinite()) invalid("Widget rotation must be finite.")
343
+ requireWidget(id).setRotation(Math.toRadians(rotation).toFloat())
344
+ }
345
+
346
+ fun startAnimation(id: String): Unit =
347
+ throw CodedException(
348
+ GUI_UNSUPPORTED,
349
+ "Info-window animator $id is unavailable in Android SDK 2.15.2.",
350
+ null,
351
+ )
352
+
353
+ fun stopAnimation(id: String): Unit = startAnimation(id)
354
+
355
+ fun controlAnimatedImage(
356
+ ownerType: String,
357
+ ownerId: String,
358
+ componentId: String,
359
+ action: String,
360
+ ): Unit =
361
+ throw CodedException(
362
+ GUI_UNSUPPORTED,
363
+ "Animated GUI image $componentId on $ownerType $ownerId cannot run $action on Android SDK 2.15.2.",
364
+ null,
365
+ )
366
+
367
+ fun status(): Map<String, Any> {
368
+ if (isDisposed) throw CodedException(GUI_NOT_FOUND, "The GUI manager was disposed.", null)
369
+ val manager = requireManager()
370
+ val infoWindowStatus =
371
+ manager.infoWindowLayer.allInfoWindows.orEmpty().sortedBy(InfoWindow::getId).map { infoWindow ->
372
+ mapOf(
373
+ "id" to infoWindow.id,
374
+ "visible" to infoWindow.isShow,
375
+ "position" to
376
+ mapOf(
377
+ "latitude" to infoWindow.position.latitude,
378
+ "longitude" to infoWindow.position.longitude,
379
+ ),
380
+ "zIndex" to infoWindow.zOrder,
381
+ )
382
+ }
383
+ val widgetStatus =
384
+ manager.mapWidgetLayer.allMapWidgets.orEmpty().sortedBy(MapWidget::getId).map { widget ->
385
+ mapOf(
386
+ "id" to widget.id,
387
+ "visible" to widget.isShow,
388
+ "position" to
389
+ mapOf(
390
+ "gravity" to gravityName(widget.mapGravity),
391
+ "x" to widget.position.x / density,
392
+ "y" to widget.position.y / density,
393
+ ),
394
+ "rotation" to Math.toDegrees(widget.rotation.toDouble()),
395
+ "zIndex" to widget.zOrder,
396
+ )
397
+ }
398
+ return mapOf(
399
+ "infoWindows" to infoWindowStatus,
400
+ "widgets" to widgetStatus,
401
+ "animations" to emptyList<Map<String, Any>>(),
402
+ "counts" to
403
+ mapOf(
404
+ "infoWindows" to infoWindowStatus.size,
405
+ "widgets" to widgetStatus.size,
406
+ "animations" to 0,
407
+ ),
408
+ "capabilities" to
409
+ mapOf(
410
+ "button" to false,
411
+ "animatedImage" to false,
412
+ "infoWindowAnimation" to false,
413
+ "arbitraryComponentClicks" to true,
414
+ "componentAnimationEvents" to false,
415
+ "guiMoveEvents" to false,
416
+ "autoMoveDuration" to true,
417
+ ),
418
+ )
419
+ }
420
+
421
+ fun dispose() {
422
+ if (isDisposed) return
423
+ isDisposed = true
424
+ applyGeneration += 1
425
+ pendingApply?.invoke(CodedException(GUI_APPLY_CANCELLED, "The GUI manager was disposed.", null))
426
+ pendingApply = null
427
+ imageTokens.forEach(MapImageLoadToken::cancel)
428
+ imageTokens.clear()
429
+ runCatching(::clearScene)
430
+ infoWindows.clear()
431
+ widgets.clear()
432
+ runCatching { map.setOnInfoWindowClickListener(null) }
433
+ runCatching { map.setOnMapWidgetClickListener(null) }
434
+ }
435
+
436
+ private fun replaceScene(
437
+ scene: MapGuiSceneRecord,
438
+ images: Map<String, Bitmap>,
439
+ ) {
440
+ clearScene()
441
+ val manager = requireManager()
442
+ scene.infoWindows.forEach { record ->
443
+ val options =
444
+ InfoWindowOptions
445
+ .from(record.id, record.position.latLng())
446
+ .setBody(component(record.body, images))
447
+ .setApplyDpScale(false)
448
+ .setVisible(false)
449
+ .setZOrder(record.zIndex)
450
+ record.tail?.let { options.setTail(image(it, images)) }
451
+ record.bodyOffset?.let {
452
+ options.setBodyOffset(it.x.dpPixelsFloat("Info-window body offset x"), it.y.dpPixelsFloat("Info-window body offset y"))
453
+ }
454
+ record.tailOffset?.let {
455
+ options.setTailOffset(it.x.dpPixelsFloat("Info-window tail offset x"), it.y.dpPixelsFloat("Info-window tail offset y"))
456
+ }
457
+ val infoWindow =
458
+ manager.infoWindowLayer.addInfoWindow(options)
459
+ ?: throw CodedException(GUI_NATIVE_ERROR, "Could not add info window ${record.id}.", null)
460
+ if (record.visible) {
461
+ val autoMove = record.autoMove
462
+ when {
463
+ autoMove?.enabled == true && autoMove.duration != null -> {
464
+ infoWindow.show(true, duration(autoMove.duration!!))
465
+ }
466
+
467
+ autoMove?.enabled == true -> {
468
+ infoWindow.show(true)
469
+ }
470
+
471
+ else -> {
472
+ infoWindow.show()
473
+ }
474
+ }
475
+ }
476
+ infoWindows[record.id] = infoWindow
477
+ }
478
+ scene.widgets.forEach { record ->
479
+ val options =
480
+ MapWidgetOptions
481
+ .from(
482
+ record.id,
483
+ record.position.nativeGravity(),
484
+ record.position.x.dpPixelsInt("Widget position x"),
485
+ record.position.y.dpPixelsInt("Widget position y"),
486
+ ).setRootView(component(record.root, images))
487
+ .setVisible(record.visible)
488
+ .setZOrder(record.zIndex)
489
+ .setRotation(Math.toRadians(record.rotation).toFloat())
490
+ val widget =
491
+ manager.mapWidgetLayer.addMapWidget(options)
492
+ ?: throw CodedException(GUI_NATIVE_ERROR, "Could not add widget ${record.id}.", null)
493
+ widgets[record.id] = widget
494
+ }
495
+ }
496
+
497
+ private fun clearScene() {
498
+ val manager = requireManager()
499
+ manager.infoWindowLayer.removeAll()
500
+ manager.mapWidgetLayer.removeAll()
501
+ infoWindows.clear()
502
+ widgets.clear()
503
+ }
504
+
505
+ private fun component(
506
+ record: MapGuiNodeRecord,
507
+ images: Map<String, Bitmap>,
508
+ ): GuiView {
509
+ val result =
510
+ when (record.type) {
511
+ "layout" -> {
512
+ GuiLayout(if (record.orientation == "horizontal") Orientation.Horizontal else Orientation.Vertical).apply {
513
+ record.children.forEach { addView(component(it, images)) }
514
+ record.android?.background?.let { setBackground(image(it, images)) }
515
+ }
516
+ }
517
+
518
+ "image" -> {
519
+ image(record, images)
520
+ }
521
+
522
+ "text" -> {
523
+ val text = record.text ?: invalid("GUI text is required.")
524
+ GuiText(text).apply {
525
+ setTextSize(positiveDpPixels(record.textSize ?: 0.0, "Text size"))
526
+ setTextColor(color(record.color ?: ""))
527
+ record.stroke?.let {
528
+ setStrokeColor(color(it.color))
529
+ setStrokeSize(nonnegativeDpPixels(it.width, "Text stroke width"))
530
+ }
531
+ }
532
+ }
533
+
534
+ "button", "animatedImage" -> {
535
+ throw CodedException(
536
+ GUI_UNSUPPORTED,
537
+ "${record.type} is unavailable in Android SDK 2.15.2.",
538
+ null,
539
+ )
540
+ }
541
+
542
+ else -> {
543
+ invalid("Unknown GUI component type: ${record.type}.")
544
+ }
545
+ }
546
+ result.id = record.id
547
+ result.isClickable = record.clickable
548
+ record.padding?.let {
549
+ result.setPadding(
550
+ nonnegativeDpPixels(it.left, "GUI padding left"),
551
+ nonnegativeDpPixels(it.top, "GUI padding top"),
552
+ nonnegativeDpPixels(it.right, "GUI padding right"),
553
+ nonnegativeDpPixels(it.bottom, "GUI padding bottom"),
554
+ )
555
+ }
556
+ val origin = alignment(record.origin ?: "topLeft")
557
+ result.setOrigin(origin.first, origin.second)
558
+ val align = alignment(record.align ?: "topLeft")
559
+ result.setAlign(align.first, align.second)
560
+ return result
561
+ }
562
+
563
+ private fun image(
564
+ record: MapGuiNodeRecord,
565
+ images: Map<String, Bitmap>,
566
+ ): GuiImage {
567
+ val source = record.source
568
+ val nativeImage = requiredImage(source, images)
569
+ val requestedWidth = record.size?.width ?: source?.width
570
+ val requestedHeight = record.size?.height ?: source?.height
571
+ val aspectRatio = nativeImage.width.toDouble() / nativeImage.height
572
+ val width = requestedWidth ?: requestedHeight?.times(aspectRatio)
573
+ val height = requestedHeight ?: requestedWidth?.div(aspectRatio)
574
+ val sizedImage =
575
+ if (width != null && height != null) {
576
+ Bitmap.createScaledBitmap(
577
+ nativeImage,
578
+ positiveDpPixels(width, "Image width"),
579
+ positiveDpPixels(height, "Image height"),
580
+ true,
581
+ )
582
+ } else {
583
+ nativeImage
584
+ }
585
+ val displayedImage =
586
+ (if (sizedImage === nativeImage) sizedImage.copy(Bitmap.Config.ARGB_8888, false) else sizedImage).also {
587
+ it.density = Bitmap.DENSITY_NONE
588
+ }
589
+ val guiImage = GuiImage(displayedImage)
590
+ record.stretch?.let {
591
+ guiImage.setFixedArea(
592
+ nonnegativeDpPixels(it.left, "Image stretch left"),
593
+ nonnegativeDpPixels(it.top, "Image stretch top"),
594
+ nonnegativeDpPixels(it.right, "Image stretch right"),
595
+ nonnegativeDpPixels(it.bottom, "Image stretch bottom"),
596
+ )
597
+ }
598
+ record.child?.let { guiImage.addChild(component(it, images)) }
599
+ return guiImage
600
+ }
601
+
602
+ private fun loadImages(
603
+ scene: MapGuiSceneRecord,
604
+ generation: Int,
605
+ completion: (Result<Map<String, Bitmap>>) -> Unit,
606
+ ) {
607
+ val sources = mutableMapOf<String, MapGuiImageSourceRecord>()
608
+ scene.infoWindows.forEach {
609
+ collectImages(it.body, sources)
610
+ it.tail?.let { tail -> collectImages(tail, sources) }
611
+ }
612
+ scene.widgets.forEach { collectImages(it.root, sources) }
613
+ if (sources.isEmpty()) {
614
+ completion(Result.success(emptyMap()))
615
+ return
616
+ }
617
+ val images = mutableMapOf<String, Bitmap>()
618
+ var remaining = sources.size
619
+ var failure: Throwable? = null
620
+ sources.values.forEach { source ->
621
+ imageTokens +=
622
+ imageLoader.load(source.uri) { result ->
623
+ if (isDisposed || generation != applyGeneration) return@load
624
+ result.onSuccess { images[source.uri] = it }
625
+ result.onFailure { if (failure == null) failure = it }
626
+ remaining -= 1
627
+ if (remaining == 0) {
628
+ imageTokens.clear()
629
+ completion(failure?.let { Result.failure(it) } ?: Result.success(images))
630
+ }
631
+ }
632
+ }
633
+ }
634
+
635
+ private fun collectImages(
636
+ record: MapGuiNodeRecord,
637
+ sources: MutableMap<String, MapGuiImageSourceRecord>,
638
+ ) {
639
+ listOfNotNull(record.source, record.pressedSource).forEach { sources[it.uri] = it }
640
+ record.sources.forEach { sources[it.uri] = it }
641
+ record.children.forEach { collectImages(it, sources) }
642
+ record.child?.let { collectImages(it, sources) }
643
+ record.android?.background?.let { collectImages(it, sources) }
644
+ }
645
+
646
+ private fun validate(scene: MapGuiSceneRecord) {
647
+ unique(scene.infoWindows.map { it.id }, "info-window")
648
+ unique(scene.widgets.map { it.id }, "widget")
649
+ unique(scene.animations.map { it.id }, "GUI animation")
650
+ if (scene.animations.isNotEmpty()) {
651
+ throw CodedException(GUI_UNSUPPORTED, "Info-window animators are unavailable in Android SDK 2.15.2.", null)
652
+ }
653
+ scene.infoWindows.forEach { record ->
654
+ nonempty(record.id, "Info-window ID")
655
+ record.position.validate()
656
+ record.bodyOffset?.let {
657
+ it.validate("Info-window body offset")
658
+ it.x.dpPixelsFloat("Info-window body offset x")
659
+ it.y.dpPixelsFloat("Info-window body offset y")
660
+ }
661
+ record.tailOffset?.let {
662
+ it.validate("Info-window tail offset")
663
+ it.x.dpPixelsFloat("Info-window tail offset x")
664
+ it.y.dpPixelsFloat("Info-window tail offset y")
665
+ }
666
+ val componentIds = mutableSetOf<String>()
667
+ validate(record.body, componentIds)
668
+ record.tail?.let {
669
+ if (it.type != "image") invalid("Info-window tails must be images.")
670
+ validate(it, componentIds)
671
+ }
672
+ record.autoMove?.duration?.let(::duration)
673
+ }
674
+ scene.widgets.forEach { record ->
675
+ nonempty(record.id, "Widget ID")
676
+ if (record.root.type != "layout") invalid("Widget roots must be layouts.")
677
+ record.position.validate("Widget position")
678
+ record.position.x.dpPixelsInt("Widget position x")
679
+ record.position.y.dpPixelsInt("Widget position y")
680
+ if (!record.rotation.isFinite()) invalid("Widget rotation must be finite.")
681
+ validate(record.root, mutableSetOf())
682
+ }
683
+ }
684
+
685
+ private fun validate(
686
+ record: MapGuiNodeRecord,
687
+ ids: MutableSet<String>,
688
+ ) {
689
+ nonempty(record.id, "GUI component ID")
690
+ if (!ids.add(record.id)) invalid("Duplicate GUI component ID: ${record.id}.")
691
+ alignment(record.origin ?: "topLeft")
692
+ alignment(record.align ?: "topLeft")
693
+ record.size?.validate("Image size")
694
+ record.pressedSize?.validate("Pressed image size")
695
+ record.stretch?.validateNonnegative("Image stretch")
696
+ record.padding?.validateNonnegative("GUI padding")
697
+ when (record.type) {
698
+ "layout" -> {
699
+ if (record.orientation !in setOf("horizontal", "vertical")) {
700
+ invalid("GUI layout orientation must be horizontal or vertical.")
701
+ }
702
+ record.children.forEach { validate(it, ids) }
703
+ record.android?.background?.let { validate(it, ids) }
704
+ }
705
+
706
+ "image" -> {
707
+ validateSource(record.source)
708
+ record.child?.let { validate(it, ids) }
709
+ }
710
+
711
+ "text" -> {
712
+ if (record.text == null) invalid("GUI text is required.")
713
+ positiveDpPixels(record.textSize ?: 0.0, "Text size")
714
+ color(record.color ?: "")
715
+ record.stroke?.let {
716
+ color(it.color)
717
+ nonnegativeDpPixels(it.width, "Text stroke width")
718
+ }
719
+ }
720
+
721
+ "button", "animatedImage" -> {
722
+ throw CodedException(GUI_UNSUPPORTED, "${record.type} is unavailable in Android SDK 2.15.2.", null)
723
+ }
724
+
725
+ else -> {
726
+ invalid("Unknown GUI component type: ${record.type}.")
727
+ }
728
+ }
729
+ }
730
+
731
+ private fun validateSource(source: MapGuiImageSourceRecord?) {
732
+ if (source == null || source.uri.isBlank()) invalid("GUI image URI must not be empty.")
733
+ source.width?.let { positiveDpPixels(it, "Image source width") }
734
+ source.height?.let { positiveDpPixels(it, "Image source height") }
735
+ }
736
+
737
+ private fun requiredImage(
738
+ source: MapGuiImageSourceRecord?,
739
+ images: Map<String, Bitmap>,
740
+ ): Bitmap {
741
+ validateSource(source)
742
+ return images[source!!.uri] ?: invalid("GUI image was not loaded.")
743
+ }
744
+
745
+ private fun requireInfoWindow(id: String): InfoWindow {
746
+ if (isDisposed) throw CodedException(GUI_NOT_FOUND, "The GUI manager was disposed.", null)
747
+ return infoWindows[id] ?: throw CodedException(GUI_NOT_FOUND, "Info window $id was not found.", null)
748
+ }
749
+
750
+ private fun requireWidget(id: String): MapWidget {
751
+ if (isDisposed) throw CodedException(GUI_NOT_FOUND, "The GUI manager was disposed.", null)
752
+ return widgets[id] ?: throw CodedException(GUI_NOT_FOUND, "Widget $id was not found.", null)
753
+ }
754
+
755
+ private fun requireManager() = map.mapWidgetManager ?: throw CodedException(GUI_NOT_FOUND, "The map widget manager is unavailable.", null)
756
+
757
+ private fun CoordinateRecord.latLng() = LatLng.from(latitude, longitude)
758
+
759
+ private fun Double.toDpPixels() = dpPixelsFloat("Screen position")
760
+
761
+ private fun Double.dpPixelsFloat(name: String): Float {
762
+ val pixels = this * density
763
+ if (!pixels.isFinite() || pixels > Float.MAX_VALUE || pixels < -Float.MAX_VALUE) {
764
+ invalid("$name must fit a 32-bit floating-point pixel value.")
765
+ }
766
+ return pixels.toFloat()
767
+ }
768
+
769
+ private fun Double.dpPixelsInt(name: String): Int {
770
+ val pixels = this * density
771
+ if (!pixels.isFinite() || pixels > Int.MAX_VALUE || pixels < Int.MIN_VALUE) {
772
+ invalid("$name is out of range after pixel conversion.")
773
+ }
774
+ return pixels.roundToInt()
775
+ }
776
+
777
+ private fun positiveDpPixels(
778
+ value: Double,
779
+ name: String,
780
+ ): Int {
781
+ positiveInt(value, name)
782
+ return pixelInt(value, name, isPositive = true)
783
+ }
784
+
785
+ private fun nonnegativeDpPixels(
786
+ value: Double,
787
+ name: String,
788
+ ): Int {
789
+ nonnegativeInt(value, name)
790
+ return pixelInt(value, name, isPositive = false)
791
+ }
792
+
793
+ private fun pixelInt(
794
+ value: Double,
795
+ name: String,
796
+ isPositive: Boolean,
797
+ ): Int {
798
+ val pixels = value * density
799
+ if (!pixels.isFinite() || pixels > Int.MAX_VALUE) invalid("$name is too large after pixel conversion.")
800
+ val result = pixels.roundToInt()
801
+ if (isPositive && result <= 0) invalid("$name is too small after pixel conversion.")
802
+ return result
803
+ }
804
+
805
+ private fun MapGuiSizeRecord.validate(name: String) {
806
+ positiveDpPixels(width, "$name width")
807
+ positiveDpPixels(height, "$name height")
808
+ }
809
+
810
+ private fun MapPaddingRecord.validateNonnegative(name: String) {
811
+ nonnegativeDpPixels(top, "$name top")
812
+ nonnegativeDpPixels(right, "$name right")
813
+ nonnegativeDpPixels(bottom, "$name bottom")
814
+ nonnegativeDpPixels(left, "$name left")
815
+ }
816
+
817
+ private fun alignment(value: String): Pair<Vertical, Horizontal> =
818
+ when (value) {
819
+ "topLeft" -> Vertical.Top to Horizontal.Left
820
+ "topCenter" -> Vertical.Top to Horizontal.Center
821
+ "topRight" -> Vertical.Top to Horizontal.Right
822
+ "centerLeft" -> Vertical.Center to Horizontal.Left
823
+ "center" -> Vertical.Center to Horizontal.Center
824
+ "centerRight" -> Vertical.Center to Horizontal.Right
825
+ "bottomLeft" -> Vertical.Bottom to Horizontal.Left
826
+ "bottomCenter" -> Vertical.Bottom to Horizontal.Center
827
+ "bottomRight" -> Vertical.Bottom to Horizontal.Right
828
+ else -> invalid("Unknown GUI alignment: $value.")
829
+ }
830
+
831
+ private fun gravityName(gravity: Int) =
832
+ when (gravity) {
833
+ MapGravity.TOP or MapGravity.LEFT -> "topLeft"
834
+ MapGravity.TOP or MapGravity.CENTER_HORIZONTAL -> "topCenter"
835
+ MapGravity.TOP or MapGravity.RIGHT -> "topRight"
836
+ MapGravity.CENTER_VERTICAL or MapGravity.LEFT -> "centerLeft"
837
+ MapGravity.CENTER -> "center"
838
+ MapGravity.CENTER_VERTICAL or MapGravity.RIGHT -> "centerRight"
839
+ MapGravity.BOTTOM or MapGravity.LEFT -> "bottomLeft"
840
+ MapGravity.BOTTOM or MapGravity.CENTER_HORIZONTAL -> "bottomCenter"
841
+ MapGravity.BOTTOM or MapGravity.RIGHT -> "bottomRight"
842
+ else -> "topLeft"
843
+ }
844
+
845
+ private fun color(value: String): Int {
846
+ if (!Regex("^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$").matches(value)) {
847
+ invalid("Colors must use #RRGGBB or #RRGGBBAA.")
848
+ }
849
+ return if (value.length == 7) {
850
+ Color.parseColor(value)
851
+ } else {
852
+ val rgba = value.drop(1).toLong(16)
853
+ Color.argb(
854
+ (rgba and 0xFF).toInt(),
855
+ ((rgba shr 24) and 0xFF).toInt(),
856
+ ((rgba shr 16) and 0xFF).toInt(),
857
+ ((rgba shr 8) and 0xFF).toInt(),
858
+ )
859
+ }
860
+ }
861
+
862
+ private fun duration(value: Double): Int = nonnegativeInt(value, "Duration")
863
+
864
+ private fun positiveInt(
865
+ value: Double,
866
+ name: String,
867
+ ): Int {
868
+ if (!value.isFinite() || value <= 0 || value > Int.MAX_VALUE || value % 1.0 != 0.0) {
869
+ invalid("$name must be a positive integer.")
870
+ }
871
+ return value.toInt()
872
+ }
873
+
874
+ private fun nonnegativeInt(
875
+ value: Double,
876
+ name: String,
877
+ ): Int {
878
+ if (!value.isFinite() || value < 0 || value > Int.MAX_VALUE || value % 1.0 != 0.0) {
879
+ invalid("$name must be a nonnegative integer.")
880
+ }
881
+ return value.toInt()
882
+ }
883
+
884
+ private fun unique(
885
+ values: List<String>,
886
+ name: String,
887
+ ) {
888
+ if (values.toSet().size != values.size) invalid("Duplicate $name ID.")
889
+ }
890
+
891
+ private fun nonempty(
892
+ value: String,
893
+ name: String,
894
+ ) {
895
+ if (value.isBlank()) invalid("$name must not be empty.")
896
+ }
897
+
898
+ private fun invalid(message: String): Nothing = throw CodedException(GUI_INVALID_ARGUMENT, message, null)
899
+ }