@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,431 @@
1
+ package net.mjstudio.rnkakao.map
2
+
3
+ import android.graphics.Color
4
+ import com.kakao.vectormap.LatLng
5
+ import com.kakao.vectormap.shape.DimScreenCover
6
+ import com.kakao.vectormap.shape.Polygon
7
+ import com.kakao.vectormap.shape.PolygonOptions
8
+ import com.kakao.vectormap.shape.ShapeAnimator
9
+ import expo.modules.kotlin.exception.CodedException
10
+
11
+ private const val DIM_INVALID_ARGUMENT = "ERR_KAKAO_MAP_DIM_INVALID_ARGUMENT"
12
+ private const val DIM_NOT_FOUND = "ERR_KAKAO_MAP_DIM_NOT_FOUND"
13
+ private const val DIM_SHAPE_UNSUPPORTED = "ERR_KAKAO_MAP_SHAPE_UNSUPPORTED"
14
+ private const val DIM_NATIVE_ERROR = "ERR_KAKAO_MAP_DIM_NATIVE"
15
+
16
+ internal class MapDimScreen(
17
+ private val map: com.kakao.vectormap.KakaoMap,
18
+ private val density: Float,
19
+ @Suppress("UNUSED_PARAMETER")
20
+ private val emit: (Map<String, Any>) -> Unit,
21
+ ) {
22
+ private val highlights = mutableMapOf<String, Polygon>()
23
+ private val animations = mutableMapOf<String, ShapeAnimator>()
24
+ private val styleFingerprints = mutableMapOf<String, String>()
25
+ private var isDisposed = false
26
+
27
+ fun apply(options: Map<String, Any?>) =
28
+ native {
29
+ if (isDisposed) throw CodedException(DIM_NOT_FOUND, "The map is no longer available.", null)
30
+ val parsed = parse(options)
31
+ val manager = requireManager()
32
+ val layer = manager.dimScreenLayer
33
+ val nativeStyles =
34
+ parsed.styleSets.associate { style ->
35
+ val id = string(style, "id")
36
+ val fingerprint = fingerprint(style)
37
+ val current = styleFingerprints[id]
38
+ if (current != null && current != fingerprint) {
39
+ invalid("Dim-screen style $id cannot be replaced during a map engine lifetime; use a new style ID.")
40
+ }
41
+ id to mapShapePolygonStylesSet(style, density)
42
+ }
43
+ val availableStyleIds = styleFingerprints.keys + nativeStyles.keys
44
+ val highlightIds = parsed.highlights.map { string(it, "id") }.toSet()
45
+ parsed.highlights.forEach { validateHighlight(it, availableStyleIds) }
46
+ parsed.animations.forEach { animation ->
47
+ mapShapeCircleWaves(animation, density)
48
+ val id = string(animation, "animationId")
49
+ val shapeIds = strings(animation, "shapeIds")
50
+ if (shapeIds.isEmpty()) invalid("Dim-screen animation $id requires a target.")
51
+ unique(shapeIds, "animation target")
52
+ shapeIds.forEach { shapeId ->
53
+ if (shapeId !in highlightIds) invalid("Dim-screen animation $id references unknown highlight $shapeId.")
54
+ }
55
+ }
56
+ clearObjects()
57
+ try {
58
+ nativeStyles.forEach { (id, style) ->
59
+ if (styleFingerprints[id] == null) {
60
+ manager.addPolygonStyles(style)
61
+ styleFingerprints[id] = fingerprint(parsed.styleSets.first { string(it, "id") == id })
62
+ }
63
+ }
64
+ parsed.highlights.forEach { addHighlight(it) }
65
+ parsed.animations.forEach { addAnimation(it) }
66
+ layer.setColor(color(parsed.color))
67
+ layer.setDimScreenCover(cover(parsed.cover))
68
+ parsed.zIndex?.let(layer::setZOrder)
69
+ layer.setVisible(parsed.enabled)
70
+ } catch (error: Throwable) {
71
+ runCatching(::clearObjects)
72
+ throw error
73
+ }
74
+ }
75
+
76
+ fun setHighlightVisible(
77
+ id: String,
78
+ visible: Boolean,
79
+ ) = native {
80
+ ensureAlive()
81
+ val highlight = highlights[id] ?: throw CodedException(DIM_NOT_FOUND, "Dim-screen highlight $id was not found.", null)
82
+ if (visible) highlight.show() else highlight.hide()
83
+ }
84
+
85
+ fun startAnimation(id: String) =
86
+ native {
87
+ ensureAlive()
88
+ requireAnimation(id).start()
89
+ }
90
+
91
+ fun stopAnimation(id: String) =
92
+ native {
93
+ ensureAlive()
94
+ requireAnimation(id).stop()
95
+ }
96
+
97
+ fun status(): Map<String, Any?> =
98
+ native {
99
+ ensureAlive()
100
+ val manager = requireManager()
101
+ val nativeHighlights =
102
+ manager.dimScreenLayer.allPolygons.orEmpty().sortedBy(Polygon::getId).map { polygon ->
103
+ mapOf(
104
+ "id" to polygon.id,
105
+ "visible" to polygon.isShow,
106
+ "geometryType" to if (polygon.mapPoints.orEmpty().isNotEmpty()) "geographicPolygon" else "modelPolygon",
107
+ )
108
+ }
109
+ val nativeAnimations =
110
+ animations.keys.sorted().mapNotNull { id ->
111
+ manager.getAnimator(id)?.let { mapOf("id" to it.id, "running" to null) }
112
+ }
113
+ mapOf(
114
+ "enabled" to null,
115
+ "color" to null,
116
+ "cover" to null,
117
+ "highlights" to nativeHighlights,
118
+ "animations" to nativeAnimations,
119
+ "counts" to
120
+ mapOf(
121
+ "highlights" to nativeHighlights.size,
122
+ "animations" to nativeAnimations.size,
123
+ ),
124
+ "capabilities" to
125
+ mapOf(
126
+ "coversRoute" to true,
127
+ "modelHighlights" to true,
128
+ "modelOrientation" to false,
129
+ "animations" to true,
130
+ "animationDuration" to true,
131
+ "animationState" to false,
132
+ "nativeConfigurationState" to false,
133
+ ),
134
+ )
135
+ }
136
+
137
+ fun dispose() {
138
+ if (isDisposed) return
139
+ isDisposed = true
140
+ runCatching(::clearObjects)
141
+ animations.clear()
142
+ highlights.clear()
143
+ runCatching { requireManager().dimScreenLayer.setVisible(false) }
144
+ }
145
+
146
+ private fun addHighlight(value: Map<String, Any?>) {
147
+ val id = string(value, "id")
148
+ val styleSetId = string(value, "styleSetId")
149
+ val styles =
150
+ requireManager().getPolygonStyles(styleSetId)
151
+ ?: invalid("Highlight $id references unknown style set $styleSetId.")
152
+ val geometry = dictionary(value, "geometry")
153
+ val type = string(geometry, "type")
154
+ val parts = dictionaries(geometry, "parts")
155
+ if (parts.isEmpty()) invalid("Highlight $id requires at least one polygon part.")
156
+ val option =
157
+ PolygonOptions
158
+ .from(id)
159
+ .setStylesSet(styles)
160
+ .setVisible(value["visible"] as? Boolean ?: true)
161
+ .setZOrder(integer(value, "zIndex", 0))
162
+ val polygon =
163
+ when (type) {
164
+ "geographicPolygon" -> {
165
+ option.setMapPoints(parts.map(::mapShapeMapPoints))
166
+ requireManager().dimScreenLayer.addPolygon(option)
167
+ }
168
+
169
+ "modelPolygon" -> {
170
+ val base = coordinate(dictionary(geometry, "base"))
171
+ option.setDotPoints(parts.map { mapShapeDotPoints(base, it) })
172
+ requireManager().dimScreenLayer.addPolygon(option)
173
+ }
174
+
175
+ else -> {
176
+ invalid("Dim-screen highlights require geographicPolygon or modelPolygon geometry.")
177
+ }
178
+ } ?: invalid("Could not add dim-screen highlight $id.")
179
+ highlights[id] = polygon
180
+ }
181
+
182
+ private fun addAnimation(value: Map<String, Any?>) {
183
+ val id = string(value, "animationId")
184
+ val animator =
185
+ requireManager().addAnimator(mapShapeCircleWaves(value, density))
186
+ ?: invalid("Could not add dim-screen animation $id.")
187
+ val shapeIds = strings(value, "shapeIds")
188
+ if (shapeIds.isEmpty()) invalid("Dim-screen animation $id requires a target.")
189
+ animator.addPolygons(
190
+ shapeIds.map { shapeId ->
191
+ highlights[shapeId] ?: invalid("Dim-screen animation $id references unknown highlight $shapeId.")
192
+ },
193
+ )
194
+ animations[id] = animator
195
+ }
196
+
197
+ private fun validateHighlight(
198
+ value: Map<String, Any?>,
199
+ availableStyleIds: Set<String>,
200
+ ) {
201
+ val id = string(value, "id")
202
+ val styleSetId = string(value, "styleSetId")
203
+ if (styleSetId !in availableStyleIds) invalid("Highlight $id references unknown style set $styleSetId.")
204
+ if (value.containsKey("visible") && value["visible"] !is Boolean) {
205
+ invalid("Highlight visible must be a boolean.")
206
+ }
207
+ integer(value, "zIndex", 0)
208
+ val geometry = dictionary(value, "geometry")
209
+ val type = string(geometry, "type")
210
+ val parts = dictionaries(geometry, "parts")
211
+ if (parts.isEmpty()) invalid("Highlight $id requires at least one polygon part.")
212
+ when (type) {
213
+ "geographicPolygon" -> {
214
+ parts.map(::mapShapeMapPoints)
215
+ }
216
+
217
+ "modelPolygon" -> {
218
+ val orientation =
219
+ if (geometry.containsKey("orientation")) {
220
+ (geometry["orientation"] as? Number)?.toDouble()
221
+ ?: invalid("Model-polygon orientation must be numeric degrees.")
222
+ } else {
223
+ 0.0
224
+ }
225
+ if (!orientation.isFinite()) invalid("Model-polygon orientation must be finite degrees.")
226
+ if (orientation != 0.0) {
227
+ throw CodedException(
228
+ DIM_SHAPE_UNSUPPORTED,
229
+ "Android SDK 2.15.2 does not support model-polygon orientation.",
230
+ null,
231
+ )
232
+ }
233
+ val base = coordinate(dictionary(geometry, "base"))
234
+ parts.map { mapShapeDotPoints(base, it) }
235
+ }
236
+
237
+ else -> {
238
+ invalid("Dim-screen highlights require geographicPolygon or modelPolygon geometry.")
239
+ }
240
+ }
241
+ }
242
+
243
+ private fun clearObjects() {
244
+ val manager = requireManager()
245
+ animations.values.forEach { it.stop() }
246
+ manager.removeAllAnimator()
247
+ manager.dimScreenLayer.clearAll()
248
+ animations.clear()
249
+ highlights.clear()
250
+ }
251
+
252
+ private fun requireAnimation(id: String) =
253
+ animations[id] ?: throw CodedException(DIM_NOT_FOUND, "Dim-screen animation $id was not found.", null)
254
+
255
+ private fun ensureAlive() {
256
+ if (isDisposed) throw CodedException(DIM_NOT_FOUND, "The dim-screen manager was disposed.", null)
257
+ }
258
+
259
+ private fun requireManager() = map.dimScreenManager ?: throw CodedException(DIM_NOT_FOUND, "The dim-screen manager is unavailable.", null)
260
+
261
+ private data class ParsedOptions(
262
+ val enabled: Boolean,
263
+ val color: String,
264
+ val cover: String,
265
+ val zIndex: Int?,
266
+ val styleSets: List<Map<String, Any?>>,
267
+ val highlights: List<Map<String, Any?>>,
268
+ val animations: List<Map<String, Any?>>,
269
+ )
270
+
271
+ private fun parse(value: Map<String, Any?>): ParsedOptions {
272
+ val enabled = value["enabled"] as? Boolean ?: invalid("Dim-screen enabled is required.")
273
+ val color = string(value, "color")
274
+ color(color)
275
+ val cover = string(value, "cover")
276
+ cover(cover)
277
+ val styleSets = optionalObjects(value, "styleSets")
278
+ val highlights = optionalObjects(value, "highlights")
279
+ val animations = optionalObjects(value, "animations")
280
+ unique(styleSets.map { string(it, "id") }, "dim-screen style")
281
+ unique(highlights.map { string(it, "id") }, "dim-screen highlight")
282
+ unique(animations.map { string(it, "animationId") }, "dim-screen animation")
283
+ return ParsedOptions(
284
+ enabled,
285
+ color,
286
+ cover,
287
+ if (value.containsKey("zIndex")) integer(value, "zIndex", 0) else null,
288
+ styleSets,
289
+ highlights,
290
+ animations,
291
+ )
292
+ }
293
+
294
+ private fun cover(value: String) =
295
+ when (value) {
296
+ "map" -> DimScreenCover.Map
297
+ "mapAndRoute" -> DimScreenCover.MapAndRoute
298
+ "mapAndLabel" -> DimScreenCover.MapAndLabel
299
+ "all" -> DimScreenCover.All
300
+ else -> invalid("Unknown dim-screen cover: $value.")
301
+ }
302
+
303
+ private fun coordinate(value: Map<String, Any?>): LatLng {
304
+ val latitude = (value["latitude"] as? Number)?.toDouble() ?: invalid("Coordinate latitude is required.")
305
+ val longitude = (value["longitude"] as? Number)?.toDouble() ?: invalid("Coordinate longitude is required.")
306
+ if (!latitude.isFinite() || !longitude.isFinite() || latitude !in -90.0..90.0 || longitude !in -180.0..180.0) {
307
+ invalid("Invalid dim-screen base coordinate.")
308
+ }
309
+ return LatLng.from(latitude, longitude)
310
+ }
311
+
312
+ private fun color(value: String): Int {
313
+ if (!Regex("^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$").matches(value)) {
314
+ invalid("Colors must use #RRGGBB or #RRGGBBAA.")
315
+ }
316
+ if (value.length == 7) return Color.parseColor(value)
317
+ val rgba = value.drop(1).toLong(16)
318
+ return Color.argb(
319
+ (rgba and 0xFF).toInt(),
320
+ ((rgba shr 24) and 0xFF).toInt(),
321
+ ((rgba shr 16) and 0xFF).toInt(),
322
+ ((rgba shr 8) and 0xFF).toInt(),
323
+ )
324
+ }
325
+
326
+ private fun fingerprint(value: Map<String, Any?>): String = canonical(value)
327
+
328
+ private fun canonical(value: Any?): String =
329
+ when (value) {
330
+ null -> {
331
+ "n"
332
+ }
333
+
334
+ is String -> {
335
+ "s${value.length}:$value"
336
+ }
337
+
338
+ is Boolean -> {
339
+ if (value) "b1" else "b0"
340
+ }
341
+
342
+ is Number -> {
343
+ "d${value.toDouble()}"
344
+ }
345
+
346
+ is List<*> -> {
347
+ value.joinToString(prefix = "[", postfix = "]", separator = "") { canonical(it) }
348
+ }
349
+
350
+ is Map<*, *> -> {
351
+ value.entries
352
+ .map { (key, item) -> (key as? String ?: invalid("Object keys must be strings.")) to item }
353
+ .sortedBy { it.first }
354
+ .joinToString(prefix = "{", postfix = "}", separator = "") { (key, item) -> canonical(key) + canonical(item) }
355
+ }
356
+
357
+ else -> {
358
+ invalid("Style values must be serializable.")
359
+ }
360
+ }
361
+
362
+ private fun string(
363
+ value: Map<String, Any?>,
364
+ key: String,
365
+ ): String = (value[key] as? String)?.takeIf(String::isNotBlank) ?: invalid("$key must be a nonempty string.")
366
+
367
+ private fun strings(
368
+ value: Map<String, Any?>,
369
+ key: String,
370
+ ): List<String> =
371
+ (value[key] as? List<*>)?.map { it as? String ?: invalid("$key must contain strings.") }
372
+ ?: invalid("$key must be a string array.")
373
+
374
+ private fun integer(
375
+ value: Map<String, Any?>,
376
+ key: String,
377
+ fallback: Int,
378
+ ): Int {
379
+ if (!value.containsKey(key)) return fallback
380
+ val raw = value[key] ?: invalid("$key must be an integer.")
381
+ val number = (raw as? Number)?.toDouble() ?: invalid("$key must be an integer.")
382
+ if (!number.isFinite() || number % 1.0 != 0.0 || number < Int.MIN_VALUE || number > Int.MAX_VALUE) {
383
+ invalid("$key must be a 32-bit integer.")
384
+ }
385
+ return number.toInt()
386
+ }
387
+
388
+ private fun dictionary(
389
+ value: Map<String, Any?>,
390
+ key: String,
391
+ ) = objectMap(value[key])
392
+
393
+ private fun dictionaries(
394
+ value: Map<String, Any?>,
395
+ key: String,
396
+ ): List<Map<String, Any?>> = (value[key] as? List<*>)?.map(::objectMap) ?: invalid("$key must be an object array.")
397
+
398
+ private fun optionalObjects(
399
+ value: Map<String, Any?>,
400
+ key: String,
401
+ ): List<Map<String, Any?>> {
402
+ if (!value.containsKey(key)) return emptyList()
403
+ val items = value[key] as? List<*> ?: invalid("$key must be an object array.")
404
+ return items.map(::objectMap)
405
+ }
406
+
407
+ private fun objectMap(value: Any?): Map<String, Any?> {
408
+ if (value !is Map<*, *>) invalid("Expected an object.")
409
+ return value.entries.associate { (key, item) ->
410
+ (key as? String ?: invalid("Object keys must be strings.")) to item
411
+ }
412
+ }
413
+
414
+ private fun unique(
415
+ values: List<String>,
416
+ name: String,
417
+ ) {
418
+ if (values.toSet().size != values.size) invalid("Duplicate $name ID.")
419
+ }
420
+
421
+ private fun invalid(message: String): Nothing = throw CodedException(DIM_INVALID_ARGUMENT, message, null)
422
+
423
+ private inline fun <T> native(operation: () -> T): T =
424
+ try {
425
+ operation()
426
+ } catch (error: CodedException) {
427
+ throw error
428
+ } catch (error: Throwable) {
429
+ throw CodedException(DIM_NATIVE_ERROR, error.message ?: "The native dim-screen operation failed.", error)
430
+ }
431
+ }