@react-native-kakao/map 2.2.7 → 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,903 @@
1
+ import CoreFoundation
2
+ import KakaoMapsSDK
3
+ import UIKit
4
+
5
+ final class MapRoutes {
6
+ private let imageLoader: MapImageLoader
7
+ private let emit: (String, [String: Any]) -> Void
8
+ private let manager: RouteManager
9
+ private var layers: [String: RouteLayer] = [:]
10
+ private var routes: [String: Route] = [:]
11
+ private var styles: [String: RouteStyleSet] = [:]
12
+ private var styleCounts: [String: Int] = [:]
13
+ private var nativeStyleIDs: [String: String] = [:]
14
+ private var nativeLayerIDs: [String: String] = [:]
15
+ private var routeLayerIDs: [String: String] = [:]
16
+ private var animators: [String: RouteAnimator] = [:]
17
+ private var animationTargets: [String: Set<String>] = [:]
18
+ private var animationNativeIDs: [String: String] = [:]
19
+ private var animationRunEpochs: [String: UInt64] = [:]
20
+ private var progress: [String: Float] = [:]
21
+ private var progressEpochs: [String: UInt64] = [:]
22
+ private var imageTokens: [MapImageLoadToken] = []
23
+ private var pendingApply: ((Result<[String: Any], Error>) -> Void)?
24
+ private var generation = 0
25
+ private var sceneSequence = 0
26
+ private var animationSequence: UInt64 = 0
27
+
28
+ init(
29
+ map: KakaoMap,
30
+ imageLoader: MapImageLoader,
31
+ emit: @escaping (String, [String: Any]) -> Void
32
+ ) {
33
+ self.imageLoader = imageLoader
34
+ self.emit = emit
35
+ manager = map.getRouteManager()
36
+ }
37
+
38
+ func apply(
39
+ _ scene: [String: Any],
40
+ completion: @escaping (Result<[String: Any], Error>) -> Void
41
+ ) throws {
42
+ rejectPendingApply(
43
+ code: "ERR_KAKAO_MAP_ROUTE_SUPERSEDED",
44
+ message: "The route scene was superseded by a newer update."
45
+ )
46
+ cancelImages()
47
+ generation += 1
48
+ let token = generation
49
+ let styleSpecs = try scene.objectList("styleSets")
50
+ let layerSpecs = try scene.objectList("layers")
51
+ let routeSpecs = try scene.objectList("routes")
52
+ try validateScene(styles: styleSpecs, layers: layerSpecs, routes: routeSpecs)
53
+ pendingApply = completion
54
+ let uris = try Set(styleSpecs.flatMap { style in
55
+ try style.objectList("patterns").flatMap { pattern in
56
+ try [pattern.object("image").string("uri"), pattern.optionalObject("symbol")?.string("uri")]
57
+ .compactMap { $0 }
58
+ }
59
+ })
60
+ guard !uris.isEmpty else {
61
+ let result = Result { try buildScene(
62
+ styles: styleSpecs,
63
+ layers: layerSpecs,
64
+ routes: routeSpecs,
65
+ images: [:]
66
+ ) }
67
+ pendingApply = nil
68
+ completion(result)
69
+ return
70
+ }
71
+ var images: [String: UIImage] = [:]
72
+ var didFinish = false
73
+ let finish: (Result<[String: Any], Error>) -> Void = { [weak self] result in
74
+ guard let self, !didFinish, token == generation else { return }
75
+ didFinish = true
76
+ if case .failure = result {
77
+ imageTokens.forEach { $0.cancel() }
78
+ }
79
+ imageTokens.removeAll()
80
+ pendingApply = nil
81
+ completion(result)
82
+ }
83
+ for uri in uris {
84
+ imageTokens.append(imageLoader.load(uri) { [weak self] result in
85
+ guard let self, token == generation else { return }
86
+ switch result {
87
+ case let .success(image):
88
+ images[uri] = image
89
+ if images.count == uris.count {
90
+ finish(Result { try self.buildScene(
91
+ styles: styleSpecs,
92
+ layers: layerSpecs,
93
+ routes: routeSpecs,
94
+ images: images
95
+ ) })
96
+ }
97
+ case let .failure(error):
98
+ finish(.failure(error))
99
+ }
100
+ })
101
+ }
102
+ }
103
+
104
+ func command(_ command: [String: Any]) throws -> [String: Any] {
105
+ switch try command.string("type") {
106
+ case "setVisible":
107
+ let visible = try command.bool("visible")
108
+ let targets = try routeEntries(command.stringList("ids"))
109
+ targets.forEach { visible ? $0.show() : $0.hide() }
110
+ return none()
111
+ case "setZIndex":
112
+ try route(command.string("id")).zOrder = command.int("zIndex")
113
+ return none()
114
+ case "changeData":
115
+ let id = try command.string("id")
116
+ let styleID = try command.string("styleSetId")
117
+ guard styles[styleID] != nil else { throw routeNotFound("route style set", id: styleID) }
118
+ guard let nativeStyleID = nativeStyleIDs[styleID]
119
+ else { throw routeNotFound("route style set", id: styleID) }
120
+ let segmentSpecs = try command.objectList("segments")
121
+ try validateSegments(segmentSpecs, styleCount: styleCounts[styleID] ?? 0)
122
+ let target = try route(id)
123
+ invalidateProgressRequest(id)
124
+ try target.changeStyleAndData(
125
+ styleID: nativeStyleID,
126
+ segments: segmentSpecs.map(routeSegment)
127
+ )
128
+ return none()
129
+ case "setProgress":
130
+ let id = try command.string("id")
131
+ let value = try routeProgress(command.double("progress"))
132
+ let mode = try progressType(command.string("mode"))
133
+ let duration = try command.nonnegativeInt("duration", default: 0)
134
+ let target = try route(id)
135
+ progress[id] = value
136
+ let sceneGeneration = generation
137
+ let requestEpoch = nextProgressEpoch(id)
138
+ if duration == 0 {
139
+ target.setProgress(progress: value, type: mode, duration: 0, callback: nil)
140
+ emitRequestedProgress(
141
+ id: id,
142
+ progress: value,
143
+ sceneGeneration: sceneGeneration,
144
+ requestEpoch: requestEpoch,
145
+ target: target
146
+ )
147
+ } else {
148
+ target
149
+ .setProgress(
150
+ progress: value,
151
+ type: mode,
152
+ duration: UInt(duration)
153
+ ) { [weak self] completedRoute in
154
+ guard completedRoute == nil || completedRoute === target else { return }
155
+ self?.emitRequestedProgress(
156
+ id: id,
157
+ progress: value,
158
+ sceneGeneration: sceneGeneration,
159
+ requestEpoch: requestEpoch,
160
+ target: target
161
+ )
162
+ }
163
+ }
164
+ return none()
165
+ case "getProgress":
166
+ throw routeUnsupported("iOS Kakao Maps does not expose fields from RouteProgressInfo.")
167
+ case "getRequestedProgress":
168
+ let id = try command.string("id")
169
+ _ = try route(id)
170
+ return ["type": "progress", "progress": progress[id] ?? 0, "source": "requested"]
171
+ case "getProgressAtCoordinate":
172
+ return try [
173
+ "type": "progress",
174
+ "source": "native",
175
+ "progress": route(command.string("id"))
176
+ .getProgressAlongRouteLine(position: command.coordinate("coordinate"))
177
+ ]
178
+ case "getCoordinateAtProgress":
179
+ throw routeUnsupported(
180
+ "iOS Kakao Maps does not expose fields from RoutePointInfo."
181
+ )
182
+ case "getState":
183
+ return try state(command.string("id"))
184
+ case "getCounts":
185
+ return counts()
186
+ case "getCapabilities":
187
+ return capabilities().merging(["type": "capabilities"]) { _, new in new }
188
+ case "startProgressAnimation":
189
+ try startAnimation(command.object("animation"))
190
+ return none()
191
+ case "stopAnimation":
192
+ try stopAnimation(command.string("animationId"), emitEvent: true)
193
+ return none()
194
+ case "remove":
195
+ let ids = try command.stringList("ids")
196
+ _ = try routeEntries(ids)
197
+ try ids.forEach(remove)
198
+ return none()
199
+ default:
200
+ throw routeInvalid("Unknown route command type.")
201
+ }
202
+ }
203
+
204
+ func dispose() {
205
+ generation += 1
206
+ rejectPendingApply(
207
+ code: "ERR_KAKAO_MAP_ROUTE_DISPOSED",
208
+ message: "The map view was disposed while applying the route scene."
209
+ )
210
+ cancelImages()
211
+ disposeObjects()
212
+ styles.removeAll()
213
+ styleCounts.removeAll()
214
+ nativeStyleIDs.removeAll()
215
+ nativeLayerIDs.removeAll()
216
+ routeLayerIDs.removeAll()
217
+ }
218
+
219
+ private func rejectPendingApply(code: String, message: String) {
220
+ let completion = pendingApply
221
+ pendingApply = nil
222
+ completion?(.failure(KakaoMapException(code, message)))
223
+ }
224
+
225
+ func capabilities() -> [String: Any] {
226
+ [
227
+ "curves": true,
228
+ "patterns": true,
229
+ "patternSymbols": true,
230
+ "patternImageSizing": false,
231
+ "progressAtCoordinate": true,
232
+ "coordinateAtProgress": false,
233
+ "nativeProgressRead": false,
234
+ "fractionalStyleWidth": false,
235
+ "progressAnimationRepeat": false
236
+ ]
237
+ }
238
+
239
+ private func validateScene(
240
+ styles styleSpecs: [[String: Any]],
241
+ layers layerSpecs: [[String: Any]],
242
+ routes routeSpecs: [[String: Any]]
243
+ ) throws {
244
+ try requireUniqueRoute(styleSpecs, noun: "style set")
245
+ try requireUniqueRoute(layerSpecs, noun: "route layer")
246
+ try requireUniqueRoute(routeSpecs, noun: "route")
247
+ for layer in layerSpecs {
248
+ _ = try layer.int("zIndex", default: 0)
249
+ _ = try layer.bool("visible", default: true)
250
+ }
251
+ let styles = try Dictionary(uniqueKeysWithValues: styleSpecs.map { try ($0.string("id"), $0) })
252
+ let layerIDs = try Set(layerSpecs.map { try $0.string("id") })
253
+ for route in routeSpecs {
254
+ _ = try route.int("zIndex", default: 0)
255
+ _ = try route.bool("visible", default: true)
256
+ let id = try route.string("id")
257
+ guard try layerIDs.contains(route.string("layerId"))
258
+ else { throw routeInvalid("Route \(id) references an unknown layer.") }
259
+ let styleID = try route.string("styleSetId")
260
+ guard let style = styles[styleID]
261
+ else { throw routeInvalid("Route \(id) references an unknown style set.") }
262
+ let styleCount = try style.objectList("styles").count
263
+ let segments = try route.objectList("segments")
264
+ guard !segments.isEmpty else { throw routeInvalid("Routes require at least one segment.") }
265
+ for segment in segments {
266
+ guard try segment.coordinateList("coordinates").count >= 2
267
+ else { throw routeInvalid("Route segments require at least two coordinates.") }
268
+ guard try segment.nonnegativeInt("styleIndex", default: 0) < styleCount
269
+ else { throw routeInvalid("Route segment styleIndex is out of range.") }
270
+ _ = try routeSegment(segment)
271
+ }
272
+ }
273
+ for style in styleSpecs {
274
+ let patterns = try style.objectList("patterns")
275
+ let patternCount = patterns.count
276
+ for pattern in patterns {
277
+ let image = try pattern.object("image")
278
+ guard try !image.string("uri").isEmpty
279
+ else { throw routeInvalid("Route pattern image URI must not be empty.") }
280
+ try rejectRouteImageSizing(image)
281
+ if let symbol = try pattern.optionalObject("symbol") {
282
+ guard try !symbol.string("uri").isEmpty
283
+ else { throw routeInvalid("Route symbol image URI must not be empty.") }
284
+ try rejectRouteImageSizing(symbol)
285
+ }
286
+ guard try pattern.nonnegativeDouble("distance") > 0
287
+ else { throw routeInvalid("Route pattern distance must be positive.") }
288
+ }
289
+ let routeStyles = try style.objectList("styles")
290
+ guard !routeStyles.isEmpty else { throw routeInvalid("Route style sets require styles.") }
291
+ for routeStyle in routeStyles {
292
+ guard try !routeStyle.objectList("levels").isEmpty
293
+ else { throw routeInvalid("Route styles require levels.") }
294
+ }
295
+ for level in try routeStyles.flatMap({ try $0.objectList("levels") }) {
296
+ let index = try level.int("patternIndex", default: -1)
297
+ guard (-1 ..< patternCount).contains(index)
298
+ else { throw routeInvalid("Route patternIndex is out of range.") }
299
+ _ = try level.nonnegativeInt("width")
300
+ _ = try level.nonnegativeInt("strokeWidth", default: 0)
301
+ _ = try level.int("level")
302
+ _ = try level.color("color")
303
+ _ = try level.color("strokeColor", default: .clear)
304
+ }
305
+ }
306
+ }
307
+
308
+ private func buildScene(
309
+ styles styleSpecs: [[String: Any]],
310
+ layers layerSpecs: [[String: Any]],
311
+ routes routeSpecs: [[String: Any]],
312
+ images: [String: UIImage]
313
+ ) throws -> [String: Any] {
314
+ var nextStyles: [String: RouteStyleSet] = [:]
315
+ var nextStyleCounts: [String: Int] = [:]
316
+ var nextNativeStyleIDs: [String: String] = [:]
317
+ var nextLayers: [String: RouteLayer] = [:]
318
+ var nextNativeLayerIDs: [String: String] = [:]
319
+ var nextRoutes: [String: Route] = [:]
320
+ var nextRouteLayerIDs: [String: String] = [:]
321
+ try registerStyles(
322
+ styleSpecs,
323
+ images: images,
324
+ styles: &nextStyles,
325
+ styleCounts: &nextStyleCounts,
326
+ nativeStyleIDs: &nextNativeStyleIDs
327
+ )
328
+ let scenePrefix = "rnk-routes-\(ObjectIdentifier(self).hashValue)-scene-\(sceneSequence)"
329
+ sceneSequence += 1
330
+ do {
331
+ for spec in layerSpecs {
332
+ let id = try spec.string("id")
333
+ let nativeID = "\(scenePrefix)-layer-\(id)"
334
+ guard let layer = try manager.addRouteLayer(
335
+ layerID: nativeID,
336
+ zOrder: spec.int("zIndex", default: 0)
337
+ ) else {
338
+ throw routeInvalid("Could not create route layer \(id).")
339
+ }
340
+ layer.visible = false
341
+ nextLayers[id] = layer
342
+ nextNativeLayerIDs[id] = nativeID
343
+ }
344
+ for spec in routeSpecs {
345
+ let id = try spec.string("id")
346
+ let styleID = try spec.string("styleSetId")
347
+ guard let nativeStyleID = nextNativeStyleIDs[styleID]
348
+ else { throw routeNotFound("route style set", id: styleID) }
349
+ let options = try RouteOptions(
350
+ routeID: id,
351
+ styleID: nativeStyleID,
352
+ zOrder: spec.int("zIndex", default: 0)
353
+ )
354
+ options.segments = try spec.objectList("segments").map(routeSegment)
355
+ let layerID = try spec.string("layerId")
356
+ guard let value = nextLayers[layerID]?.addRoute(option: options) else {
357
+ throw routeInvalid("Could not create route \(id).")
358
+ }
359
+ if try spec.bool("visible", default: true) {
360
+ value.show()
361
+ } else {
362
+ value.hide()
363
+ }
364
+ nextRoutes[id] = value
365
+ nextRouteLayerIDs[id] = layerID
366
+ }
367
+ } catch {
368
+ nextNativeLayerIDs.values.forEach { manager.removeRouteLayer(layerID: $0) }
369
+ throw error
370
+ }
371
+ disposeObjects()
372
+ styles = nextStyles
373
+ styleCounts = nextStyleCounts
374
+ nativeStyleIDs = nextNativeStyleIDs
375
+ layers = nextLayers
376
+ nativeLayerIDs = nextNativeLayerIDs
377
+ routes = nextRoutes
378
+ routeLayerIDs = nextRouteLayerIDs
379
+ progress = Dictionary(uniqueKeysWithValues: nextRoutes.keys.map { ($0, 0) })
380
+ for spec in layerSpecs {
381
+ try nextLayers[spec.string("id")]?.visible = try spec.bool("visible", default: true)
382
+ }
383
+ return counts()
384
+ }
385
+
386
+ private func registerStyles(
387
+ _ specs: [[String: Any]],
388
+ images: [String: UIImage],
389
+ styles: inout [String: RouteStyleSet],
390
+ styleCounts: inout [String: Int],
391
+ nativeStyleIDs: inout [String: String]
392
+ ) throws {
393
+ for spec in specs {
394
+ let id = try spec.string("id")
395
+ let nativeID = mapStyleNativeID("rnk-route-style", spec)
396
+ let patterns = try spec.objectList("patterns").map { pattern -> RoutePattern in
397
+ let imageURI = try pattern.object("image").string("uri")
398
+ guard let image = images[imageURI]
399
+ else { throw routeInvalid("Missing route pattern image.") }
400
+ let symbol = try pattern.optionalObject("symbol").map { source -> UIImage in
401
+ let uri = try source.string("uri")
402
+ guard let result = images[uri] else { throw routeInvalid("Missing route symbol image.") }
403
+ return result
404
+ }
405
+ return try RoutePattern(
406
+ pattern: image,
407
+ distance: routePixelFloat(pattern.nonnegativeDouble("distance")),
408
+ symbol: symbol,
409
+ pinStart: pattern.bool("pinStart", default: false),
410
+ pinEnd: pattern.bool("pinEnd", default: false)
411
+ )
412
+ }
413
+ let routeStyles = try spec.objectList("styles").map { style in
414
+ try RouteStyle(styles: style.objectList("levels").map { level in
415
+ try PerLevelRouteStyle(
416
+ width: routePixelUInt(level.nonnegativeInt("width")),
417
+ color: level.color("color"),
418
+ strokeWidth: routePixelUInt(level.nonnegativeInt("strokeWidth", default: 0)),
419
+ strokeColor: level.color("strokeColor", default: .clear),
420
+ level: level.int("level"),
421
+ patternIndex: level.int("patternIndex", default: -1)
422
+ )
423
+ })
424
+ }
425
+ let styleSet = RouteStyleSet(styleID: nativeID, styles: routeStyles)
426
+ patterns.forEach(styleSet.addPattern)
427
+ manager.addRouteStyleSet(styleSet)
428
+ styles[id] = styleSet
429
+ styleCounts[id] = routeStyles.count
430
+ nativeStyleIDs[id] = nativeID
431
+ }
432
+ }
433
+
434
+ private func routeSegment(_ spec: [String: Any]) throws -> RouteSegment {
435
+ try RouteSegment(
436
+ points: routePoints(spec),
437
+ styleIndex: UInt(spec.nonnegativeInt("styleIndex", default: 0))
438
+ )
439
+ }
440
+
441
+ private func validateSegments(_ segments: [[String: Any]], styleCount: Int) throws {
442
+ guard !segments.isEmpty else { throw routeInvalid("Routes require at least one segment.") }
443
+ for segment in segments {
444
+ guard try segment.coordinateList("coordinates").count >= 2
445
+ else { throw routeInvalid("Route segments require at least two coordinates.") }
446
+ guard try segment.nonnegativeInt("styleIndex", default: 0) < styleCount
447
+ else { throw routeInvalid("Route segment styleIndex is out of range.") }
448
+ _ = try routeSegment(segment)
449
+ }
450
+ }
451
+
452
+ private func routePoints(_ spec: [String: Any]) throws -> [MapPoint] {
453
+ let coordinates = try spec.coordinateList("coordinates")
454
+ let curve = try spec.optionalString("curve") ?? "none"
455
+ let points: [MapPoint]
456
+ switch curve {
457
+ case "none": points = coordinates
458
+ case "left",
459
+ "right":
460
+ points = coordinates.indices.dropLast().flatMap { index -> [MapPoint] in
461
+ let part = Primitives.getCurvePoints(
462
+ startPoint: coordinates[index],
463
+ endPoint: coordinates[index + 1],
464
+ isLeft: curve == "left"
465
+ )
466
+ return index == 0 ? part : Array(part.dropFirst())
467
+ }
468
+ default: throw routeInvalid("Unknown route curve type.")
469
+ }
470
+ return points
471
+ }
472
+
473
+ private func startAnimation(_ animation: [String: Any]) throws {
474
+ let id = try animation.string("animationId")
475
+ guard !id.isEmpty else { throw routeInvalid("Route animationId must not be empty.") }
476
+ guard animators[id] == nil else { throw routeInvalid("Route animation \(id) already exists.") }
477
+ guard try animation.minimumInt("repeat", default: 1, minimum: 1) == 1 else {
478
+ throw routeUnsupported("The Kakao route progress animator does not support repeat counts.")
479
+ }
480
+ let duration = try animation.minimumInt("duration", minimum: 1)
481
+ let direction: ProgressDirection
482
+ switch try animation.string("direction") {
483
+ case "forward": direction = .forward
484
+ case "backward": direction = .backward
485
+ default: throw routeInvalid("Unknown route animation direction.")
486
+ }
487
+ let routeIDs = try animation.stringList("routeIds")
488
+ guard !routeIDs.isEmpty else { throw routeInvalid("Route animation requires routeIds.") }
489
+ let mode = try animation.string("mode")
490
+ let resetToInitial = try animation.bool("resetToInitial", default: false)
491
+ let targets = try routeIDs.map(route)
492
+ let effect = try ProgressAnimationEffect(
493
+ direction: direction,
494
+ type: progressType(mode)
495
+ )
496
+ effect.interpolation = try routeInterpolation(
497
+ duration: duration,
498
+ method: animation.optionalString("interpolation") ?? "linear"
499
+ )
500
+ effect.hideAtStop = try animation.bool("hideAtStop", default: false)
501
+ effect.resetToInitialState = resetToInitial
502
+ animationSequence &+= 1
503
+ let runEpoch = animationSequence
504
+ let nativeID = "rnk-route-\(ObjectIdentifier(self).hashValue)-\(runEpoch)"
505
+ guard let animator = manager.addRouteAnimator(animatorID: nativeID, effect: effect) else {
506
+ throw routeInvalid("Could not create route animation \(id).")
507
+ }
508
+ routeIDs.forEach(invalidateProgressRequest)
509
+ for (index, routeID) in routeIDs.enumerated() {
510
+ animator.addRoute(targets[index])
511
+ if !resetToInitial {
512
+ progress[routeID] = mode == "fill" ? 1 : 0
513
+ }
514
+ }
515
+ animator.setStopCallback { [weak self, weak animator] _ in
516
+ guard let self,
517
+ let animator,
518
+ animationNativeIDs[id] == nativeID,
519
+ animationRunEpochs[id] == runEpoch,
520
+ animators[id] === animator
521
+ else { return }
522
+ animators.removeValue(forKey: id)
523
+ animationTargets.removeValue(forKey: id)
524
+ animationNativeIDs.removeValue(forKey: id)
525
+ animationRunEpochs.removeValue(forKey: id)
526
+ manager.removeRouteAnimator(animatorID: nativeID)
527
+ emit("onRouteAnimationStop", ["animationId": id])
528
+ }
529
+ animators[id] = animator
530
+ animationTargets[id] = Set(routeIDs)
531
+ animationNativeIDs[id] = nativeID
532
+ animationRunEpochs[id] = runEpoch
533
+ animator.start()
534
+ }
535
+
536
+ private func stopAnimation(_ id: String, emitEvent: Bool) throws {
537
+ guard let animator = animators.removeValue(forKey: id) else { throw routeNotFound(
538
+ "route animation",
539
+ id: id
540
+ ) }
541
+ animationTargets.removeValue(forKey: id)
542
+ guard let nativeID = animationNativeIDs.removeValue(forKey: id)
543
+ else { throw routeNotFound("route animation", id: id) }
544
+ animationRunEpochs.removeValue(forKey: id)
545
+ animator.setStopCallback(nil)
546
+ animator.stop()
547
+ manager.removeRouteAnimator(animatorID: nativeID)
548
+ if emitEvent {
549
+ emit("onRouteAnimationStop", ["animationId": id])
550
+ }
551
+ }
552
+
553
+ private func state(_ id: String) throws -> [String: Any] {
554
+ let value = try route(id)
555
+ guard let layerID = routeLayerIDs[id]
556
+ else { throw routeNotFound("route layer", id: "") }
557
+ return [
558
+ "type": "state",
559
+ "id": id,
560
+ "layerId": layerID,
561
+ "visible": value.isShow,
562
+ "zIndex": value.zOrder,
563
+ "nativeProgress": NSNull(),
564
+ "requestedProgress": progress[id] ?? 0
565
+ ]
566
+ }
567
+
568
+ private func counts() -> [String: Any] {
569
+ [
570
+ "type": "counts",
571
+ "layers": layers.count,
572
+ "routes": routes.count,
573
+ "animations": animators.count
574
+ ]
575
+ }
576
+
577
+ private func none() -> [String: Any] {
578
+ ["type": "none"]
579
+ }
580
+
581
+ private func route(_ id: String) throws -> Route {
582
+ guard let value = routes[id]
583
+ else { throw routeNotFound(
584
+ "route",
585
+ id: id
586
+ ) }; return value
587
+ }
588
+
589
+ private func routeEntries(_ ids: [String]) throws -> [Route] {
590
+ guard Set(ids).count == ids.count
591
+ else { throw routeInvalid("Route command IDs must be unique.") }
592
+ return try ids.map(route)
593
+ }
594
+
595
+ private func remove(_ id: String) throws {
596
+ invalidateProgressRequest(id)
597
+ for animationID in animationTargets.filter({ $0.value.contains(id) }).map(\.key) {
598
+ try stopAnimation(animationID, emitEvent: true)
599
+ }
600
+ let value = try route(id)
601
+ guard let layerID = routeLayerIDs[id], let layer = layers[layerID] else { throw routeNotFound(
602
+ "route layer",
603
+ id: routeLayerIDs[id] ?? ""
604
+ ) }
605
+ layer.removeRoute(routeID: value.routeID)
606
+ routes.removeValue(forKey: id)
607
+ progress.removeValue(forKey: id)
608
+ routeLayerIDs.removeValue(forKey: id)
609
+ }
610
+
611
+ private func cancelImages() {
612
+ imageTokens.forEach { $0.cancel() }
613
+ imageTokens.removeAll()
614
+ }
615
+
616
+ private func disposeObjects() {
617
+ let activeAnimators = animators
618
+ let activeNativeIDs = animationNativeIDs
619
+ animators.removeAll()
620
+ animationTargets.removeAll()
621
+ animationNativeIDs.removeAll()
622
+ animationRunEpochs.removeAll()
623
+ for (id, animator) in activeAnimators {
624
+ animator.setStopCallback(nil)
625
+ animator.stop()
626
+ if let nativeID = activeNativeIDs[id] {
627
+ manager.removeRouteAnimator(animatorID: nativeID)
628
+ }
629
+ }
630
+ nativeLayerIDs.values.forEach { manager.removeRouteLayer(layerID: $0) }
631
+ layers.removeAll()
632
+ nativeLayerIDs.removeAll()
633
+ routes.removeAll()
634
+ routeLayerIDs.removeAll()
635
+ progress.removeAll()
636
+ progressEpochs.removeAll()
637
+ }
638
+
639
+ @discardableResult
640
+ private func nextProgressEpoch(_ id: String) -> UInt64 {
641
+ let epoch = (progressEpochs[id] ?? 0) &+ 1
642
+ progressEpochs[id] = epoch
643
+ return epoch
644
+ }
645
+
646
+ private func invalidateProgressRequest(_ id: String) {
647
+ nextProgressEpoch(id)
648
+ }
649
+
650
+ private func emitRequestedProgress(
651
+ id: String,
652
+ progress: Float,
653
+ sceneGeneration: Int,
654
+ requestEpoch: UInt64,
655
+ target: Route
656
+ ) {
657
+ guard generation == sceneGeneration,
658
+ progressEpochs[id] == requestEpoch,
659
+ routes[id] === target
660
+ else { return }
661
+ emit("onRouteProgress", ["id": id, "progress": progress, "source": "requested"])
662
+ }
663
+ }
664
+
665
+ private func routeProgress(_ value: Double) throws -> Float {
666
+ guard value.isFinite,
667
+ (0 ... 1).contains(value)
668
+ else { throw routeInvalid("Route progress must be between 0 and 1.") }
669
+ return Float(value)
670
+ }
671
+
672
+ private func progressType(_ value: String) throws -> RouteProgressType {
673
+ switch value {
674
+ case "fill": .fillFromStart; case "clear": .clearFromStart; default: throw routeInvalid(
675
+ "Unknown route progress mode."
676
+ )
677
+ }
678
+ }
679
+
680
+ private func routeInterpolation(
681
+ duration: Int,
682
+ method value: String
683
+ ) throws -> AnimationInterpolation {
684
+ let method: InterpolationMethodType
685
+ switch value {
686
+ case "linear": method = .linear; case "cubicIn": method = .cubicIn; case "cubicOut": method =
687
+ .cubicOut; case "cubicInOut": method =
688
+ .cubicInOut; default: throw routeInvalid("Unknown interpolation \(value).")
689
+ }
690
+ return AnimationInterpolation(duration: UInt32(duration), method: method)
691
+ }
692
+
693
+ private func rejectRouteImageSizing(_ source: [String: Any]) throws {
694
+ guard source["width"] == nil, source["height"] == nil
695
+ else { throw routeUnsupported("Kakao route patterns use the source bitmap size.") }
696
+ }
697
+
698
+ private func requireUniqueRoute(_ values: [[String: Any]], noun: String) throws {
699
+ let ids = try values.map { try $0.string("id") }; guard Set(ids).count == ids.count
700
+ else { throw routeInvalid("Duplicate \(noun) ID.") }
701
+ guard !ids.contains(where: \.isEmpty)
702
+ else { throw routeInvalid("\(noun) ID must not be empty.") }
703
+ }
704
+
705
+ private func routeInvalid(_ message: String) -> KakaoMapException {
706
+ KakaoMapException(
707
+ "ERR_KAKAO_MAP_ROUTE_INVALID_ARGUMENT",
708
+ message
709
+ )
710
+ }
711
+
712
+ private func routeUnsupported(_ message: String) -> KakaoMapException {
713
+ KakaoMapException(
714
+ "ERR_KAKAO_MAP_ROUTE_UNSUPPORTED",
715
+ message
716
+ )
717
+ }
718
+
719
+ private func routeNotFound(_ noun: String, id: String) -> KakaoMapException {
720
+ KakaoMapException(
721
+ "ERR_KAKAO_MAP_ROUTE_NOT_FOUND",
722
+ "Unknown \(noun) \(id)."
723
+ )
724
+ }
725
+
726
+ private extension [String: Any] {
727
+ func object(_ key: String) throws
728
+ -> [String: Any]
729
+ {
730
+ guard let value = self[key] as? [String: Any]
731
+ else { throw routeInvalid("\(key) must be an object.") }; return value
732
+ }
733
+
734
+ func optionalObject(_ key: String) throws -> [String: Any]? {
735
+ guard let raw = self[key]
736
+ else { return nil }; guard let value = raw as? [String: Any]
737
+ else { throw routeInvalid("\(key) must be an object.") }; return value
738
+ }
739
+
740
+ func objectList(_ key: String) throws -> [[String: Any]] {
741
+ guard let raw = self[key]
742
+ else { return [] }; guard let value = raw as? [[String: Any]]
743
+ else { throw routeInvalid("\(key) must be an array of objects.") }; return value
744
+ }
745
+
746
+ func string(_ key: String) throws -> String {
747
+ guard let value = self[key] as? String
748
+ else { throw routeInvalid("\(key) must be a string.") }; return value
749
+ }
750
+
751
+ func optionalString(_ key: String) throws -> String? {
752
+ guard let raw = self[key] else { return nil }
753
+ guard let value = raw as? String
754
+ else { throw routeInvalid("\(key) must be a string.") }
755
+ return value
756
+ }
757
+
758
+ func stringList(_ key: String) throws -> [String] {
759
+ guard let value = self[key] as? [String]
760
+ else { throw routeInvalid("\(key) must be an array of strings.") }; return value
761
+ }
762
+
763
+ func bool(
764
+ _ key: String,
765
+ default fallback: Bool? = nil
766
+ ) throws
767
+ -> Bool
768
+ {
769
+ guard let raw = self[key] else {
770
+ guard let fallback else { throw routeInvalid("\(key) must be a boolean.") }
771
+ return fallback
772
+ }
773
+ guard CFGetTypeID(raw as CFTypeRef) == CFBooleanGetTypeID(),
774
+ let value = raw as? Bool
775
+ else { throw routeInvalid("\(key) must be a boolean.") }
776
+ return value
777
+ }
778
+
779
+ func double(
780
+ _ key: String,
781
+ default fallback: Double? = nil
782
+ ) throws -> Double {
783
+ guard let raw = self[key] else {
784
+ guard let fallback else { throw routeInvalid("\(key) must be finite.") }
785
+ return fallback
786
+ }
787
+ guard let value = raw as? NSNumber,
788
+ CFGetTypeID(value) != CFBooleanGetTypeID(),
789
+ value.doubleValue.isFinite
790
+ else { throw routeInvalid("\(key) must be finite.") }
791
+ return value.doubleValue
792
+ }
793
+
794
+ func int(_ key: String, default fallback: Int? = nil) throws -> Int {
795
+ let value = try double(
796
+ key,
797
+ default: fallback.map(Double.init)
798
+ ); guard let integer = Int(exactly: value)
799
+ else { throw routeInvalid("\(key) must be an integer.") }; return integer
800
+ }
801
+
802
+ func nonnegativeInt(
803
+ _ key: String,
804
+ default fallback: Int? = nil
805
+ ) throws -> Int {
806
+ let value = try int(
807
+ key,
808
+ default: fallback
809
+ ); guard value >= 0 else { throw routeInvalid("\(key) must be non-negative.") }; return value
810
+ }
811
+
812
+ func minimumInt(
813
+ _ key: String,
814
+ default fallback: Int? = nil,
815
+ minimum: Int
816
+ ) throws -> Int {
817
+ let value = try int(
818
+ key,
819
+ default: fallback
820
+ ); guard value >= minimum
821
+ else { throw routeInvalid("\(key) must be at least \(minimum).") }; return value
822
+ }
823
+
824
+ func nonnegativeDouble(_ key: String) throws -> Double {
825
+ let value = try double(
826
+ key
827
+ ); guard value >=
828
+ 0 else { throw routeInvalid("\(key) must be non-negative.") }; return value
829
+ }
830
+
831
+ func color(
832
+ _ key: String,
833
+ default fallback: UIColor? = nil
834
+ ) throws
835
+ -> UIColor
836
+ {
837
+ guard let raw = self[key] else {
838
+ guard let fallback else { throw routeInvalid("\(key) must be a color.") }
839
+ return fallback
840
+ }
841
+ guard let value = raw as? String
842
+ else { throw routeInvalid("\(key) must be a color.") }
843
+ return try parseRouteColor(value)
844
+ }
845
+
846
+ func coordinate(_ key: String) throws -> MapPoint {
847
+ try object(key).coordinateValue()
848
+ }
849
+
850
+ func coordinateValue() throws
851
+ -> MapPoint
852
+ {
853
+ let latitude = try double("latitude"); let longitude =
854
+ try double("longitude"); guard (-90 ... 90).contains(latitude),
855
+ (-180 ... 180).contains(longitude)
856
+ else { throw routeInvalid("Invalid coordinate.") }; return MapPoint(
857
+ longitude: longitude,
858
+ latitude: latitude
859
+ )
860
+ }
861
+
862
+ func coordinateList(_ key: String) throws
863
+ -> [MapPoint]
864
+ {
865
+ guard let values = self[key] as? [[String: Any]]
866
+ else { throw routeInvalid("\(key) must be an array of coordinates.") }; return try values
867
+ .map { try $0.coordinateValue() }
868
+ }
869
+ }
870
+
871
+ private func routePixels(_ points: Double) throws -> Double {
872
+ let pixels = MapUnits.pixels(points)
873
+ guard pixels.isFinite else { throw routeInvalid("Pixel dimensions must be finite.") }
874
+ return pixels
875
+ }
876
+
877
+ private func routePixelUInt(_ points: Int) throws -> UInt {
878
+ let pixels = try routePixels(Double(points)).rounded()
879
+ guard let integer = UInt(exactly: pixels)
880
+ else { throw routeInvalid("Pixel dimensions are out of range.") }
881
+ return integer
882
+ }
883
+
884
+ private func routePixelFloat(_ points: Double) throws -> Float {
885
+ let pixels = try routePixels(points)
886
+ guard abs(pixels) <= Double(Float.greatestFiniteMagnitude)
887
+ else { throw routeInvalid("Pixel dimensions are out of range.") }
888
+ return Float(pixels)
889
+ }
890
+
891
+ private func parseRouteColor(_ value: String) throws -> UIColor {
892
+ guard value.range(of: "^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$", options: .regularExpression) != nil
893
+ else { throw routeInvalid("Colors must be #RRGGBB or #RRGGBBAA.") }
894
+ let raw = String(value.dropFirst())
895
+ let rgb = UInt64(raw.prefix(6), radix: 16) ?? 0
896
+ let alpha = raw.count == 8 ? UInt64(raw.suffix(2), radix: 16) ?? 255 : 255
897
+ return UIColor(
898
+ red: CGFloat((rgb >> 16) & 255) / 255,
899
+ green: CGFloat((rgb >> 8) & 255) / 255,
900
+ blue: CGFloat(rgb & 255) / 255,
901
+ alpha: CGFloat(alpha) / 255
902
+ )
903
+ }