@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,805 @@
1
+ package net.mjstudio.rnkakao.map
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Color
5
+ import com.kakao.vectormap.CurveType
6
+ import com.kakao.vectormap.KakaoMap
7
+ import com.kakao.vectormap.LatLng
8
+ import com.kakao.vectormap.animation.Interpolation
9
+ import com.kakao.vectormap.route.RouteLine
10
+ import com.kakao.vectormap.route.RouteLineAnimator
11
+ import com.kakao.vectormap.route.RouteLineLayer
12
+ import com.kakao.vectormap.route.RouteLineOptions
13
+ import com.kakao.vectormap.route.RouteLinePattern
14
+ import com.kakao.vectormap.route.RouteLineSegment
15
+ import com.kakao.vectormap.route.RouteLineStyle
16
+ import com.kakao.vectormap.route.RouteLineStyles
17
+ import com.kakao.vectormap.route.RouteLineStylesSet
18
+ import com.kakao.vectormap.route.animation.ProgressAnimation
19
+ import com.kakao.vectormap.route.animation.ProgressDirection
20
+ import com.kakao.vectormap.route.animation.ProgressType
21
+ import expo.modules.kotlin.exception.CodedException
22
+ import java.lang.ref.WeakReference
23
+
24
+ internal class MapRoutes(
25
+ map: KakaoMap,
26
+ density: Float,
27
+ private val imageLoader: MapImageLoader,
28
+ private val emit: (String, Map<String, Any>) -> Unit,
29
+ ) {
30
+ private val manager = map.routeLineManager ?: invalidRoute("Route manager is unavailable.")
31
+ private val density = density.takeIf { it.isFinite() && it > 0 } ?: invalidRoute("Display density must be positive and finite.")
32
+ private val layers = mutableMapOf<String, RouteLineLayer>()
33
+ private val routes = mutableMapOf<String, RouteLine>()
34
+ private val styleSets = mutableMapOf<String, RouteLineStylesSet>()
35
+ private val styleCounts = mutableMapOf<String, Int>()
36
+ private val animators = mutableMapOf<String, RouteLineAnimator>()
37
+ private val animationTargets = mutableMapOf<String, Set<String>>()
38
+ private val animationNativeIds = mutableMapOf<String, String>()
39
+ private val animationRunEpochs = mutableMapOf<String, Long>()
40
+ private val animatorIdsByDefinition = mutableMapOf<String, String>()
41
+ private val animatorCache = mutableMapOf<String, RouteLineAnimator>()
42
+ private val animatorTargetIds = mutableMapOf<String, Set<String>>()
43
+ private val animatorsNeedingTargets = mutableSetOf<String>()
44
+ private val requestedProgress = mutableMapOf<String, Float>()
45
+ private val progressEpochs = mutableMapOf<String, Long>()
46
+ private val routeLayerIds = mutableMapOf<String, String>()
47
+ private val pendingImages = mutableSetOf<MapImageLoadToken>()
48
+ private var pendingApply: ((Result<Map<String, Any>>) -> Unit)? = null
49
+ private var generation = 0
50
+ private var animationSequence = 0
51
+ private var animationRunSequence = 0L
52
+ private var sceneSequence = 0
53
+ private var replacementSequence = 0
54
+ private val nativePrefix = "rnk-routes-${System.identityHashCode(this)}"
55
+
56
+ fun apply(
57
+ scene: Map<String, Any?>,
58
+ completion: (Result<Map<String, Any>>) -> Unit,
59
+ ) {
60
+ rejectPendingApply("${ROUTE_ERROR}_SUPERSEDED", "The route scene was superseded by a newer update.")
61
+ pendingImages.forEach(MapImageLoadToken::cancel)
62
+ pendingImages.clear()
63
+ val token = ++generation
64
+ val styleSpecs = scene.list("styleSets")
65
+ val layerSpecs = scene.list("layers")
66
+ val routeSpecs = scene.list("routes")
67
+ validateScene(styleSpecs, layerSpecs, routeSpecs)
68
+ pendingApply = completion
69
+ val uris =
70
+ styleSpecs
71
+ .flatMap { it.list("patterns") }
72
+ .flatMap { pattern ->
73
+ listOfNotNull(pattern.map("image").string("uri"), pattern.mapOrNull("symbol")?.string("uri"))
74
+ }.toSet()
75
+ if (uris.isEmpty()) {
76
+ val result = runCatching { buildScene(styleSpecs, layerSpecs, routeSpecs, emptyMap()) }
77
+ pendingApply = null
78
+ completion(result)
79
+ return
80
+ }
81
+ val images = mutableMapOf<String, Bitmap>()
82
+ val requests = mutableListOf<MapImageLoadToken>()
83
+ var isCompleted = false
84
+ val managerReference = WeakReference(this)
85
+
86
+ fun finish(result: Result<Map<String, Any>>) {
87
+ val owner = managerReference.get() ?: return
88
+ if (isCompleted || token != owner.generation) return
89
+ isCompleted = true
90
+ if (result.isFailure) requests.forEach(MapImageLoadToken::cancel)
91
+ requests.forEach(owner.pendingImages::remove)
92
+ owner.pendingApply = null
93
+ completion(result)
94
+ }
95
+ uris.forEach { uri ->
96
+ val request =
97
+ imageLoader.load(uri) { result ->
98
+ result.fold(
99
+ onSuccess = { bitmap ->
100
+ images[uri] = bitmap
101
+ if (images.size == uris.size) {
102
+ managerReference.get()?.let { owner ->
103
+ finish(runCatching { owner.buildScene(styleSpecs, layerSpecs, routeSpecs, images) })
104
+ }
105
+ }
106
+ },
107
+ onFailure = { error -> finish(Result.failure(error)) },
108
+ )
109
+ }
110
+ requests += request
111
+ pendingImages += request
112
+ }
113
+ }
114
+
115
+ fun command(command: Map<String, Any?>): Map<String, Any?> =
116
+ when (command.string("type")) {
117
+ "setVisible" -> {
118
+ val targets = routeEntries(command.stringList("ids"))
119
+ val isVisible = command.boolean("visible")
120
+ targets.forEach { if (isVisible) it.show() else it.hide() }
121
+ none()
122
+ }
123
+
124
+ "setZIndex" -> {
125
+ route(command.string("id")).setZOrder(command.int("zIndex"))
126
+ none()
127
+ }
128
+
129
+ "changeData" -> {
130
+ val id = command.string("id")
131
+ val value = route(id)
132
+ val styleId = command.string("styleSetId")
133
+ val style = style(styleId)
134
+ val styleCount = styleCounts[styleId] ?: unknownRoute("route style set", styleId)
135
+ val segments = command.list("segments")
136
+ validateSegments(segments, styleCount)
137
+ invalidateProgressRequest(id)
138
+ val layer = value.layer ?: invalidRoute("Route $id has no layer.")
139
+ val isVisible = value.isShow
140
+ val options =
141
+ RouteLineOptions
142
+ .from(
143
+ "$nativePrefix-replacement-${replacementSequence++}",
144
+ segments.map { routeSegment(it, style) },
145
+ ).setStylesSet(style)
146
+ .setZOrder(value.zOrder)
147
+ .setVisible(false)
148
+ val replacement = layer.addRouteLine(options) ?: invalidRoute("Could not replace route $id.")
149
+ replacement.setProgress(value.progress)
150
+ stopAnimationsForRoute(id)
151
+ invalidateCachedAnimationsForRoute(id)
152
+ value.remove()
153
+ if (isVisible) replacement.show()
154
+ routes[id] = replacement
155
+ none()
156
+ }
157
+
158
+ "setProgress" -> {
159
+ val requested = command.double("progress")
160
+ val value = signedProgress(requested, command.string("mode"))
161
+ val id = command.string("id")
162
+ val target = route(id)
163
+ val duration = command.int("duration", 0)
164
+ if (duration < 0) invalidRoute("duration must be non-negative.")
165
+ requestedProgress[id] = requested.toFloat()
166
+ val sceneGeneration = generation
167
+ val requestEpoch = nextProgressEpoch(id)
168
+ if (duration == 0) {
169
+ target.setProgress(value)
170
+ emitRequestedProgress(id, requested, sceneGeneration, requestEpoch, target)
171
+ } else {
172
+ val managerReference = WeakReference(this)
173
+ target.progressTo(value, duration) { completedRoute, _ ->
174
+ managerReference.get()?.let { owner ->
175
+ if (completedRoute === target) {
176
+ owner.emitRequestedProgress(id, requested, sceneGeneration, requestEpoch, target)
177
+ }
178
+ }
179
+ }
180
+ }
181
+ none()
182
+ }
183
+
184
+ "getProgress" -> {
185
+ mapOf("type" to "progress", "progress" to route(command.string("id")).progress, "source" to "native")
186
+ }
187
+
188
+ "getRequestedProgress" -> {
189
+ val id = command.string("id")
190
+ route(id)
191
+ mapOf("type" to "progress", "progress" to (requestedProgress[id] ?: 0f), "source" to "requested")
192
+ }
193
+
194
+ "getProgressAtCoordinate" -> {
195
+ mapOf(
196
+ "type" to "progress",
197
+ "progress" to route(command.string("id")).getProgressFromPoint(command.coordinate("coordinate")),
198
+ "source" to "native",
199
+ )
200
+ }
201
+
202
+ "getCoordinateAtProgress" -> {
203
+ val point =
204
+ route(command.string("id")).getPointFromProgress(command.progress())
205
+ ?: invalidRoute("The SDK could not resolve this route progress.")
206
+ mapOf(
207
+ "type" to "coordinate",
208
+ "latitude" to point.point.latitude,
209
+ "longitude" to point.point.longitude,
210
+ "orientation" to point.orientation.toDouble(),
211
+ )
212
+ }
213
+
214
+ "getState" -> {
215
+ state(command.string("id"))
216
+ }
217
+
218
+ "getCounts" -> {
219
+ counts()
220
+ }
221
+
222
+ "getCapabilities" -> {
223
+ capabilities() + ("type" to "capabilities")
224
+ }
225
+
226
+ "startProgressAnimation" -> {
227
+ startAnimation(command.map("animation"))
228
+ none()
229
+ }
230
+
231
+ "stopAnimation" -> {
232
+ stopAnimation(command.string("animationId"), true)
233
+ none()
234
+ }
235
+
236
+ "remove" -> {
237
+ val ids = command.stringList("ids")
238
+ routeEntries(ids)
239
+ ids.forEach(::remove)
240
+ none()
241
+ }
242
+
243
+ else -> {
244
+ invalidRoute("Unknown route command type.")
245
+ }
246
+ }
247
+
248
+ fun dispose() {
249
+ generation++
250
+ rejectPendingApply("${ROUTE_ERROR}_DISPOSED", "The map view was disposed while applying the route scene.")
251
+ pendingImages.forEach(MapImageLoadToken::cancel)
252
+ pendingImages.clear()
253
+ disposeObjects()
254
+ styleSets.clear()
255
+ styleCounts.clear()
256
+ }
257
+
258
+ private fun rejectPendingApply(
259
+ code: String,
260
+ message: String,
261
+ ) {
262
+ val completion = pendingApply
263
+ pendingApply = null
264
+ completion?.invoke(Result.failure(CodedException(code, message, null)))
265
+ }
266
+
267
+ fun capabilities() =
268
+ mapOf(
269
+ "curves" to true,
270
+ "patterns" to true,
271
+ "patternSymbols" to true,
272
+ "patternImageSizing" to false,
273
+ "progressAtCoordinate" to true,
274
+ "coordinateAtProgress" to true,
275
+ "nativeProgressRead" to true,
276
+ "fractionalStyleWidth" to true,
277
+ "progressAnimationRepeat" to false,
278
+ )
279
+
280
+ private fun validateScene(
281
+ styleSpecs: List<Map<String, Any?>>,
282
+ layerSpecs: List<Map<String, Any?>>,
283
+ routeSpecs: List<Map<String, Any?>>,
284
+ ) {
285
+ requireUniqueRoute(styleSpecs, "style set")
286
+ requireUniqueRoute(layerSpecs, "route layer")
287
+ requireUniqueRoute(routeSpecs, "route")
288
+ layerSpecs.forEach { layer ->
289
+ layer.int("zIndex", 0)
290
+ layer.boolean("visible", true)
291
+ }
292
+ val styles = styleSpecs.associateBy { it.string("id") }
293
+ val layerIds = layerSpecs.mapTo(mutableSetOf()) { it.string("id") }
294
+ routeSpecs.forEach { route ->
295
+ route.int("zIndex", 0)
296
+ route.boolean("visible", true)
297
+ if (route.string("layerId") !in layerIds) invalidRoute("Route ${route.string("id")} references an unknown layer.")
298
+ val styleSpec = styles[route.string("styleSetId")] ?: invalidRoute("Route ${route.string("id")} references an unknown style set.")
299
+ val styleCount = styleSpec.list("styles").size
300
+ route.list("segments").forEach { segment ->
301
+ if (segment.coordinateList("coordinates").size < 2) invalidRoute("Route segments require at least two coordinates.")
302
+ if (segment.int("styleIndex", 0) !in 0 until styleCount) invalidRoute("Route segment styleIndex is out of range.")
303
+ routeCurve(segment)
304
+ }
305
+ if (route.list("segments").isEmpty()) invalidRoute("Routes require at least one segment.")
306
+ }
307
+ styleSpecs.forEach { spec ->
308
+ val patterns = spec.list("patterns")
309
+ val patternCount = patterns.size
310
+ patterns.forEach { pattern ->
311
+ val image = pattern.map("image")
312
+ if (image.string("uri").isBlank()) invalidRoute("Route pattern image URI must not be empty.")
313
+ rejectRouteImageSizing(image)
314
+ pattern.mapOrNull("symbol")?.let {
315
+ if (it.string("uri").isBlank()) invalidRoute("Route symbol image URI must not be empty.")
316
+ rejectRouteImageSizing(it)
317
+ }
318
+ if (pattern.float("distance") <= 0) invalidRoute("Route pattern distance must be positive.")
319
+ }
320
+ val styles = spec.list("styles")
321
+ if (styles.isEmpty()) invalidRoute("Route style sets require styles.")
322
+ styles.forEach { style -> if (style.list("levels").isEmpty()) invalidRoute("Route styles require levels.") }
323
+ styles.flatMap { it.list("levels") }.forEach { level ->
324
+ val index = level.int("patternIndex", -1)
325
+ if (index !in -1 until patternCount) invalidRoute("Route patternIndex is out of range.")
326
+ if (level.float("width") < 0 || level.float("strokeWidth", 0f) < 0) invalidRoute("Route widths must be non-negative.")
327
+ level.int("level")
328
+ level.color("color")
329
+ level.color("strokeColor", Color.TRANSPARENT)
330
+ }
331
+ }
332
+ }
333
+
334
+ private fun buildScene(
335
+ styleSpecs: List<Map<String, Any?>>,
336
+ layerSpecs: List<Map<String, Any?>>,
337
+ routeSpecs: List<Map<String, Any?>>,
338
+ images: Map<String, Bitmap>,
339
+ ): Map<String, Any> {
340
+ val nextStyleSets = mutableMapOf<String, RouteLineStylesSet>()
341
+ val nextStyleCounts = mutableMapOf<String, Int>()
342
+ val nextLayers = mutableMapOf<String, RouteLineLayer>()
343
+ val nextRoutes = mutableMapOf<String, RouteLine>()
344
+ val nextRouteLayerIds = mutableMapOf<String, String>()
345
+ registerStyles(styleSpecs, images, nextStyleSets, nextStyleCounts)
346
+ val sceneId = sceneSequence++
347
+ val scenePrefix = "$nativePrefix-scene-$sceneId"
348
+ try {
349
+ layerSpecs.forEach { spec ->
350
+ val id = spec.string("id")
351
+ val nativeId = "$scenePrefix-layer-$id"
352
+ val layer = manager.addLayer(nativeId, spec.int("zIndex", 0)) ?: invalidRoute("Could not create route layer $id.")
353
+ layer.setVisible(false)
354
+ nextLayers[id] = layer
355
+ }
356
+ routeSpecs.forEach { spec ->
357
+ val id = spec.string("id")
358
+ val styleId = spec.string("styleSetId")
359
+ val styleSet = nextStyleSets[styleId] ?: unknownRoute("route style set", styleId)
360
+ val options =
361
+ RouteLineOptions
362
+ .from(id, spec.list("segments").map { routeSegment(it, styleSet) })
363
+ .setStylesSet(styleSet)
364
+ .setZOrder(spec.int("zIndex", 0))
365
+ .setVisible(spec.boolean("visible", true))
366
+ val layerId = spec.string("layerId")
367
+ nextRoutes[id] = nextLayers[layerId]?.addRouteLine(options) ?: invalidRoute("Could not create route $id.")
368
+ nextRouteLayerIds[id] = layerId
369
+ }
370
+ } catch (error: Throwable) {
371
+ nextLayers.values.forEach(manager::remove)
372
+ throw error
373
+ }
374
+ disposeObjects()
375
+ styleSets.clear()
376
+ styleSets.putAll(nextStyleSets)
377
+ styleCounts.clear()
378
+ styleCounts.putAll(nextStyleCounts)
379
+ layers.putAll(nextLayers)
380
+ routes.putAll(nextRoutes)
381
+ routeLayerIds.putAll(nextRouteLayerIds)
382
+ requestedProgress.putAll(nextRoutes.keys.associateWith { 0f })
383
+ layerSpecs.forEach { spec -> nextLayers.getValue(spec.string("id")).setVisible(spec.boolean("visible", true)) }
384
+ return counts()
385
+ }
386
+
387
+ private fun registerStyles(
388
+ specs: List<Map<String, Any?>>,
389
+ images: Map<String, Bitmap>,
390
+ targetStyleSets: MutableMap<String, RouteLineStylesSet>,
391
+ targetStyleCounts: MutableMap<String, Int>,
392
+ ) {
393
+ specs.forEach { spec ->
394
+ val id = spec.string("id")
395
+ val nativeId = mapStyleNativeID("rnk-route-style", spec)
396
+ val patterns =
397
+ spec.list("patterns").map { pattern ->
398
+ val bitmap = images[pattern.map("image").string("uri")] ?: invalidRoute("Missing route pattern image.")
399
+ val symbol = pattern.mapOrNull("symbol")?.let { images[it.string("uri")] ?: invalidRoute("Missing route symbol image.") }
400
+ (
401
+ if (symbol ==
402
+ null
403
+ ) {
404
+ RouteLinePattern.from(bitmap, pattern.float("distance") * density)
405
+ } else {
406
+ RouteLinePattern.from(bitmap, symbol, pattern.float("distance") * density)
407
+ }
408
+ ).setPinStart(pattern.boolean("pinStart", false))
409
+ .setPinEnd(pattern.boolean("pinEnd", false))
410
+ }
411
+ val styles =
412
+ spec.list("styles").map { style ->
413
+ RouteLineStyles.from(
414
+ style.list("levels").map { level ->
415
+ val patternIndex = level.int("patternIndex", -1)
416
+ val width = level.float("width") * density
417
+ val color = level.color("color")
418
+ val strokeWidth = level.float("strokeWidth", 0f) * density
419
+ val strokeColor = level.color("strokeColor", Color.TRANSPARENT)
420
+ val pattern = patterns.getOrNull(patternIndex)
421
+ val routeStyle =
422
+ if (pattern == null) {
423
+ RouteLineStyle.from(width, color, strokeWidth, strokeColor)
424
+ } else {
425
+ RouteLineStyle.from(width, color, strokeWidth, strokeColor, pattern)
426
+ }
427
+ routeStyle.setZoomLevel(level.int("level"))
428
+ },
429
+ )
430
+ }
431
+ targetStyleSets[id] = manager.addStylesSet(RouteLineStylesSet.from(nativeId, styles, patterns))
432
+ targetStyleCounts[id] = styles.size
433
+ }
434
+ }
435
+
436
+ private fun routeSegment(
437
+ spec: Map<String, Any?>,
438
+ styleSet: RouteLineStylesSet,
439
+ ): RouteLineSegment {
440
+ val styleIndex = spec.int("styleIndex", 0)
441
+ val styles = styleSet.getStyles(styleIndex) ?: invalidRoute("Route segment styleIndex is out of range.")
442
+ return RouteLineSegment
443
+ .from(spec.coordinateList("coordinates"), styles)
444
+ .setCurveType(routeCurve(spec))
445
+ }
446
+
447
+ private fun routeCurve(spec: Map<String, Any?>) =
448
+ when (spec.stringOrNull("curve") ?: "none") {
449
+ "none" -> CurveType.None
450
+ "left" -> CurveType.LeftCurve
451
+ "right" -> CurveType.RightCurve
452
+ else -> invalidRoute("Unknown route curve type.")
453
+ }
454
+
455
+ private fun validateSegments(
456
+ segments: List<Map<String, Any?>>,
457
+ styleCount: Int,
458
+ ) {
459
+ if (segments.isEmpty()) invalidRoute("Routes require at least one segment.")
460
+ segments.forEach { segment ->
461
+ if (segment.coordinateList("coordinates").size < 2) invalidRoute("Route segments require at least two coordinates.")
462
+ if (segment.int("styleIndex", 0) !in 0 until styleCount) invalidRoute("Route segment styleIndex is out of range.")
463
+ routeCurve(segment)
464
+ }
465
+ }
466
+
467
+ private fun startAnimation(animation: Map<String, Any?>) {
468
+ val id = animation.string("animationId")
469
+ if (id.isBlank()) invalidRoute("Route animationId must not be empty.")
470
+ if (animators.containsKey(id)) invalidRoute("Route animation $id already exists.")
471
+ if (animation.int("repeat", 1) != 1) throw unsupportedRoute("The Kakao route progress animator does not support repeat counts.")
472
+ val duration = animation.int("duration")
473
+ if (duration <= 0) invalidRoute("Route animation duration must be positive.")
474
+ val routeIds = animation.stringList("routeIds")
475
+ if (routeIds.isEmpty()) invalidRoute("Route animation requires routeIds.")
476
+ val direction =
477
+ when (animation.string("direction")) {
478
+ "forward" -> ProgressDirection.StartFirst
479
+ "backward" -> ProgressDirection.EndFirst
480
+ else -> invalidRoute("Unknown route animation direction.")
481
+ }
482
+ val type =
483
+ when (animation.string("mode")) {
484
+ "fill" -> ProgressType.ToShow
485
+ "clear" -> ProgressType.ToHide
486
+ else -> invalidRoute("Unknown route animation mode.")
487
+ }
488
+ val requested = if (animation.string("mode") == "fill") 1f else 0f
489
+ val resetToInitial = animation.boolean("resetToInitial", false)
490
+ val targets = routeIds.map(::route)
491
+ val interpolation = routeInterpolation(animation.stringOrNull("interpolation") ?: "linear")
492
+ val hideAtStop = animation.boolean("hideAtStop", false)
493
+ val definition = mapCanonicalValue(animation - "animationId")
494
+ val nativeId =
495
+ animatorIdsByDefinition.getOrPut(definition) {
496
+ "rnk-route-${System.identityHashCode(this)}-${animationSequence++}"
497
+ }
498
+ if (nativeId in animationNativeIds.values) invalidRoute("An equivalent route animation is already running.")
499
+ val cachedAnimator = animatorCache[nativeId]
500
+ val animator =
501
+ cachedAnimator ?: run {
502
+ val effect =
503
+ ProgressAnimation
504
+ .from(nativeId, duration)
505
+ .setProgressDirection(direction)
506
+ .setProgressType(type)
507
+ .setInterpolation(interpolation)
508
+ .setHideAtStop(hideAtStop)
509
+ .setResetToInitialState(resetToInitial)
510
+ (manager.addAnimator(effect) ?: invalidRoute("Could not create route animation $id.")).also {
511
+ it.addRouteLines(*targets.toTypedArray())
512
+ animatorCache[nativeId] = it
513
+ animatorTargetIds[nativeId] = routeIds.toSet()
514
+ }
515
+ }
516
+ if (cachedAnimator != null && nativeId in animatorsNeedingTargets) {
517
+ animator.addRouteLines(*targets.toTypedArray())
518
+ animatorsNeedingTargets.remove(nativeId)
519
+ }
520
+ routeIds.forEach(::invalidateProgressRequest)
521
+ if (!resetToInitial) routeIds.forEach { requestedProgress[it] = requested }
522
+ animators[id] = animator
523
+ animationTargets[id] = routeIds.toSet()
524
+ animationNativeIds[id] = nativeId
525
+ val runEpoch = ++animationRunSequence
526
+ animationRunEpochs[id] = runEpoch
527
+ val managerReference = WeakReference(this)
528
+ animator.start { completedAnimator -> managerReference.get()?.completeAnimation(id, nativeId, runEpoch, completedAnimator) }
529
+ }
530
+
531
+ private fun stopAnimation(
532
+ id: String,
533
+ shouldStop: Boolean,
534
+ ) {
535
+ val animator = animators.remove(id) ?: unknownRoute("route animation", id)
536
+ animationTargets.remove(id)
537
+ val nativeId = animationNativeIds.remove(id)
538
+ animationRunEpochs.remove(id)
539
+ if (shouldStop) {
540
+ animator.stop()
541
+ nativeId?.let(animatorsNeedingTargets::add)
542
+ }
543
+ emit("onRouteAnimationStop", mapOf("animationId" to id))
544
+ }
545
+
546
+ private fun completeAnimation(
547
+ id: String,
548
+ nativeId: String,
549
+ runEpoch: Long,
550
+ completedAnimator: RouteLineAnimator,
551
+ ) {
552
+ if (animationNativeIds[id] != nativeId || animationRunEpochs[id] != runEpoch || animators[id] !== completedAnimator) return
553
+ animators.remove(id)
554
+ animationTargets.remove(id)
555
+ animationNativeIds.remove(id)
556
+ animationRunEpochs.remove(id)
557
+ emit("onRouteAnimationStop", mapOf("animationId" to id))
558
+ }
559
+
560
+ private fun state(id: String): Map<String, Any?> {
561
+ val value = route(id)
562
+ return mapOf(
563
+ "type" to "state",
564
+ "id" to id,
565
+ "layerId" to routeLayerIds.getValue(id),
566
+ "visible" to value.isShow,
567
+ "zIndex" to value.zOrder,
568
+ "nativeProgress" to value.progress,
569
+ "requestedProgress" to (requestedProgress[id] ?: 0f),
570
+ )
571
+ }
572
+
573
+ private fun counts() = mapOf("type" to "counts", "layers" to layers.size, "routes" to routes.size, "animations" to animators.size)
574
+
575
+ private fun none() = mapOf<String, Any?>("type" to "none")
576
+
577
+ private fun route(id: String) = routes[id] ?: unknownRoute("route", id)
578
+
579
+ private fun routeEntries(ids: List<String>): List<RouteLine> {
580
+ if (ids.toSet().size != ids.size) invalidRoute("Route command IDs must be unique.")
581
+ return ids.map(::route)
582
+ }
583
+
584
+ private fun style(id: String) = styleSets[id] ?: unknownRoute("route style set", id)
585
+
586
+ private fun remove(id: String) {
587
+ invalidateProgressRequest(id)
588
+ stopAnimationsForRoute(id)
589
+ invalidateCachedAnimationsForRoute(id)
590
+ routes.remove(id)?.remove() ?: unknownRoute("route", id)
591
+ requestedProgress.remove(id)
592
+ routeLayerIds.remove(id)
593
+ }
594
+
595
+ private fun stopAnimationsForRoute(id: String) {
596
+ animationTargets
597
+ .filterValues { id in it }
598
+ .keys
599
+ .toList()
600
+ .forEach { stopAnimation(it, true) }
601
+ }
602
+
603
+ private fun invalidateCachedAnimationsForRoute(id: String) {
604
+ animatorTargetIds
605
+ .filterValues { id in it }
606
+ .keys
607
+ .forEach { nativeId ->
608
+ animatorCache[nativeId]?.stop()
609
+ animatorsNeedingTargets += nativeId
610
+ }
611
+ }
612
+
613
+ private fun disposeObjects() {
614
+ animators.clear()
615
+ animationTargets.clear()
616
+ animationNativeIds.clear()
617
+ animationRunEpochs.clear()
618
+ animatorCache.values.forEach(RouteLineAnimator::stop)
619
+ animatorsNeedingTargets.addAll(animatorCache.keys)
620
+ layers.values.forEach(manager::remove)
621
+ layers.clear()
622
+ routes.clear()
623
+ requestedProgress.clear()
624
+ progressEpochs.clear()
625
+ routeLayerIds.clear()
626
+ }
627
+
628
+ private fun nextProgressEpoch(id: String): Long {
629
+ val epoch = (progressEpochs[id] ?: 0L) + 1
630
+ progressEpochs[id] = epoch
631
+ return epoch
632
+ }
633
+
634
+ private fun invalidateProgressRequest(id: String) {
635
+ nextProgressEpoch(id)
636
+ }
637
+
638
+ private fun emitRequestedProgress(
639
+ id: String,
640
+ requested: Double,
641
+ sceneGeneration: Int,
642
+ requestEpoch: Long,
643
+ target: RouteLine,
644
+ ) {
645
+ if (generation != sceneGeneration || progressEpochs[id] != requestEpoch || routes[id] !== target) return
646
+ emit("onRouteProgress", mapOf("id" to id, "progress" to requested, "source" to "requested"))
647
+ }
648
+ }
649
+
650
+ private const val ROUTE_ERROR = "ERR_KAKAO_MAP_ROUTE"
651
+
652
+ private fun signedProgress(
653
+ value: Double,
654
+ mode: String,
655
+ ): Float {
656
+ if (!value.isFinite() || value !in 0.0..1.0) invalidRoute("Route progress must be between 0 and 1.")
657
+ return when (mode) {
658
+ "fill" -> value.toFloat()
659
+ "clear" -> -value.toFloat()
660
+ else -> invalidRoute("Unknown route progress mode.")
661
+ }
662
+ }
663
+
664
+ private fun Map<String, Any?>.progress() =
665
+ double("progress")
666
+ .also {
667
+ if (it !in
668
+ 0.0..1.0
669
+ ) {
670
+ invalidRoute("Route progress must be between 0 and 1.")
671
+ }
672
+ }.toFloat()
673
+
674
+ private fun routeInterpolation(value: String) =
675
+ when (value) {
676
+ "linear" -> Interpolation.Linear
677
+ "cubicIn" -> Interpolation.CubicIn
678
+ "cubicOut" -> Interpolation.CubicOut
679
+ "cubicInOut" -> Interpolation.CubicInOut
680
+ else -> invalidRoute("Unknown interpolation $value.")
681
+ }
682
+
683
+ private fun rejectRouteImageSizing(source: Map<String, Any?>) {
684
+ if (source.containsKey("width") || source.containsKey("height")) {
685
+ throw unsupportedRoute("Kakao route patterns use the source bitmap size.")
686
+ }
687
+ }
688
+
689
+ private fun requireUniqueRoute(
690
+ values: List<Map<String, Any?>>,
691
+ noun: String,
692
+ ) {
693
+ val ids = values.map { it.string("id") }
694
+ if (ids.any(String::isBlank)) invalidRoute("$noun ID must not be empty.")
695
+ if (ids.toSet().size !=
696
+ ids.size
697
+ ) {
698
+ invalidRoute("Duplicate $noun ID.")
699
+ }
700
+ }
701
+
702
+ private fun Map<String, Any?>.map(key: String) = this[key] as? Map<String, Any?> ?: invalidRoute("$key must be an object.")
703
+
704
+ private fun Map<String, Any?>.mapOrNull(key: String): Map<String, Any?>? {
705
+ if (!containsKey(key)) return null
706
+ return this[key] as? Map<String, Any?> ?: invalidRoute("$key must be an object.")
707
+ }
708
+
709
+ private fun Map<String, Any?>.list(key: String): List<Map<String, Any?>> {
710
+ if (!containsKey(key)) return emptyList()
711
+ val raw = this[key] as? List<*> ?: invalidRoute("$key must be an array of objects.")
712
+ return raw.map { it as? Map<String, Any?> ?: invalidRoute("$key must contain objects.") }
713
+ }
714
+
715
+ private fun Map<String, Any?>.string(key: String) = this[key] as? String ?: invalidRoute("$key must be a string.")
716
+
717
+ private fun Map<String, Any?>.stringOrNull(key: String): String? {
718
+ if (!containsKey(key)) return null
719
+ return this[key] as? String ?: invalidRoute("$key must be a string.")
720
+ }
721
+
722
+ private fun Map<String, Any?>.stringList(key: String) =
723
+ (this[key] as? List<*>)?.map { it as? String ?: invalidRoute("$key must contain strings.") } ?: invalidRoute("$key must be an array.")
724
+
725
+ private fun Map<String, Any?>.boolean(
726
+ key: String,
727
+ fallback: Boolean? = null,
728
+ ): Boolean {
729
+ if (!containsKey(key)) return fallback ?: invalidRoute("$key must be a boolean.")
730
+ return this[key] as? Boolean ?: invalidRoute("$key must be a boolean.")
731
+ }
732
+
733
+ private fun Map<String, Any?>.double(
734
+ key: String,
735
+ fallback: Double? = null,
736
+ ): Double {
737
+ if (!containsKey(key)) return fallback ?: invalidRoute("$key must be finite.")
738
+ return (this[key] as? Number)?.toDouble()?.takeIf { it.isFinite() } ?: invalidRoute("$key must be finite.")
739
+ }
740
+
741
+ private fun Map<String, Any?>.float(
742
+ key: String,
743
+ fallback: Float? = null,
744
+ ) = double(key, fallback?.toDouble()).toFloat()
745
+
746
+ private fun Map<String, Any?>.int(
747
+ key: String,
748
+ fallback: Int? = null,
749
+ ): Int {
750
+ val value = double(key, fallback?.toDouble())
751
+ if (value % 1 != 0.0 ||
752
+ value !in Int.MIN_VALUE.toDouble()..Int.MAX_VALUE.toDouble()
753
+ ) {
754
+ invalidRoute("$key must be an integer.")
755
+ }
756
+ return value.toInt()
757
+ }
758
+
759
+ private fun Map<String, Any?>.color(
760
+ key: String,
761
+ fallback: Int? = null,
762
+ ): Int {
763
+ if (!containsKey(key)) return fallback ?: invalidRoute("$key must be a color.")
764
+ return (this[key] as? String)?.let(::parseRouteColor) ?: invalidRoute("$key must be a color.")
765
+ }
766
+
767
+ private fun Map<String, Any?>.coordinate(key: String) = map(key).coordinateValue()
768
+
769
+ private fun Map<String, Any?>.coordinateList(key: String) =
770
+ (this[key] as? List<*>)?.map { it.coordinateValue() } ?: invalidRoute("$key must be an array.")
771
+
772
+ private fun Any?.coordinateValue(): LatLng {
773
+ val value =
774
+ this as? Map<String, Any?> ?: invalidRoute("Coordinate must be an object.")
775
+ val latitude = value.double("latitude")
776
+ val longitude = value.double("longitude")
777
+ if (latitude !in
778
+ -90.0..90.0 ||
779
+ longitude !in -180.0..180.0
780
+ ) {
781
+ invalidRoute("Invalid coordinate.")
782
+ }
783
+ return LatLng.from(latitude, longitude)
784
+ }
785
+
786
+ private fun parseRouteColor(value: String): Int {
787
+ if (!Regex("^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$").matches(value)) invalidRoute("Colors must be #RRGGBB or #RRGGBBAA.")
788
+ val raw = value.drop(1)
789
+ return if (raw.length ==
790
+ 6
791
+ ) {
792
+ Color.parseColor(value)
793
+ } else {
794
+ ((raw.takeLast(2).toLong(16) shl 24) or raw.dropLast(2).toLong(16)).toInt()
795
+ }
796
+ }
797
+
798
+ private fun invalidRoute(message: String): Nothing = throw CodedException("${ROUTE_ERROR}_INVALID_ARGUMENT", message, null)
799
+
800
+ private fun unsupportedRoute(message: String): CodedException = CodedException("${ROUTE_ERROR}_UNSUPPORTED", message, null)
801
+
802
+ private fun unknownRoute(
803
+ noun: String,
804
+ id: String,
805
+ ): Nothing = throw CodedException("${ROUTE_ERROR}_NOT_FOUND", "Unknown $noun $id.", null)