@react-native-kakao/map 2.2.6 → 3.2.0

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 +27 -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,1096 @@
1
+ package net.mjstudio.rnkakao.map
2
+
3
+ import android.graphics.Color
4
+ import android.graphics.PointF
5
+ import com.kakao.vectormap.KakaoMap
6
+ import com.kakao.vectormap.LatLng
7
+ import com.kakao.vectormap.animation.Interpolation
8
+ import com.kakao.vectormap.shape.DotPoints
9
+ import com.kakao.vectormap.shape.LatLngVertex
10
+ import com.kakao.vectormap.shape.MapPoints
11
+ import com.kakao.vectormap.shape.PointVertex
12
+ import com.kakao.vectormap.shape.Polygon
13
+ import com.kakao.vectormap.shape.PolygonOptions
14
+ import com.kakao.vectormap.shape.PolygonStyle
15
+ import com.kakao.vectormap.shape.PolygonStyles
16
+ import com.kakao.vectormap.shape.PolygonStylesSet
17
+ import com.kakao.vectormap.shape.Polyline
18
+ import com.kakao.vectormap.shape.PolylineCap
19
+ import com.kakao.vectormap.shape.PolylineOptions
20
+ import com.kakao.vectormap.shape.PolylineStyle
21
+ import com.kakao.vectormap.shape.PolylineStyles
22
+ import com.kakao.vectormap.shape.PolylineStylesSet
23
+ import com.kakao.vectormap.shape.ShapeAnimator
24
+ import com.kakao.vectormap.shape.ShapeLayer
25
+ import com.kakao.vectormap.shape.ShapeLayerOptions
26
+ import com.kakao.vectormap.shape.ShapeLayerPass
27
+ import com.kakao.vectormap.shape.animation.CircleWave
28
+ import com.kakao.vectormap.shape.animation.CircleWaves
29
+ import expo.modules.kotlin.exception.CodedException
30
+ import kotlin.math.asin
31
+ import kotlin.math.atan2
32
+ import kotlin.math.cos
33
+ import kotlin.math.sin
34
+
35
+ internal class MapShapes(
36
+ map: KakaoMap,
37
+ density: Float,
38
+ private val emit: (String, Map<String, Any?>) -> Unit,
39
+ ) {
40
+ private val manager = map.shapeManager ?: invalid("Shape manager is unavailable.")
41
+ private val density = density.takeIf { it.isFinite() && it > 0 } ?: invalid("Display density must be positive and finite.")
42
+ private val layers = mutableMapOf<String, ShapeLayer>()
43
+ private val shapes = mutableMapOf<String, ShapeEntry>()
44
+ private val animators = mutableMapOf<String, ShapeAnimator>()
45
+ private val animationTargets = mutableMapOf<String, Set<String>>()
46
+ private val animationNativeIds = mutableMapOf<String, String>()
47
+ private val animatorIdsByDefinition = mutableMapOf<String, String>()
48
+ private val animatorCache = mutableMapOf<String, ShapeAnimator>()
49
+ private val polygonStyles = mutableMapOf<String, PolygonStylesSet>()
50
+ private val polylineStyles = mutableMapOf<String, PolylineStylesSet>()
51
+ private val styleCounts = mutableMapOf<String, Int>()
52
+ private var animationSequence = 0
53
+ private var sceneSequence = 0
54
+ private var replacementSequence = 0
55
+ private val nativePrefix = "rnk-shapes-${System.identityHashCode(this)}"
56
+
57
+ fun apply(scene: Map<String, Any?>): Map<String, Any> {
58
+ val styleSpecs = scene.list("styleSets")
59
+ val layerSpecs = scene.list("layers")
60
+ val shapeSpecs = scene.list("shapes")
61
+ validateScene(styleSpecs, layerSpecs, shapeSpecs)
62
+ val nextPolygonStyles = mutableMapOf<String, PolygonStylesSet>()
63
+ val nextPolylineStyles = mutableMapOf<String, PolylineStylesSet>()
64
+ val nextStyleCounts = mutableMapOf<String, Int>()
65
+ val nextLayers = mutableMapOf<String, ShapeLayer>()
66
+ val nextShapes = mutableMapOf<String, ShapeEntry>()
67
+ registerStyles(styleSpecs, nextPolygonStyles, nextPolylineStyles, nextStyleCounts)
68
+ val scenePrefix = "$nativePrefix-scene-${sceneSequence++}"
69
+ try {
70
+ layerSpecs.forEach { addLayer(it, nextLayers, scenePrefix) }
71
+ shapeSpecs.forEach { addShape(it, nextLayers, nextShapes, nextPolygonStyles, nextPolylineStyles) }
72
+ } catch (error: Throwable) {
73
+ nextLayers.values.forEach(manager::remove)
74
+ throw error
75
+ }
76
+ disposeObjects()
77
+ polygonStyles.clear()
78
+ polygonStyles.putAll(nextPolygonStyles)
79
+ polylineStyles.clear()
80
+ polylineStyles.putAll(nextPolylineStyles)
81
+ styleCounts.clear()
82
+ styleCounts.putAll(nextStyleCounts)
83
+ layers.putAll(nextLayers)
84
+ shapes.putAll(nextShapes)
85
+ layerSpecs.forEach { spec -> nextLayers.getValue(spec.string("id")).setVisible(spec.boolean("visible", true)) }
86
+ return counts()
87
+ }
88
+
89
+ fun command(command: Map<String, Any?>): Map<String, Any?> =
90
+ when (command.string("type")) {
91
+ "setVisible" -> {
92
+ val targets = entries(command.stringList("ids"))
93
+ val isVisible = command.boolean("visible")
94
+ targets.forEach { it.setVisible(isVisible) }
95
+ none()
96
+ }
97
+
98
+ "setPosition" -> {
99
+ entry(command.string("id")).setPosition(command.coordinate("position"))
100
+ none()
101
+ }
102
+
103
+ "setZIndex" -> {
104
+ entry(command.string("id")).setZIndex(command.int("zIndex"))
105
+ none()
106
+ }
107
+
108
+ "setOrientation" -> {
109
+ throw unsupported("Android Kakao Maps does not expose shape orientation.")
110
+ }
111
+
112
+ "changeData" -> {
113
+ changeData(command)
114
+ none()
115
+ }
116
+
117
+ "startWave" -> {
118
+ startWave(command.map("animation"))
119
+ none()
120
+ }
121
+
122
+ "stopAnimation" -> {
123
+ stopAnimation(command.string("animationId"))
124
+ none()
125
+ }
126
+
127
+ "getState" -> {
128
+ state(command.string("id"))
129
+ }
130
+
131
+ "getCounts" -> {
132
+ counts()
133
+ }
134
+
135
+ "getCapabilities" -> {
136
+ capabilities() + ("type" to "capabilities")
137
+ }
138
+
139
+ "remove" -> {
140
+ val ids = command.stringList("ids")
141
+ entries(ids)
142
+ ids.forEach(::remove)
143
+ none()
144
+ }
145
+
146
+ else -> {
147
+ invalid("Unknown shape command type.")
148
+ }
149
+ }
150
+
151
+ fun capabilities() =
152
+ mapOf(
153
+ "layerPasses" to listOf("default", "overlay", "route"),
154
+ "geographicPrimitives" to true,
155
+ "modelPrimitives" to true,
156
+ "geographicPosition" to true,
157
+ "geographicOrientation" to false,
158
+ "modelPosition" to true,
159
+ "modelOrientation" to false,
160
+ "zIndexMutation" to true,
161
+ "fractionalStyleWidth" to true,
162
+ "waveAnimation" to true,
163
+ "waveTargets" to listOf("polygon"),
164
+ "waveNaturalStopEvent" to false,
165
+ )
166
+
167
+ fun resolvePolygon(id: String): Polygon? = shapes[id]?.native as? Polygon
168
+
169
+ fun dispose() {
170
+ disposeObjects()
171
+ polygonStyles.clear()
172
+ polylineStyles.clear()
173
+ styleCounts.clear()
174
+ }
175
+
176
+ private fun validateScene(
177
+ styleSpecs: List<Map<String, Any?>>,
178
+ layerSpecs: List<Map<String, Any?>>,
179
+ shapeSpecs: List<Map<String, Any?>>,
180
+ ) {
181
+ requireUnique(styleSpecs, "id", "style set")
182
+ requireUnique(layerSpecs, "id", "shape layer")
183
+ requireUnique(shapeSpecs, "id", "shape")
184
+ styleSpecs.forEach(::validateStyleSet)
185
+ layerSpecs.forEach { layer ->
186
+ layer.int("zIndex", DEFAULT_SHAPE_LAYER_Z_INDEX)
187
+ layerPass(layer.stringOrNull("pass") ?: "default")
188
+ layer.boolean("visible", true)
189
+ }
190
+ val styleTypes = styleSpecs.associate { it.string("id") to it.string("type") }
191
+ val layerIds = layerSpecs.mapTo(mutableSetOf()) { it.string("id") }
192
+ shapeSpecs.forEach { spec ->
193
+ spec.int("zIndex", 0)
194
+ spec.boolean("visible", true)
195
+ val geometry = spec.map("geometry")
196
+ val geometryType = geometry.string("type")
197
+ val requiredStyle =
198
+ if (geometryType.contains("Polyline") || geometryType in setOf("geographicCurve", "modelArc", "geographicArc")) {
199
+ "polyline"
200
+ } else {
201
+ "polygon"
202
+ }
203
+ if (spec.string("layerId") !in layerIds) invalid("Shape ${spec.string("id")} references an unknown layer.")
204
+ if (styleTypes[spec.string("styleSetId")] != requiredStyle) {
205
+ invalid("Shape ${spec.string("id")} references an incompatible style set.")
206
+ }
207
+ val normalized = normalizeGeometry(geometry)
208
+ rejectAndroidOrientation(normalized)
209
+ validateGeometry(normalized, styleSpecs.first { it.string("id") == spec.string("styleSetId") }.list("styles").size)
210
+ }
211
+ }
212
+
213
+ private fun validateStyleSet(spec: Map<String, Any?>) {
214
+ val styles = spec.list("styles")
215
+ if (styles.isEmpty()) invalid("Shape style sets require styles.")
216
+ when (spec.string("type")) {
217
+ "polygon" -> {
218
+ mapShapePolygonStylesSet(spec)
219
+ }
220
+
221
+ "polyline" -> {
222
+ polylineCap(spec.stringOrNull("cap") ?: "square")
223
+ styles.forEach { style ->
224
+ val levels = style.list("levels")
225
+ if (levels.isEmpty()) invalid("Polyline styles require levels.")
226
+ levels.forEach { level ->
227
+ level.int("level")
228
+ if (level.float("width") < 0 || level.float("strokeWidth", 0f) < 0) {
229
+ invalid("Polyline widths must be non-negative.")
230
+ }
231
+ level.color("color")
232
+ level.color("strokeColor", Color.TRANSPARENT)
233
+ }
234
+ }
235
+ }
236
+
237
+ else -> {
238
+ invalid("Unknown shape style type.")
239
+ }
240
+ }
241
+ }
242
+
243
+ private fun registerStyles(
244
+ specs: List<Map<String, Any?>>,
245
+ targetPolygonStyles: MutableMap<String, PolygonStylesSet>,
246
+ targetPolylineStyles: MutableMap<String, PolylineStylesSet>,
247
+ targetStyleCounts: MutableMap<String, Int>,
248
+ ) {
249
+ specs.forEach { spec ->
250
+ val id = spec.string("id")
251
+ val nativeSpec = spec + ("id" to mapStyleNativeID("rnk-shape-style", spec))
252
+ if (spec.string("type") == "polygon") {
253
+ targetPolygonStyles[id] = manager.addPolygonStyles(mapShapePolygonStylesSet(nativeSpec, density))
254
+ } else {
255
+ val styles =
256
+ spec.list("styles").map { style ->
257
+ PolylineStyles.from(
258
+ style.list("levels").map { level ->
259
+ PolylineStyle.from(
260
+ level.int("level"),
261
+ level.float("width") * density,
262
+ level.color("color"),
263
+ level.float("strokeWidth", 0f) * density,
264
+ level.color("strokeColor", Color.TRANSPARENT),
265
+ )
266
+ },
267
+ )
268
+ }
269
+ val set = PolylineStylesSet.from(nativeSpec.string("id"), styles).setPolylineCap(polylineCap(spec.stringOrNull("cap") ?: "square"))
270
+ targetPolylineStyles[id] = manager.addPolylineStyles(set)
271
+ }
272
+ targetStyleCounts[id] = spec.list("styles").size
273
+ }
274
+ }
275
+
276
+ private fun addLayer(
277
+ spec: Map<String, Any?>,
278
+ targetLayers: MutableMap<String, ShapeLayer>,
279
+ scenePrefix: String,
280
+ ) {
281
+ val id = spec.string("id")
282
+ val options =
283
+ ShapeLayerOptions
284
+ .from(
285
+ "$scenePrefix-layer-$id",
286
+ spec.int("zIndex", DEFAULT_SHAPE_LAYER_Z_INDEX),
287
+ layerPass(spec.stringOrNull("pass") ?: "default"),
288
+ ).setVisible(false)
289
+ targetLayers[id] = manager.addLayer(options) ?: invalid("Could not create shape layer $id.")
290
+ }
291
+
292
+ private fun addShape(
293
+ spec: Map<String, Any?>,
294
+ targetLayers: Map<String, ShapeLayer>,
295
+ targetShapes: MutableMap<String, ShapeEntry>,
296
+ targetPolygonStyles: Map<String, PolygonStylesSet>,
297
+ targetPolylineStyles: Map<String, PolylineStylesSet>,
298
+ nativeId: String = spec.string("id"),
299
+ ) {
300
+ val id = spec.string("id")
301
+ val layerId = spec.string("layerId")
302
+ val layer = targetLayers[layerId] ?: invalid("Unknown shape layer $layerId.")
303
+ val styleId = spec.string("styleSetId")
304
+ val rawGeometry = spec.map("geometry")
305
+ val geometryType = rawGeometry.string("type")
306
+ val geometry = normalizeGeometry(rawGeometry)
307
+ val entry =
308
+ when (geometry.string("type")) {
309
+ "geographicPolygon" -> {
310
+ val options =
311
+ PolygonOptions
312
+ .from(
313
+ nativeId,
314
+ ).setStylesSet(targetPolygonStyles[styleId] ?: unknown("polygon style set", styleId))
315
+ .setZOrder(spec.int("zIndex", 0))
316
+ .setVisible(spec.boolean("visible", true))
317
+ geometry.list("parts").forEach { options.addPolygon(mapShapeMapPoints(it), it.int("styleIndex", 0)) }
318
+ ShapeEntry(layerId, geometryType, layer.addPolygon(options) ?: invalid("Could not create polygon $id."))
319
+ }
320
+
321
+ "modelPolygon" -> {
322
+ rejectAndroidOrientation(geometry)
323
+ val options =
324
+ PolygonOptions
325
+ .from(
326
+ nativeId,
327
+ ).setStylesSet(targetPolygonStyles[styleId] ?: unknown("polygon style set", styleId))
328
+ .setZOrder(spec.int("zIndex", 0))
329
+ .setVisible(spec.boolean("visible", true))
330
+ geometry.list("parts").forEach {
331
+ options.addPolygon(mapShapeDotPoints(geometry.coordinate("base"), it), it.int("styleIndex", 0))
332
+ }
333
+ ShapeEntry(layerId, geometryType, layer.addPolygon(options) ?: invalid("Could not create model polygon $id."))
334
+ }
335
+
336
+ "geographicPolyline", "geographicCurve" -> {
337
+ val options =
338
+ PolylineOptions
339
+ .from(
340
+ nativeId,
341
+ ).setStylesSet(targetPolylineStyles[styleId] ?: unknown("polyline style set", styleId))
342
+ .setZOrder(spec.int("zIndex", 0))
343
+ .setVisible(spec.boolean("visible", true))
344
+ geometry.list("parts").forEach { options.addPolyline(mapLine(it), it.int("styleIndex", 0), 0) }
345
+ ShapeEntry(layerId, geometryType, layer.addPolyline(options) ?: invalid("Could not create polyline $id."))
346
+ }
347
+
348
+ "modelPolyline" -> {
349
+ rejectAndroidOrientation(geometry)
350
+ val options =
351
+ PolylineOptions
352
+ .from(
353
+ nativeId,
354
+ ).setStylesSet(targetPolylineStyles[styleId] ?: unknown("polyline style set", styleId))
355
+ .setZOrder(spec.int("zIndex", 0))
356
+ .setVisible(spec.boolean("visible", true))
357
+ geometry
358
+ .list(
359
+ "parts",
360
+ ).forEach { options.addPolyline(dotLine(geometry.coordinate("base"), it), it.int("styleIndex", 0), 0) }
361
+ ShapeEntry(layerId, geometryType, layer.addPolyline(options) ?: invalid("Could not create model polyline $id."))
362
+ }
363
+
364
+ else -> {
365
+ invalid("Unsupported normalized shape geometry.")
366
+ }
367
+ }
368
+ targetShapes[id] = entry
369
+ }
370
+
371
+ private fun normalizeGeometry(geometry: Map<String, Any?>): Map<String, Any?> =
372
+ when (geometry.string("type")) {
373
+ "modelCircle" -> geometry.primitiveModel(circle = true)
374
+ "modelRectangle" -> geometry.primitiveModel(circle = false)
375
+ "geographicCircle" -> geometry.primitiveGeographic(circle = true)
376
+ "geographicRectangle" -> geometry.primitiveGeographic(circle = false)
377
+ "geographicCurve" -> geometry.curveGeometry()
378
+ "modelArc" -> geometry.modelArcGeometry()
379
+ "geographicArc" -> geometry.geographicArcGeometry()
380
+ else -> geometry
381
+ }
382
+
383
+ private fun changeData(command: Map<String, Any?>) {
384
+ val id = command.string("id")
385
+ val old = entry(id)
386
+ val styleId = command.string("styleSetId")
387
+ val geometry = normalizeGeometry(command.map("geometry"))
388
+ rejectAndroidOrientation(geometry)
389
+ val expectsPolyline = geometry.string("type").contains("Polyline") || geometry.string("type") == "geographicCurve"
390
+ if ((expectsPolyline && styleId !in polylineStyles) || (!expectsPolyline && styleId !in polygonStyles)) {
391
+ invalid("Shape $id references an incompatible style set.")
392
+ }
393
+ validateGeometry(geometry, styleCounts[styleId] ?: invalid("Unknown shape style set $styleId."))
394
+ val visible = old.visible()
395
+ val zIndex = old.zIndex()
396
+ val spec =
397
+ mapOf(
398
+ "id" to id,
399
+ "layerId" to old.layerId,
400
+ "styleSetId" to styleId,
401
+ "zIndex" to zIndex,
402
+ "visible" to false,
403
+ "geometry" to command.map("geometry"),
404
+ )
405
+ val replacement = mutableMapOf<String, ShapeEntry>()
406
+ addShape(
407
+ spec,
408
+ layers,
409
+ replacement,
410
+ polygonStyles,
411
+ polylineStyles,
412
+ "$nativePrefix-replacement-${replacementSequence++}",
413
+ )
414
+ remove(id)
415
+ replacement.getValue(id).also {
416
+ it.setVisible(visible)
417
+ shapes[id] = it
418
+ }
419
+ }
420
+
421
+ private fun startWave(animation: Map<String, Any?>) {
422
+ val id = animation.string("animationId")
423
+ if (id.isBlank()) invalid("Shape animationId must not be empty.")
424
+ if (animators.containsKey(id)) invalid("Shape animation $id already exists.")
425
+ val definition = mapCanonicalValue(animation - "animationId")
426
+ val nativeId =
427
+ animatorIdsByDefinition.getOrPut(definition) {
428
+ "rnk-shape-${System.identityHashCode(this)}-${animationSequence++}"
429
+ }
430
+ if (nativeId in animationNativeIds.values) invalid("An equivalent shape animation is already running.")
431
+ val polygons =
432
+ animation.stringList("shapeIds").map { shapeId ->
433
+ val native = entry(shapeId).native
434
+ native as? Polygon ?: invalid("Wave animation supports polygons only: $shapeId.")
435
+ }
436
+ if (polygons.isEmpty()) invalid("Shape wave animation requires shapeIds.")
437
+ val animator =
438
+ animatorCache[nativeId] ?: run {
439
+ val effect = mapShapeCircleWaves(animation + ("animationId" to nativeId), density)
440
+ (manager.addAnimator(effect) ?: invalid("Could not create shape animation $id.")).also {
441
+ animatorCache[nativeId] = it
442
+ }
443
+ }
444
+ animator.removeAllPolygon()
445
+ animator.addPolygons(polygons)
446
+ animators[id] = animator
447
+ animationTargets[id] = animation.stringList("shapeIds").toSet()
448
+ animationNativeIds[id] = nativeId
449
+ animator.start()
450
+ }
451
+
452
+ private fun stopAnimation(id: String) {
453
+ val animator = animators.remove(id) ?: unknown("shape animation", id)
454
+ animationTargets.remove(id)
455
+ animationNativeIds.remove(id)
456
+ animator.stop()
457
+ animator.removeAllPolygon()
458
+ emit("onShapeAnimationStop", mapOf("animationId" to id))
459
+ }
460
+
461
+ private fun remove(id: String) {
462
+ animationTargets
463
+ .filterValues { id in it }
464
+ .keys
465
+ .toList()
466
+ .forEach(::stopAnimation)
467
+ val shape = shapes.remove(id) ?: unknown("shape", id)
468
+ shape.remove()
469
+ }
470
+
471
+ private fun disposeObjects() {
472
+ animators.values.forEach {
473
+ it.stop()
474
+ it.removeAllPolygon()
475
+ }
476
+ animators.clear()
477
+ animationTargets.clear()
478
+ animationNativeIds.clear()
479
+ layers.values.forEach(manager::remove)
480
+ layers.clear()
481
+ shapes.clear()
482
+ }
483
+
484
+ private fun state(id: String): Map<String, Any?> {
485
+ val value = entry(id)
486
+ return mapOf(
487
+ "type" to "state",
488
+ "id" to id,
489
+ "layerId" to value.layerId,
490
+ "visible" to value.visible(),
491
+ "zIndex" to value.zIndex(),
492
+ "geometryType" to value.geometryType,
493
+ "position" to null,
494
+ "orientation" to null,
495
+ )
496
+ }
497
+
498
+ private fun counts() = mapOf("type" to "counts", "layers" to layers.size, "shapes" to shapes.size, "animations" to animators.size)
499
+
500
+ private fun none() = mapOf<String, Any?>("type" to "none")
501
+
502
+ private fun entry(id: String) = shapes[id] ?: unknown("shape", id)
503
+
504
+ private fun entries(ids: List<String>): List<ShapeEntry> {
505
+ if (ids.toSet().size != ids.size) invalid("Shape command IDs must be unique.")
506
+ return ids.map(::entry)
507
+ }
508
+
509
+ private data class ShapeEntry(
510
+ val layerId: String,
511
+ val geometryType: String,
512
+ val native: Any,
513
+ ) {
514
+ fun setVisible(visible: Boolean) =
515
+ when (val value = native) {
516
+ is Polygon -> if (visible) value.show() else value.hide()
517
+ is Polyline -> if (visible) value.show() else value.hide()
518
+ else -> Unit
519
+ }
520
+
521
+ fun visible() =
522
+ when (val value = native) {
523
+ is Polygon -> value.isShow
524
+ is Polyline -> value.isShow
525
+ else -> false
526
+ }
527
+
528
+ fun zIndex() =
529
+ when (val value = native) {
530
+ is Polygon -> value.zOrder
531
+ is Polyline -> value.zOrder
532
+ else -> 0
533
+ }
534
+
535
+ fun setPosition(position: LatLng) =
536
+ when (val value = native) {
537
+ is Polygon -> value.setPosition(position)
538
+ is Polyline -> value.setPosition(position)
539
+ else -> Unit
540
+ }
541
+
542
+ fun setZIndex(zIndex: Int) =
543
+ when (val value = native) {
544
+ is Polygon -> value.setZOrder(zIndex)
545
+ is Polyline -> value.setZOrder(zIndex)
546
+ else -> Unit
547
+ }
548
+
549
+ fun remove() =
550
+ when (val value = native) {
551
+ is Polygon -> value.remove()
552
+ is Polyline -> value.remove()
553
+ else -> Unit
554
+ }
555
+ }
556
+ }
557
+
558
+ private const val SHAPE_ERROR = "ERR_KAKAO_MAP_SHAPE"
559
+ private const val DEFAULT_SHAPE_LAYER_Z_INDEX = 10_000
560
+ private const val EARTH_RADIUS_METERS = 6_378_137.0
561
+
562
+ internal fun mapStyleNativeID(
563
+ prefix: String,
564
+ spec: Map<String, Any?>,
565
+ ): String {
566
+ val canonical = mapCanonicalValue(spec)
567
+ return "$prefix-${canonical.length}-${canonical.hashCode().toUInt().toString(16)}"
568
+ }
569
+
570
+ internal fun mapCanonicalValue(value: Any?): String =
571
+ when (value) {
572
+ null -> {
573
+ "n"
574
+ }
575
+
576
+ is Map<*, *> -> {
577
+ value.entries
578
+ .sortedBy { it.key.toString() }
579
+ .joinToString(prefix = "{", postfix = "}") { entry ->
580
+ val key = entry.key.toString()
581
+ "${key.length}:$key=${mapCanonicalValue(entry.value)}"
582
+ }
583
+ }
584
+
585
+ is List<*> -> {
586
+ value.joinToString(prefix = "[", postfix = "]", transform = ::mapCanonicalValue)
587
+ }
588
+
589
+ is String -> {
590
+ "s${value.length}:$value"
591
+ }
592
+
593
+ is Number -> {
594
+ "d${value.toDouble()}"
595
+ }
596
+
597
+ is Boolean -> {
598
+ if (value) "t" else "f"
599
+ }
600
+
601
+ else -> {
602
+ "o${value.toString().length}:$value"
603
+ }
604
+ }
605
+
606
+ private fun validateGeometry(
607
+ geometry: Map<String, Any?>,
608
+ styleCount: Int,
609
+ ) {
610
+ val type = geometry.string("type")
611
+ val parts =
612
+ when (type) {
613
+ "geographicPolygon", "modelPolygon", "geographicPolyline", "modelPolyline" -> geometry.list("parts")
614
+ else -> emptyList()
615
+ }
616
+ if (parts.isEmpty() &&
617
+ type in setOf("geographicPolygon", "modelPolygon", "geographicPolyline", "modelPolyline")
618
+ ) {
619
+ invalid("Shape geometry requires parts.")
620
+ }
621
+ parts.forEach { part ->
622
+ val index = part.int("styleIndex", 0)
623
+ if (index !in 0 until styleCount) invalid("Shape styleIndex $index is out of range.")
624
+ val points = (part["outer"] ?: part["points"]) as? List<*> ?: invalid("Shape part requires points.")
625
+ val minimum = if (type.contains("Polygon")) 3 else 2
626
+ if (points.size < minimum) invalid("Shape part requires at least $minimum points.")
627
+ if (type.contains("Polygon")) {
628
+ part.listOrNull("holes")?.forEach { hole ->
629
+ if (hole.size < 3) invalid("Shape polygon holes require at least 3 points.")
630
+ }
631
+ }
632
+ }
633
+ when (type) {
634
+ "geographicPolygon" -> {
635
+ parts.forEach(::mapShapeMapPoints)
636
+ }
637
+
638
+ "modelPolygon" -> {
639
+ val base = geometry.coordinate("base")
640
+ parts.forEach { mapShapeDotPoints(base, it) }
641
+ geometry.double("orientation", 0.0)
642
+ }
643
+
644
+ "geographicPolyline", "geographicCurve" -> {
645
+ parts.forEach(::mapLine)
646
+ }
647
+
648
+ "modelPolyline" -> {
649
+ val base = geometry.coordinate("base")
650
+ parts.forEach { dotLine(base, it) }
651
+ geometry.double("orientation", 0.0)
652
+ }
653
+ }
654
+ }
655
+
656
+ private fun Map<String, Any?>.primitiveModel(circle: Boolean): Map<String, Any?> {
657
+ val base = map("base")
658
+ val points =
659
+ if (circle) {
660
+ circlePoints(
661
+ double("radius"),
662
+ int("pointCount", 64),
663
+ boolean("clockwise", true),
664
+ )
665
+ } else {
666
+ rectanglePoints(double("width"), double("height"), boolean("clockwise", true))
667
+ }
668
+ return mapOf(
669
+ "type" to "modelPolygon",
670
+ "base" to base,
671
+ "orientation" to double("orientation", 0.0),
672
+ "parts" to listOf(mapOf("outer" to points, "styleIndex" to int("styleIndex", 0))),
673
+ )
674
+ }
675
+
676
+ private fun Map<String, Any?>.primitiveGeographic(circle: Boolean): Map<String, Any?> {
677
+ val center = coordinate("center")
678
+ val points =
679
+ if (circle) {
680
+ geographicCircle(
681
+ center,
682
+ double("radius"),
683
+ int("pointCount", 64),
684
+ boolean("clockwise", true),
685
+ )
686
+ } else {
687
+ geographicRectangle(center, double("width"), double("height"), boolean("clockwise", true))
688
+ }
689
+ return mapOf("type" to "geographicPolygon", "parts" to listOf(mapOf("outer" to points, "styleIndex" to int("styleIndex", 0))))
690
+ }
691
+
692
+ private fun Map<String, Any?>.curveGeometry(): Map<String, Any?> {
693
+ val start = coordinate("start")
694
+ val end = coordinate("end")
695
+ val left =
696
+ when (string("side")) {
697
+ "left" -> true
698
+ "right" -> false
699
+ else -> invalid("Curve side must be left or right.")
700
+ }
701
+ val points = quadraticCurve(start, end, left)
702
+ return mapOf("type" to "geographicCurve", "parts" to listOf(mapOf("points" to points, "styleIndex" to int("styleIndex", 0))))
703
+ }
704
+
705
+ private fun Map<String, Any?>.modelArcGeometry(): Map<String, Any?> =
706
+ mapOf(
707
+ "type" to "modelPolyline",
708
+ "base" to map("base"),
709
+ "orientation" to double("orientation", 0.0),
710
+ "parts" to listOf(mapOf("points" to arcPoints(), "styleIndex" to int("styleIndex", 0))),
711
+ )
712
+
713
+ private fun Map<String, Any?>.geographicArcGeometry(): Map<String, Any?> {
714
+ val center = coordinate("center")
715
+ return mapOf(
716
+ "type" to "geographicPolyline",
717
+ "parts" to
718
+ listOf(
719
+ mapOf(
720
+ "points" to arcPoints().map { offset(center, it.getValue("x"), it.getValue("y")) },
721
+ "styleIndex" to int("styleIndex", 0),
722
+ ),
723
+ ),
724
+ )
725
+ }
726
+
727
+ private fun Map<String, Any?>.arcPoints(): List<Map<String, Double>> {
728
+ val radius = double("radius")
729
+ val count = int("pointCount", 64)
730
+ if (radius <= 0 || count < 2) invalid("Arc radius must be positive and pointCount at least 2.")
731
+ val startDegrees = double("startAngle")
732
+ val endDegrees = double("endAngle")
733
+ val clockwise = boolean("clockwise", true)
734
+ val (start, sweep) = arcAngles(startDegrees, endDegrees, clockwise)
735
+ return List(count) { index ->
736
+ val angle = start + sweep * index / (count - 1)
737
+ mapOf("x" to radius * sin(angle), "y" to radius * cos(angle))
738
+ }
739
+ }
740
+
741
+ private fun arcAngles(
742
+ start: Double,
743
+ end: Double,
744
+ clockwise: Boolean,
745
+ ): Pair<Double, Double> {
746
+ val normalizedStart = normalizedDegrees(start)
747
+ val normalizedEnd = normalizedDegrees(end)
748
+ val delta = normalizedEnd - normalizedStart
749
+ val sweep =
750
+ if (delta == 0.0) {
751
+ if (start == end) {
752
+ 0.0
753
+ } else if (clockwise) {
754
+ 360.0
755
+ } else {
756
+ -360.0
757
+ }
758
+ } else if (clockwise) {
759
+ if (delta > 0) delta else delta + 360.0
760
+ } else {
761
+ if (delta < 0) delta else delta - 360.0
762
+ }
763
+ return Math.toRadians(normalizedStart) to Math.toRadians(sweep)
764
+ }
765
+
766
+ private fun normalizedDegrees(value: Double): Double {
767
+ val remainder = value % 360.0
768
+ return if (remainder < 0) remainder + 360.0 else remainder
769
+ }
770
+
771
+ internal fun mapShapeMapPoints(part: Map<String, Any?>): MapPoints {
772
+ val value = MapPoints.fromLatLng(part.coordinateList("outer"))
773
+ val holes =
774
+ part
775
+ .listOrNull("holes")
776
+ ?.map { hole -> LatLngVertex.from(hole.map { it.coordinateValue() }) }
777
+ .orEmpty()
778
+ if (holes.isNotEmpty()) value.setHolePoints(*holes.toTypedArray())
779
+ return value.setMergeOverlappingHoles(part.boolean("mergeOverlappedHoles", false))
780
+ }
781
+
782
+ private fun mapLine(part: Map<String, Any?>) = MapPoints.fromLatLng(part.coordinateList("points"))
783
+
784
+ internal fun mapShapeDotPoints(
785
+ base: LatLng,
786
+ part: Map<String, Any?>,
787
+ ): DotPoints {
788
+ val value = DotPoints.fromPoints(base, part.pointList("outer"), true)
789
+ val holes =
790
+ part
791
+ .listOrNull("holes")
792
+ ?.map { hole -> PointVertex.fromPoint(hole.map { point -> point.pointValue() }, true) }
793
+ .orEmpty()
794
+ if (holes.isNotEmpty()) value.setHolePoints(*holes.toTypedArray())
795
+ return value.setMergeOverlappingHoles(part.boolean("mergeOverlappedHoles", false))
796
+ }
797
+
798
+ private fun dotLine(
799
+ base: LatLng,
800
+ part: Map<String, Any?>,
801
+ ) = DotPoints.fromPoints(base, part.pointList("points"), true)
802
+
803
+ private fun layerPass(value: String) =
804
+ when (value) {
805
+ "default" -> ShapeLayerPass.Default
806
+ "overlay" -> ShapeLayerPass.Overlay
807
+ "route" -> ShapeLayerPass.Route
808
+ else -> invalid("Unknown shape layer pass $value.")
809
+ }
810
+
811
+ private fun polylineCap(value: String) =
812
+ when (value) {
813
+ "butt" -> PolylineCap.Butt
814
+ "round" -> PolylineCap.Round
815
+ "square" -> PolylineCap.Square
816
+ else -> invalid("Unknown polyline cap $value.")
817
+ }
818
+
819
+ private fun interpolation(value: String) =
820
+ when (value) {
821
+ "linear" -> Interpolation.Linear
822
+ "cubicIn" -> Interpolation.CubicIn
823
+ "cubicOut" -> Interpolation.CubicOut
824
+ "cubicInOut" -> Interpolation.CubicInOut
825
+ else -> invalid("Unknown interpolation $value.")
826
+ }
827
+
828
+ internal fun mapShapePolygonStylesSet(
829
+ spec: Map<String, Any?>,
830
+ density: Float = 1f,
831
+ ): PolygonStylesSet =
832
+ PolygonStylesSet.from(
833
+ spec.string("id"),
834
+ spec.list("styles").map { style ->
835
+ val levels = style.list("levels")
836
+ if (levels.isEmpty()) invalid("Polygon styles require levels.")
837
+ PolygonStyles.from(
838
+ levels.map { level ->
839
+ val strokeWidth = level.float("strokeWidth", 0f)
840
+ if (strokeWidth < 0) invalid("Polygon strokeWidth must be non-negative.")
841
+ PolygonStyle.from(
842
+ level.int("level"),
843
+ level.color("fillColor"),
844
+ strokeWidth * density,
845
+ level.color("strokeColor", Color.TRANSPARENT),
846
+ )
847
+ },
848
+ )
849
+ },
850
+ )
851
+
852
+ internal fun mapShapeCircleWaves(
853
+ animation: Map<String, Any?>,
854
+ density: Float = 1f,
855
+ ): CircleWaves {
856
+ val duration = animation.int("duration")
857
+ if (duration <= 0) invalid("Shape animation duration must be positive.")
858
+ val repeat = animation.int("repeat", 1)
859
+ if (repeat <= 0) invalid("Shape animation repeat must be positive.")
860
+ val waves =
861
+ animation.list("frames").map { frame ->
862
+ val startAlpha = frame.float("startAlpha")
863
+ val endAlpha = frame.float("endAlpha")
864
+ val startRadius = frame.float("startRadius")
865
+ val endRadius = frame.float("endRadius")
866
+ if (startAlpha !in 0f..1f || endAlpha !in 0f..1f) invalid("Shape animation alpha must be between 0 and 1.")
867
+ if (startRadius < 0f || endRadius < 0f) invalid("Shape animation radius must be non-negative.")
868
+ CircleWave.from(
869
+ frame.int("level"),
870
+ startAlpha,
871
+ endAlpha,
872
+ startRadius * density,
873
+ endRadius * density,
874
+ )
875
+ }
876
+ if (waves.isEmpty()) invalid("Shape wave animation requires frames.")
877
+ return CircleWaves
878
+ .from(animation.string("animationId"), *waves.toTypedArray())
879
+ .setDuration(duration)
880
+ .setRepeatCount(repeat)
881
+ .setHideShapeAtStop(animation.boolean("hideAtStop", false))
882
+ .setInterpolation(interpolation(animation.stringOrNull("interpolation") ?: "linear"))
883
+ }
884
+
885
+ private fun rejectAndroidOrientation(geometry: Map<String, Any?>) {
886
+ if (geometry.double("orientation", 0.0) != 0.0) throw unsupported("Android Kakao Maps does not expose shape orientation.")
887
+ }
888
+
889
+ private fun circlePoints(
890
+ radius: Double,
891
+ count: Int,
892
+ clockwise: Boolean,
893
+ ): List<Map<String, Double>> {
894
+ if (radius <= 0 || count < 3) invalid("Circle radius must be positive and pointCount at least 3.")
895
+ val sign = if (clockwise) 1 else -1
896
+ return List(count) { index ->
897
+ val angle = sign * 2 * Math.PI * index / count
898
+ mapOf("x" to radius * cos(angle), "y" to radius * sin(angle))
899
+ }
900
+ }
901
+
902
+ private fun rectanglePoints(
903
+ width: Double,
904
+ height: Double,
905
+ clockwise: Boolean,
906
+ ): List<Map<String, Double>> {
907
+ if (width <= 0 || height <= 0) invalid("Rectangle dimensions must be positive.")
908
+ val points =
909
+ listOf(
910
+ mapOf("x" to -width / 2, "y" to -height / 2),
911
+ mapOf("x" to width / 2, "y" to -height / 2),
912
+ mapOf(
913
+ "x" to width / 2,
914
+ "y" to height / 2,
915
+ ),
916
+ mapOf("x" to -width / 2, "y" to height / 2),
917
+ )
918
+ return if (clockwise) points else points.reversed()
919
+ }
920
+
921
+ private fun geographicCircle(
922
+ center: LatLng,
923
+ radius: Double,
924
+ count: Int,
925
+ clockwise: Boolean,
926
+ ) = circlePoints(radius, count, clockwise).map { offset(center, it.getValue("x"), it.getValue("y")) }
927
+
928
+ private fun geographicRectangle(
929
+ center: LatLng,
930
+ width: Double,
931
+ height: Double,
932
+ clockwise: Boolean,
933
+ ) = rectanglePoints(width, height, clockwise).map { offset(center, it.getValue("x"), it.getValue("y")) }
934
+
935
+ private fun offset(
936
+ center: LatLng,
937
+ east: Double,
938
+ north: Double,
939
+ ): Map<String, Double> {
940
+ val distance = kotlin.math.sqrt(east * east + north * north)
941
+ val bearing = atan2(east, north)
942
+ val latitude = Math.toRadians(center.latitude)
943
+ val longitude = Math.toRadians(center.longitude)
944
+ val angular = distance / EARTH_RADIUS_METERS
945
+ val targetLatitude = asin(sin(latitude) * cos(angular) + cos(latitude) * sin(angular) * cos(bearing))
946
+ val targetLongitude = longitude + atan2(sin(bearing) * sin(angular) * cos(latitude), cos(angular) - sin(latitude) * sin(targetLatitude))
947
+ return mapOf("latitude" to Math.toDegrees(targetLatitude), "longitude" to Math.toDegrees(targetLongitude))
948
+ }
949
+
950
+ private fun quadraticCurve(
951
+ start: LatLng,
952
+ end: LatLng,
953
+ left: Boolean,
954
+ ): List<Map<String, Double>> {
955
+ val dx = end.longitude - start.longitude
956
+ val dy = end.latitude - start.latitude
957
+ val side = if (left) 0.25 else -0.25
958
+ val controlLat = (start.latitude + end.latitude) / 2 + dx * side
959
+ val controlLng = (start.longitude + end.longitude) / 2 - dy * side
960
+ return List(33) { index ->
961
+ val t = index / 32.0
962
+ val u = 1 - t
963
+ mapOf(
964
+ "latitude" to u * u * start.latitude + 2 * u * t * controlLat + t * t * end.latitude,
965
+ "longitude" to u * u * start.longitude + 2 * u * t * controlLng + t * t * end.longitude,
966
+ )
967
+ }
968
+ }
969
+
970
+ private fun requireUnique(
971
+ values: List<Map<String, Any?>>,
972
+ key: String,
973
+ noun: String,
974
+ ) {
975
+ val ids = values.map { it.string(key) }
976
+ if (ids.any(String::isBlank)) invalid("$noun ID must not be empty.")
977
+ if (ids.toSet().size != ids.size) invalid("Duplicate $noun ID.")
978
+ }
979
+
980
+ private fun Map<String, Any?>.map(key: String) = this[key] as? Map<String, Any?> ?: invalid("$key must be an object.")
981
+
982
+ private fun Map<String, Any?>.list(key: String): List<Map<String, Any?>> {
983
+ if (!containsKey(key)) return emptyList()
984
+ val raw = this[key] as? List<*> ?: invalid("$key must be an array of objects.")
985
+ return raw.map { it as? Map<String, Any?> ?: invalid("$key must contain objects.") }
986
+ }
987
+
988
+ private fun Map<String, Any?>.listOrNull(key: String): List<List<*>>? {
989
+ if (!containsKey(key)) return null
990
+ val raw = this[key] as? List<*> ?: invalid("$key must be an array of arrays.")
991
+ return raw.map {
992
+ it as? List<*>
993
+ ?: invalid("$key must contain arrays.")
994
+ }
995
+ }
996
+
997
+ private fun Map<String, Any?>.string(key: String) = this[key] as? String ?: invalid("$key must be a string.")
998
+
999
+ private fun Map<String, Any?>.stringOrNull(key: String): String? {
1000
+ if (!containsKey(key)) return null
1001
+ return this[key] as? String ?: invalid("$key must be a string.")
1002
+ }
1003
+
1004
+ private fun Map<String, Any?>.stringList(key: String) =
1005
+ (this[key] as? List<*>)?.map { it as? String ?: invalid("$key must contain strings.") } ?: invalid("$key must be an array.")
1006
+
1007
+ private fun Map<String, Any?>.boolean(
1008
+ key: String,
1009
+ fallback: Boolean? = null,
1010
+ ): Boolean {
1011
+ if (!containsKey(key)) return fallback ?: invalid("$key must be a boolean.")
1012
+ return this[key] as? Boolean ?: invalid("$key must be a boolean.")
1013
+ }
1014
+
1015
+ private fun Map<String, Any?>.double(
1016
+ key: String,
1017
+ fallback: Double? = null,
1018
+ ): Double {
1019
+ if (!containsKey(key)) return fallback ?: invalid("$key must be finite.")
1020
+ return (this[key] as? Number)?.toDouble()?.takeIf { it.isFinite() } ?: invalid("$key must be finite.")
1021
+ }
1022
+
1023
+ private fun Map<String, Any?>.float(
1024
+ key: String,
1025
+ fallback: Float? = null,
1026
+ ) = double(key, fallback?.toDouble()).toFloat()
1027
+
1028
+ private fun Map<String, Any?>.int(
1029
+ key: String,
1030
+ fallback: Int? = null,
1031
+ ): Int {
1032
+ val value = double(key, fallback?.toDouble())
1033
+ if (value % 1 !=
1034
+ 0.0 ||
1035
+ value !in Int.MIN_VALUE.toDouble()..Int.MAX_VALUE.toDouble()
1036
+ ) {
1037
+ invalid("$key must be an integer.")
1038
+ }
1039
+ return value.toInt()
1040
+ }
1041
+
1042
+ private fun Map<String, Any?>.color(
1043
+ key: String,
1044
+ fallback: Int? = null,
1045
+ ): Int {
1046
+ if (!containsKey(key)) return fallback ?: invalid("$key must be a color.")
1047
+ return (this[key] as? String)?.let(::parseColor) ?: invalid("$key must be a color.")
1048
+ }
1049
+
1050
+ private fun Map<String, Any?>.coordinate(key: String) = map(key).coordinateValue()
1051
+
1052
+ private fun Map<String, Any?>.coordinateList(key: String) =
1053
+ (this[key] as? List<*>)?.map { it.coordinateValue() } ?: invalid("$key must be an array.")
1054
+
1055
+ private fun Map<String, Any?>.pointList(key: String) = (this[key] as? List<*>)?.map { it.pointValue() } ?: invalid("$key must be an array.")
1056
+
1057
+ private fun Any?.coordinateValue(): LatLng {
1058
+ val value =
1059
+ this as? Map<String, Any?> ?: invalid("Coordinate must be an object.")
1060
+ val latitude = value.double("latitude")
1061
+ val longitude = value.double("longitude")
1062
+ if (latitude !in
1063
+ -90.0..90.0 ||
1064
+ longitude !in -180.0..180.0
1065
+ ) {
1066
+ invalid("Invalid coordinate.")
1067
+ }
1068
+ return LatLng.from(latitude, longitude)
1069
+ }
1070
+
1071
+ private fun Any?.pointValue(): PointF {
1072
+ val value =
1073
+ this as? Map<String, Any?> ?: invalid("Point must be an object.")
1074
+ return PointF(value.float("x"), value.float("y"))
1075
+ }
1076
+
1077
+ private fun parseColor(value: String): Int {
1078
+ if (!Regex("^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$").matches(value)) invalid("Colors must be #RRGGBB or #RRGGBBAA.")
1079
+ val raw = value.drop(1)
1080
+ return if (raw.length ==
1081
+ 6
1082
+ ) {
1083
+ Color.parseColor(value)
1084
+ } else {
1085
+ ((raw.takeLast(2).toLong(16) shl 24) or raw.dropLast(2).toLong(16)).toInt()
1086
+ }
1087
+ }
1088
+
1089
+ private fun invalid(message: String): Nothing = throw CodedException("${SHAPE_ERROR}_INVALID_ARGUMENT", message, null)
1090
+
1091
+ private fun unsupported(message: String): CodedException = CodedException("${SHAPE_ERROR}_UNSUPPORTED", message, null)
1092
+
1093
+ private fun unknown(
1094
+ noun: String,
1095
+ id: String,
1096
+ ): Nothing = throw CodedException("${SHAPE_ERROR}_NOT_FOUND", "Unknown $noun $id.", null)