@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,1294 @@
1
+ import CoreFoundation
2
+ import KakaoMapsSDK
3
+ import UIKit
4
+
5
+ final class MapShapes {
6
+ private let emit: (String, [String: Any]) -> Void
7
+ private let manager: ShapeManager
8
+ private var layers: [String: ShapeLayer] = [:]
9
+ private var shapes: [String: ShapeEntry] = [:]
10
+ private var animators: [String: ShapeAnimator] = [:]
11
+ private var animationTargets: [String: Set<String>] = [:]
12
+ private var animationNativeIDs: [String: String] = [:]
13
+ private var animationRunEpochs: [String: UInt64] = [:]
14
+ private var polygonStyles: [String: PolygonStyleSet] = [:]
15
+ private var polylineStyles: [String: PolylineStyleSet] = [:]
16
+ private var styleCounts: [String: Int] = [:]
17
+ private var nativeStyleIDs: [String: String] = [:]
18
+ private var nativeLayerIDs: [String: String] = [:]
19
+ private var sceneSequence = 0
20
+ private var replacementSequence = 0
21
+ private var animationSequence: UInt64 = 0
22
+
23
+ init(map: KakaoMap, emit: @escaping (String, [String: Any]) -> Void) {
24
+ self.emit = emit
25
+ manager = map.getShapeManager()
26
+ }
27
+
28
+ func apply(_ scene: [String: Any]) throws -> [String: Any] {
29
+ let styleSpecs = try scene.objectList("styleSets")
30
+ let layerSpecs = try scene.objectList("layers")
31
+ let shapeSpecs = try scene.objectList("shapes")
32
+ try validateScene(styles: styleSpecs, layers: layerSpecs, shapes: shapeSpecs)
33
+ var nextPolygonStyles: [String: PolygonStyleSet] = [:]
34
+ var nextPolylineStyles: [String: PolylineStyleSet] = [:]
35
+ var nextStyleCounts: [String: Int] = [:]
36
+ var nextNativeStyleIDs: [String: String] = [:]
37
+ var nextLayers: [String: ShapeLayer] = [:]
38
+ var nextNativeLayerIDs: [String: String] = [:]
39
+ var nextShapes: [String: ShapeEntry] = [:]
40
+ try registerStyles(
41
+ styleSpecs,
42
+ polygonStyles: &nextPolygonStyles,
43
+ polylineStyles: &nextPolylineStyles,
44
+ styleCounts: &nextStyleCounts,
45
+ nativeStyleIDs: &nextNativeStyleIDs
46
+ )
47
+ let scenePrefix = "rnk-shapes-\(ObjectIdentifier(self).hashValue)-scene-\(sceneSequence)"
48
+ sceneSequence += 1
49
+ do {
50
+ for spec in layerSpecs {
51
+ try addLayer(
52
+ spec,
53
+ scenePrefix: scenePrefix,
54
+ layers: &nextLayers,
55
+ nativeLayerIDs: &nextNativeLayerIDs
56
+ )
57
+ }
58
+ for spec in shapeSpecs {
59
+ try addShape(
60
+ spec,
61
+ layers: nextLayers,
62
+ shapes: &nextShapes,
63
+ polygonStyles: nextPolygonStyles,
64
+ polylineStyles: nextPolylineStyles,
65
+ nativeStyleIDs: nextNativeStyleIDs
66
+ )
67
+ }
68
+ } catch {
69
+ nextNativeLayerIDs.values.forEach { manager.removeShapeLayer(layerID: $0) }
70
+ throw error
71
+ }
72
+ disposeObjects()
73
+ polygonStyles = nextPolygonStyles
74
+ polylineStyles = nextPolylineStyles
75
+ styleCounts = nextStyleCounts
76
+ nativeStyleIDs = nextNativeStyleIDs
77
+ layers = nextLayers
78
+ nativeLayerIDs = nextNativeLayerIDs
79
+ shapes = nextShapes
80
+ for spec in layerSpecs {
81
+ try nextLayers[spec.string("id")]?.visible = try spec.bool("visible", default: true)
82
+ }
83
+ return counts()
84
+ }
85
+
86
+ func command(_ command: [String: Any]) throws -> [String: Any] {
87
+ switch try command.string("type") {
88
+ case "setVisible":
89
+ let isVisible = try command.bool("visible")
90
+ let targets = try entries(command.stringList("ids"))
91
+ try targets.forEach { try $0.setVisible(isVisible) }
92
+ return none()
93
+ case "setPosition":
94
+ try entry(command.string("id")).setPosition(command.coordinate("position"))
95
+ return none()
96
+ case "setZIndex":
97
+ throw shapeUnsupported("iOS Kakao Maps does not expose mutable shape z-order.")
98
+ case "setOrientation":
99
+ try entry(command.string("id")).setOrientation(command.double("orientation"))
100
+ return none()
101
+ case "changeData":
102
+ try changeData(command)
103
+ return none()
104
+ case "startWave":
105
+ try startWave(command.object("animation"))
106
+ return none()
107
+ case "stopAnimation":
108
+ try stopAnimation(command.string("animationId"), emitEvent: true)
109
+ return none()
110
+ case "getState":
111
+ return try state(command.string("id"))
112
+ case "getCounts":
113
+ return counts()
114
+ case "getCapabilities":
115
+ return capabilities().merging(["type": "capabilities"]) { _, new in new }
116
+ case "remove":
117
+ let ids = try command.stringList("ids")
118
+ _ = try entries(ids)
119
+ try ids.forEach(remove)
120
+ return none()
121
+ default:
122
+ throw shapeInvalid("Unknown shape command type.")
123
+ }
124
+ }
125
+
126
+ func capabilities() -> [String: Any] {
127
+ [
128
+ "layerPasses": ["default", "overlay", "route"],
129
+ "geographicPrimitives": true,
130
+ "modelPrimitives": true,
131
+ "geographicPosition": false,
132
+ "geographicOrientation": true,
133
+ "modelPosition": true,
134
+ "modelOrientation": true,
135
+ "zIndexMutation": false,
136
+ "fractionalStyleWidth": false,
137
+ "waveAnimation": true,
138
+ "waveTargets": ["polygon", "polyline"],
139
+ "waveNaturalStopEvent": true
140
+ ]
141
+ }
142
+
143
+ func resolveShape(_ id: String) -> (any Shape)? {
144
+ shapes[id]?.nativeShape
145
+ }
146
+
147
+ func dispose() {
148
+ disposeObjects()
149
+ polygonStyles.removeAll()
150
+ polylineStyles.removeAll()
151
+ styleCounts.removeAll()
152
+ nativeStyleIDs.removeAll()
153
+ nativeLayerIDs.removeAll()
154
+ }
155
+
156
+ private func validateScene(
157
+ styles styleSpecs: [[String: Any]],
158
+ layers layerSpecs: [[String: Any]],
159
+ shapes shapeSpecs: [[String: Any]]
160
+ ) throws {
161
+ try requireUniqueShape(styleSpecs, noun: "style set")
162
+ try requireUniqueShape(layerSpecs, noun: "shape layer")
163
+ try requireUniqueShape(shapeSpecs, noun: "shape")
164
+ try styleSpecs.forEach(validateStyleSet)
165
+ for layer in layerSpecs {
166
+ _ = try layer.int("zIndex", default: defaultShapeLayerZIndex)
167
+ _ = try shapeLayerPass(layer.optionalString("pass") ?? "default")
168
+ _ = try layer.bool("visible", default: true)
169
+ }
170
+ let styleTypes = try Dictionary(uniqueKeysWithValues: styleSpecs.map { try (
171
+ $0.string("id"),
172
+ $0.string("type")
173
+ ) })
174
+ let styleCounts = try Dictionary(uniqueKeysWithValues: styleSpecs.map { try (
175
+ $0.string("id"),
176
+ $0.objectList("styles").count
177
+ ) })
178
+ let layerIDs = try Set(layerSpecs.map { try $0.string("id") })
179
+ for spec in shapeSpecs {
180
+ _ = try spec.int("zIndex", default: 0)
181
+ _ = try spec.bool("visible", default: true)
182
+ let geometry = try spec.object("geometry")
183
+ let type = try geometry.string("type")
184
+ let polylinePrimitives = ["geographicCurve", "modelArc", "geographicArc"]
185
+ let requiredStyle = type.contains("Polyline") || polylinePrimitives
186
+ .contains(type) ? "polyline" : "polygon"
187
+ let id = try spec.string("id")
188
+ guard try layerIDs.contains(spec.string("layerId"))
189
+ else { throw shapeInvalid("Shape \(id) references an unknown layer.") }
190
+ let styleID = try spec.string("styleSetId")
191
+ guard styleTypes[styleID] == requiredStyle, let count = styleCounts[styleID] else {
192
+ throw shapeInvalid("Shape \(id) references an incompatible style set.")
193
+ }
194
+ try validateShapeGeometry(normalizeShapeGeometry(geometry), styleCount: count)
195
+ }
196
+ }
197
+
198
+ private func validateStyleSet(_ spec: [String: Any]) throws {
199
+ let styles = try spec.objectList("styles")
200
+ guard !styles.isEmpty else { throw shapeInvalid("Shape style sets require styles.") }
201
+ switch try spec.string("type") {
202
+ case "polygon":
203
+ _ = try mapShapePolygonStyleSet(spec)
204
+ case "polyline":
205
+ _ = try polylineCap(spec.optionalString("cap") ?? "square")
206
+ for style in styles {
207
+ let levels = try style.objectList("levels")
208
+ guard !levels.isEmpty else { throw shapeInvalid("Polyline styles require levels.") }
209
+ for level in levels {
210
+ _ = try level.int("level")
211
+ _ = try level.nonnegativeInt("width")
212
+ _ = try level.nonnegativeInt("strokeWidth", default: 0)
213
+ _ = try level.color("color")
214
+ _ = try level.color("strokeColor", default: .clear)
215
+ }
216
+ }
217
+ default:
218
+ throw shapeInvalid("Unknown shape style type.")
219
+ }
220
+ }
221
+
222
+ private func registerStyles(
223
+ _ specs: [[String: Any]],
224
+ polygonStyles: inout [String: PolygonStyleSet],
225
+ polylineStyles: inout [String: PolylineStyleSet],
226
+ styleCounts: inout [String: Int],
227
+ nativeStyleIDs: inout [String: String]
228
+ ) throws {
229
+ for spec in specs {
230
+ let id = try spec.string("id")
231
+ let nativeID = mapStyleNativeID("rnk-shape-style", spec)
232
+ let nativeSpec = spec.merging(["id": nativeID]) { _, new in new }
233
+ if try spec.string("type") == "polygon" {
234
+ let styleSet = try mapShapePolygonStyleSet(nativeSpec)
235
+ manager.addPolygonStyleSet(styleSet)
236
+ polygonStyles[id] = styleSet
237
+ } else {
238
+ let styles = try spec.objectList("styles").map { style in
239
+ try PolylineStyle(styles: style.objectList("levels").map { level in
240
+ try PerLevelPolylineStyle(
241
+ bodyColor: level.color("color"),
242
+ bodyWidth: shapePixelUInt(level.nonnegativeInt("width")),
243
+ strokeColor: level.color("strokeColor", default: .clear),
244
+ strokeWidth: shapePixelUInt(level.nonnegativeInt("strokeWidth", default: 0)),
245
+ level: level.int("level")
246
+ )
247
+ })
248
+ }
249
+ let styleSet = try PolylineStyleSet(
250
+ styleSetID: nativeID,
251
+ styles: styles,
252
+ capType: polylineCap(spec.optionalString("cap") ?? "square")
253
+ )
254
+ manager.addPolylineStyleSet(styleSet)
255
+ polylineStyles[id] = styleSet
256
+ }
257
+ styleCounts[id] = try spec.objectList("styles").count
258
+ nativeStyleIDs[id] = nativeID
259
+ }
260
+ }
261
+
262
+ private func addLayer(
263
+ _ spec: [String: Any],
264
+ scenePrefix: String,
265
+ layers: inout [String: ShapeLayer],
266
+ nativeLayerIDs: inout [String: String]
267
+ ) throws {
268
+ let id = try spec.string("id")
269
+ let nativeID = "\(scenePrefix)-layer-\(id)"
270
+ guard let layer = try manager.addShapeLayer(
271
+ layerID: nativeID,
272
+ zOrder: spec.int("zIndex", default: defaultShapeLayerZIndex),
273
+ passType: shapeLayerPass(spec.optionalString("pass") ?? "default")
274
+ ) else { throw shapeInvalid("Could not create shape layer \(id).") }
275
+ layer.visible = false
276
+ layers[id] = layer
277
+ nativeLayerIDs[id] = nativeID
278
+ }
279
+
280
+ private func addShape(
281
+ _ spec: [String: Any],
282
+ layers: [String: ShapeLayer],
283
+ shapes: inout [String: ShapeEntry],
284
+ polygonStyles: [String: PolygonStyleSet],
285
+ polylineStyles: [String: PolylineStyleSet],
286
+ nativeStyleIDs: [String: String],
287
+ nativeShapeID: String? = nil
288
+ ) throws {
289
+ let id = try spec.string("id")
290
+ let shapeID = nativeShapeID ?? id
291
+ let layerID = try spec.string("layerId")
292
+ guard let layer = layers[layerID] else { throw shapeNotFound("shape layer", id: layerID) }
293
+ let styleID = try spec.string("styleSetId")
294
+ guard let nativeStyleID = nativeStyleIDs[styleID]
295
+ else { throw shapeNotFound("shape style set", id: styleID) }
296
+ let rawGeometry = try spec.object("geometry")
297
+ let geometryType = try rawGeometry.string("type")
298
+ let geometry = try normalizeShapeGeometry(rawGeometry)
299
+ let zOrder = try spec.int("zIndex", default: 0)
300
+ let isVisible = try spec.bool("visible", default: true)
301
+ let type = try geometry.string("type")
302
+ let entry: ShapeEntry
303
+ switch type {
304
+ case "geographicPolygon":
305
+ guard polygonStyles[styleID] != nil else { throw shapeNotFound(
306
+ "polygon style set",
307
+ id: styleID
308
+ ) }
309
+ let options = MapPolygonShapeOptions(shapeID: shapeID, styleID: nativeStyleID, zOrder: zOrder)
310
+ options.polygons = try geometry.objectList("parts").map(mapShapeMapPolygon)
311
+ guard let shape = layer.addMapPolygonShape(options)
312
+ else { throw shapeInvalid("Could not create polygon \(id).") }
313
+ shape.userObject = NSNumber(value: zOrder)
314
+ entry = .mapPolygon(layerID, geometryType, shape)
315
+ case "modelPolygon":
316
+ guard polygonStyles[styleID] != nil else { throw shapeNotFound(
317
+ "polygon style set",
318
+ id: styleID
319
+ ) }
320
+ let options = PolygonShapeOptions(shapeID: shapeID, styleID: nativeStyleID, zOrder: zOrder)
321
+ options.basePosition = try geometry.coordinate("base")
322
+ options.polygons = try geometry.objectList("parts").map(mapShapeModelPolygon)
323
+ guard let shape = layer.addPolygonShape(options)
324
+ else { throw shapeInvalid("Could not create model polygon \(id).") }
325
+ shape.userObject = NSNumber(value: zOrder)
326
+ shape.orientation = try geometry.double("orientation", default: 0) * .pi / 180
327
+ entry = .polygon(layerID, geometryType, shape)
328
+ case "geographicPolyline",
329
+ "geographicCurve":
330
+ guard polylineStyles[styleID] != nil else { throw shapeNotFound(
331
+ "polyline style set",
332
+ id: styleID
333
+ ) }
334
+ let options = MapPolylineShapeOptions(
335
+ shapeID: shapeID,
336
+ styleID: nativeStyleID,
337
+ zOrder: zOrder
338
+ )
339
+ options.polylines = try geometry.objectList("parts").map(mapPolyline)
340
+ guard let shape = layer.addMapPolylineShape(options)
341
+ else { throw shapeInvalid("Could not create polyline \(id).") }
342
+ shape.userObject = NSNumber(value: zOrder)
343
+ entry = .mapPolyline(layerID, geometryType, shape)
344
+ case "modelPolyline":
345
+ guard polylineStyles[styleID] != nil else { throw shapeNotFound(
346
+ "polyline style set",
347
+ id: styleID
348
+ ) }
349
+ let options = PolylineShapeOptions(shapeID: shapeID, styleID: nativeStyleID, zOrder: zOrder)
350
+ options.basePosition = try geometry.coordinate("base")
351
+ options.polylines = try geometry.objectList("parts").map(modelPolyline)
352
+ guard let shape = layer.addPolylineShape(options)
353
+ else { throw shapeInvalid("Could not create model polyline \(id).") }
354
+ shape.userObject = NSNumber(value: zOrder)
355
+ shape.orientation = try geometry.double("orientation", default: 0) * .pi / 180
356
+ entry = .polyline(layerID, geometryType, shape)
357
+ default:
358
+ throw shapeInvalid("Unsupported normalized shape geometry.")
359
+ }
360
+ try entry.setVisible(isVisible)
361
+ shapes[id] = entry
362
+ }
363
+
364
+ private func normalizeShapeGeometry(_ geometry: [String: Any]) throws -> [String: Any] {
365
+ switch try geometry.string("type") {
366
+ case "modelCircle":
367
+ let points = try Primitives.getCirclePoints(
368
+ radius: geometry.positiveDouble("radius"),
369
+ numPoints: geometry.minimumInt("pointCount", default: 64, minimum: 3),
370
+ cw: geometry.bool("clockwise", default: true)
371
+ ).map(shapeSDKModelPointToMeters)
372
+ return try [
373
+ "type": "modelPolygon",
374
+ "base": geometry.object("base"),
375
+ "orientation": geometry.double("orientation", default: 0),
376
+ "parts": [[
377
+ "outer": pointPayload(points),
378
+ "styleIndex": geometry.int("styleIndex", default: 0)
379
+ ]]
380
+ ]
381
+ case "modelRectangle":
382
+ let points = try Primitives.getRectanglePoints(
383
+ width: geometry.positiveDouble("width"),
384
+ height: geometry.positiveDouble("height"),
385
+ cw: geometry.bool("clockwise", default: true)
386
+ ).map(shapeSDKModelPointToMeters)
387
+ return try [
388
+ "type": "modelPolygon",
389
+ "base": geometry.object("base"),
390
+ "orientation": geometry.double("orientation", default: 0),
391
+ "parts": [[
392
+ "outer": pointPayload(points),
393
+ "styleIndex": geometry.int("styleIndex", default: 0)
394
+ ]]
395
+ ]
396
+ case "geographicCircle":
397
+ let points = try Primitives.getCirclePoints(
398
+ radius: geometry.positiveDouble("radius"),
399
+ numPoints: geometry.minimumInt("pointCount", default: 64, minimum: 3),
400
+ cw: geometry.bool("clockwise", default: true),
401
+ center: geometry.coordinate("center")
402
+ )
403
+ return try [
404
+ "type": "geographicPolygon",
405
+ "parts": [[
406
+ "outer": coordinatePayload(points),
407
+ "styleIndex": geometry.int("styleIndex", default: 0)
408
+ ]]
409
+ ]
410
+ case "geographicRectangle":
411
+ let points = try Primitives.getRectanglePoints(
412
+ width: geometry.positiveDouble("width"),
413
+ height: geometry.positiveDouble("height"),
414
+ cw: geometry.bool("clockwise", default: true),
415
+ center: geometry.coordinate("center")
416
+ )
417
+ return try [
418
+ "type": "geographicPolygon",
419
+ "parts": [[
420
+ "outer": coordinatePayload(points),
421
+ "styleIndex": geometry.int("styleIndex", default: 0)
422
+ ]]
423
+ ]
424
+ case "geographicCurve":
425
+ let isLeft: Bool
426
+ switch try geometry.string("side") {
427
+ case "left": isLeft = true
428
+ case "right": isLeft = false
429
+ default: throw shapeInvalid("Curve side must be left or right.")
430
+ }
431
+ let points = try Primitives.getCurvePoints(
432
+ startPoint: geometry.coordinate("start"),
433
+ endPoint: geometry.coordinate("end"),
434
+ isLeft: isLeft
435
+ )
436
+ return try [
437
+ "type": "geographicCurve",
438
+ "parts": [[
439
+ "points": coordinatePayload(points),
440
+ "styleIndex": geometry.int("styleIndex", default: 0)
441
+ ]]
442
+ ]
443
+ case "modelArc":
444
+ return try [
445
+ "type": "modelPolyline",
446
+ "base": geometry.object("base"),
447
+ "orientation": geometry.double("orientation", default: 0),
448
+ "parts": [[
449
+ "points": pointPayload(shapeArcPoints(geometry)),
450
+ "styleIndex": geometry.int("styleIndex", default: 0)
451
+ ]]
452
+ ]
453
+ case "geographicArc":
454
+ let center = try geometry.coordinate("center")
455
+ let points = try shapeArcPoints(geometry).map {
456
+ shapeOffset(center, east: Double($0.x), north: Double($0.y))
457
+ }
458
+ return try [
459
+ "type": "geographicPolyline",
460
+ "parts": [[
461
+ "points": coordinatePayload(points),
462
+ "styleIndex": geometry.int("styleIndex", default: 0)
463
+ ]]
464
+ ]
465
+ default:
466
+ return geometry
467
+ }
468
+ }
469
+
470
+ private func changeData(_ command: [String: Any]) throws {
471
+ let id = try command.string("id")
472
+ let old = try entry(id)
473
+ let styleID = try command.string("styleSetId")
474
+ let requestedGeometry = try command.object("geometry")
475
+ let geometry = try normalizeShapeGeometry(requestedGeometry)
476
+ let type = try geometry.string("type")
477
+ let isVisible = old.isVisible
478
+ let expectsPolyline = type.contains("Polyline") || type == "geographicCurve"
479
+ guard expectsPolyline ? polylineStyles[styleID] != nil : polygonStyles[styleID] != nil
480
+ else { throw shapeInvalid("Shape \(id) references an incompatible style set.") }
481
+ try validateShapeGeometry(geometry, styleCount: styleCounts[styleID] ?? 0)
482
+ let spec: [String: Any] = [
483
+ "id": id,
484
+ "layerId": old.layerID,
485
+ "styleSetId": styleID,
486
+ "zIndex": old.zOrder,
487
+ "visible": false,
488
+ "geometry": requestedGeometry
489
+ ]
490
+ var replacement: [String: ShapeEntry] = [:]
491
+ try addShape(
492
+ spec,
493
+ layers: layers,
494
+ shapes: &replacement,
495
+ polygonStyles: polygonStyles,
496
+ polylineStyles: polylineStyles,
497
+ nativeStyleIDs: nativeStyleIDs,
498
+ nativeShapeID: "rnk-shape-replacement-\(ObjectIdentifier(self).hashValue)-\(replacementSequence)"
499
+ )
500
+ replacementSequence += 1
501
+ try remove(id)
502
+ guard let value = replacement[id]
503
+ else { throw shapeInvalid("Could not replace shape \(id).") }
504
+ try value.setVisible(isVisible)
505
+ shapes[id] = value
506
+ }
507
+
508
+ private func startWave(_ animation: [String: Any]) throws {
509
+ let id = try animation.string("animationId")
510
+ guard !id.isEmpty else { throw shapeInvalid("Shape animationId must not be empty.") }
511
+ guard animators[id] == nil else { throw shapeInvalid("Shape animation \(id) already exists.") }
512
+ let effect = try mapShapeWaveEffect(animation)
513
+ let targets = try animation.stringList("shapeIds").map(entry)
514
+ guard !targets.isEmpty else { throw shapeInvalid("Shape wave animation requires shapeIds.") }
515
+ animationSequence &+= 1
516
+ let runEpoch = animationSequence
517
+ let nativeID = "rnk-shape-\(ObjectIdentifier(self).hashValue)-\(runEpoch)"
518
+ guard let animator = manager.addShapeAnimator(animatorID: nativeID, effect: effect) else {
519
+ throw shapeInvalid("Could not create shape animation \(id).")
520
+ }
521
+ for target in targets {
522
+ switch target {
523
+ case let .mapPolygon(_, _, shape): animator.addMapPolygonShape(shape)
524
+ case let .polygon(_, _, shape): animator.addPolygonShape(shape)
525
+ case let .mapPolyline(_, _, shape): animator.addMapPolylineShape(shape)
526
+ case let .polyline(_, _, shape): animator.addPolylineShape(shape)
527
+ }
528
+ }
529
+ animator.setStopCallback { [weak self, weak animator] _ in
530
+ guard let self,
531
+ let animator,
532
+ animationNativeIDs[id] == nativeID,
533
+ animationRunEpochs[id] == runEpoch,
534
+ animators[id] === animator
535
+ else { return }
536
+ animators.removeValue(forKey: id)
537
+ animationTargets.removeValue(forKey: id)
538
+ animationNativeIDs.removeValue(forKey: id)
539
+ animationRunEpochs.removeValue(forKey: id)
540
+ manager.removeShapeAnimator(animatorID: nativeID)
541
+ emit("onShapeAnimationStop", ["animationId": id])
542
+ }
543
+ animators[id] = animator
544
+ animationTargets[id] = try Set(animation.stringList("shapeIds"))
545
+ animationNativeIDs[id] = nativeID
546
+ animationRunEpochs[id] = runEpoch
547
+ animator.start()
548
+ }
549
+
550
+ private func stopAnimation(_ id: String, emitEvent: Bool) throws {
551
+ guard let animator = animators.removeValue(forKey: id) else { throw shapeNotFound(
552
+ "shape animation",
553
+ id: id
554
+ ) }
555
+ animationTargets.removeValue(forKey: id)
556
+ guard let nativeID = animationNativeIDs.removeValue(forKey: id)
557
+ else { throw shapeNotFound("shape animation", id: id) }
558
+ animationRunEpochs.removeValue(forKey: id)
559
+ animator.setStopCallback(nil)
560
+ animator.stop()
561
+ manager.removeShapeAnimator(animatorID: nativeID)
562
+ if emitEvent {
563
+ emit("onShapeAnimationStop", ["animationId": id])
564
+ }
565
+ }
566
+
567
+ private func remove(_ id: String) throws {
568
+ for animationID in animationTargets.filter({ $0.value.contains(id) }).map(\.key) {
569
+ try stopAnimation(animationID, emitEvent: true)
570
+ }
571
+ let value = try entry(id)
572
+ guard let layer = layers[value.layerID] else { throw shapeNotFound(
573
+ "shape layer",
574
+ id: value.layerID
575
+ ) }
576
+ switch value {
577
+ case let .mapPolygon(_, _, shape): layer.removeMapPolygonShape(shapeID: shape.shapeID)
578
+ case let .polygon(_, _, shape): layer.removePolygonShape(shapeID: shape.shapeID)
579
+ case let .mapPolyline(_, _, shape): layer.removeMapPolylineShape(shapeID: shape.shapeID)
580
+ case let .polyline(_, _, shape): layer.removePolylineShape(shapeID: shape.shapeID)
581
+ }
582
+ shapes.removeValue(forKey: id)
583
+ }
584
+
585
+ private func disposeObjects() {
586
+ let activeAnimators = animators
587
+ let activeNativeIDs = animationNativeIDs
588
+ animators.removeAll()
589
+ animationTargets.removeAll()
590
+ animationNativeIDs.removeAll()
591
+ animationRunEpochs.removeAll()
592
+ for (id, animator) in activeAnimators {
593
+ animator.setStopCallback(nil)
594
+ animator.stop()
595
+ if let nativeID = activeNativeIDs[id] {
596
+ manager.removeShapeAnimator(animatorID: nativeID)
597
+ }
598
+ }
599
+ for id in layers.keys {
600
+ if let nativeID = nativeLayerIDs[id] {
601
+ manager.removeShapeLayer(layerID: nativeID)
602
+ }
603
+ }
604
+ layers.removeAll()
605
+ nativeLayerIDs.removeAll()
606
+ shapes.removeAll()
607
+ }
608
+
609
+ private func state(_ id: String) throws -> [String: Any] {
610
+ let value = try entry(id)
611
+ return [
612
+ "type": "state",
613
+ "id": id,
614
+ "layerId": value.layerID,
615
+ "visible": value.isVisible,
616
+ "zIndex": value.zOrder,
617
+ "geometryType": value.geometryType,
618
+ "position": value.positionPayload ?? NSNull(),
619
+ "orientation": value.orientationDegrees
620
+ ]
621
+ }
622
+
623
+ private func counts() -> [String: Any] {
624
+ [
625
+ "type": "counts",
626
+ "layers": layers.count,
627
+ "shapes": shapes.count,
628
+ "animations": animators.count
629
+ ]
630
+ }
631
+
632
+ private func none() -> [String: Any] {
633
+ ["type": "none"]
634
+ }
635
+
636
+ private func entry(_ id: String) throws -> ShapeEntry {
637
+ guard let value = shapes[id]
638
+ else { throw shapeNotFound(
639
+ "shape",
640
+ id: id
641
+ ) }; return value
642
+ }
643
+
644
+ private func entries(_ ids: [String]) throws -> [ShapeEntry] {
645
+ guard Set(ids).count == ids.count
646
+ else { throw shapeInvalid("Shape command IDs must be unique.") }
647
+ return try ids.map(entry)
648
+ }
649
+ }
650
+
651
+ private enum ShapeEntry {
652
+ case mapPolygon(String, String, MapPolygonShape)
653
+ case polygon(String, String, PolygonShape)
654
+ case mapPolyline(String, String, MapPolylineShape)
655
+ case polyline(String, String, PolylineShape)
656
+
657
+ var nativeShape: any Shape {
658
+ switch self {
659
+ case let .mapPolygon(_, _, value): value
660
+ case let .polygon(_, _, value): value
661
+ case let .mapPolyline(_, _, value): value
662
+ case let .polyline(_, _, value): value
663
+ }
664
+ }
665
+
666
+ var layerID: String {
667
+ switch self { case let .mapPolygon(id, _, _),
668
+ let .polygon(id, _, _),
669
+ let .mapPolyline(
670
+ id,
671
+ _,
672
+ _
673
+ ),
674
+ let .polyline(id, _, _): id }
675
+ }
676
+
677
+ var geometryType: String {
678
+ switch self { case let .mapPolygon(_, type, _),
679
+ let .polygon(
680
+ _,
681
+ type,
682
+ _
683
+ ),
684
+ let .mapPolyline(_, type, _),
685
+ let .polyline(_, type, _): type }
686
+ }
687
+
688
+ var isVisible: Bool {
689
+ switch self { case let .mapPolygon(_, _, value): value.isShow; case let .polygon(
690
+ _,
691
+ _,
692
+ value
693
+ ): value.isShow; case let .mapPolyline(_, _, value): value
694
+ .isShow; case let .polyline(_, _, value): value.isShow }
695
+ }
696
+
697
+ var zOrder: Int {
698
+ switch self {
699
+ case let .mapPolygon(_, _, value): (value.userObject as? NSNumber)?
700
+ .intValue ?? 0; case let .polygon(
701
+ _,
702
+ _,
703
+ value
704
+ ): (value.userObject as? NSNumber)?.intValue ?? 0; case let .mapPolyline(_, _, value): (value
705
+ .userObject as? NSNumber)?.intValue ?? 0; case let .polyline(
706
+ _,
707
+ _,
708
+ value
709
+ ): (value.userObject as? NSNumber)?.intValue ?? 0
710
+ }
711
+ }
712
+
713
+ var positionPayload: [String: Double]? {
714
+ switch self {
715
+ case let .polygon(_, _, value):
716
+ [
717
+ "latitude": value.position.wgsCoord.latitude,
718
+ "longitude": value.position.wgsCoord.longitude
719
+ ]
720
+ case let .polyline(_, _, value):
721
+ [
722
+ "latitude": value.position.wgsCoord.latitude,
723
+ "longitude": value.position.wgsCoord.longitude
724
+ ]
725
+ case .mapPolygon,
726
+ .mapPolyline:
727
+ nil
728
+ }
729
+ }
730
+
731
+ var orientationDegrees: Double {
732
+ let radians: Double = switch self {
733
+ case let .mapPolygon(_, _, value): value.orientation
734
+ case let .polygon(_, _, value): value.orientation
735
+ case let .mapPolyline(_, _, value): value.orientation
736
+ case let .polyline(_, _, value): value.orientation
737
+ }
738
+ return radians * 180 / .pi
739
+ }
740
+
741
+ func setVisible(_ visible: Bool) throws {
742
+ switch self {
743
+ case let .mapPolygon(_, _, value): visible ? value.show() : value.hide(); case let .polygon(
744
+ _,
745
+ _,
746
+ value
747
+ ): visible ? value.show() : value.hide(); case let .mapPolyline(_, _, value): visible ? value
748
+ .show() : value.hide(); case let .polyline(
749
+ _,
750
+ _,
751
+ value
752
+ ): visible ? value.show() : value.hide()
753
+ }
754
+ }
755
+
756
+ func setPosition(_ position: MapPoint) throws {
757
+ switch self { case let .polygon(_, _, value): value.position = position; case let .polyline(
758
+ _,
759
+ _,
760
+ value
761
+ ): value
762
+ .position =
763
+ position; default: throw shapeUnsupported(
764
+ "iOS Kakao Maps cannot reposition geographic shapes."
765
+ )
766
+ }
767
+ }
768
+
769
+ func setOrientation(_ degrees: Double) throws {
770
+ guard degrees.isFinite
771
+ else { throw shapeInvalid("orientation must be finite.") }; switch self { case let .mapPolygon(
772
+ _,
773
+ _,
774
+ value
775
+ ): value.orientation = degrees * .pi / 180; case let .polygon(_, _, value): value
776
+ .orientation = degrees * .pi / 180; case let .mapPolyline(
777
+ _,
778
+ _,
779
+ value
780
+ ): value.orientation = degrees * .pi / 180; case let .polyline(_, _, value): value
781
+ .orientation = degrees * .pi / 180 }
782
+ }
783
+ }
784
+
785
+ func mapStyleNativeID(_ prefix: String, _ spec: [String: Any]) -> String {
786
+ let data = (try? JSONSerialization.data(withJSONObject: spec, options: [.sortedKeys])) ?? Data()
787
+ return "\(prefix)-\(data.count)-\(UInt(bitPattern: data.hashValue))"
788
+ }
789
+
790
+ func mapShapeMapPolygon(_ part: [String: Any]) throws -> MapPolygon {
791
+ let value = try MapPolygon(
792
+ exteriorRing: part.coordinateList("outer"),
793
+ holes: part.optionalCoordinateLists("holes"),
794
+ styleIndex: UInt(part.nonnegativeInt("styleIndex", default: 0))
795
+ )
796
+ value.mergeOverlappedHoles = try part.bool("mergeOverlappedHoles", default: false)
797
+ return value
798
+ }
799
+
800
+ func mapShapeModelPolygon(_ part: [String: Any]) throws -> Polygon {
801
+ let value = try Polygon(
802
+ exteriorRing: part.pointList("outer"),
803
+ holes: part.optionalPointLists("holes"),
804
+ styleIndex: UInt(part.nonnegativeInt("styleIndex", default: 0))
805
+ )
806
+ value.mergeOverlappedHoles = try part.bool("mergeOverlappedHoles", default: false)
807
+ return value
808
+ }
809
+
810
+ private func mapPolyline(_ part: [String: Any]) throws -> MapPolyline {
811
+ try MapPolyline(
812
+ line: part.coordinateList("points"),
813
+ styleIndex: UInt(part.nonnegativeInt("styleIndex", default: 0))
814
+ )
815
+ }
816
+
817
+ private func modelPolyline(_ part: [String: Any]) throws -> Polyline {
818
+ try Polyline(
819
+ line: part.pointList("points"),
820
+ styleIndex: UInt(part.nonnegativeInt("styleIndex", default: 0))
821
+ )
822
+ }
823
+
824
+ private func coordinatePayload(_ points: [MapPoint]) -> [[String: Double]] {
825
+ points.map { [
826
+ "latitude": $0.wgsCoord.latitude,
827
+ "longitude": $0.wgsCoord.longitude
828
+ ] }
829
+ }
830
+
831
+ private func pointPayload(_ points: [CGPoint]) -> [[String: Double]] {
832
+ points.map { [
833
+ "x": Double($0.x),
834
+ "y": Double($0.y)
835
+ ] }
836
+ }
837
+
838
+ private func shapeArcPoints(_ geometry: [String: Any]) throws -> [CGPoint] {
839
+ let radius = try geometry.positiveDouble("radius")
840
+ let count = try geometry.minimumInt("pointCount", default: 64, minimum: 2)
841
+ let startDegrees = try geometry.double("startAngle")
842
+ let endDegrees = try geometry.double("endAngle")
843
+ let clockwise = try geometry.bool("clockwise", default: true)
844
+ let angles = shapeArcAngles(
845
+ start: startDegrees,
846
+ end: endDegrees,
847
+ clockwise: clockwise
848
+ )
849
+ return (0 ..< count).map { index in
850
+ let angle = angles.start + angles.sweep * Double(index) / Double(count - 1)
851
+ return CGPoint(x: radius * sin(angle), y: radius * cos(angle))
852
+ }
853
+ }
854
+
855
+ private func shapeArcAngles(
856
+ start: Double,
857
+ end: Double,
858
+ clockwise: Bool
859
+ ) -> (start: Double, sweep: Double) {
860
+ let normalizedStart = shapeNormalizedDegrees(start)
861
+ let normalizedEnd = shapeNormalizedDegrees(end)
862
+ let delta = normalizedEnd - normalizedStart
863
+ let sweep: Double = if delta == 0 {
864
+ start == end ? 0 : (clockwise ? 360 : -360)
865
+ } else if clockwise {
866
+ delta > 0 ? delta : delta + 360
867
+ } else {
868
+ delta < 0 ? delta : delta - 360
869
+ }
870
+ return (normalizedStart * .pi / 180, sweep * .pi / 180)
871
+ }
872
+
873
+ private func shapeNormalizedDegrees(_ value: Double) -> Double {
874
+ let remainder = value.truncatingRemainder(dividingBy: 360)
875
+ return remainder < 0 ? remainder + 360 : remainder
876
+ }
877
+
878
+ private func shapeOffset(_ center: MapPoint, east: Double, north: Double) -> MapPoint {
879
+ let distance = sqrt(east * east + north * north)
880
+ let bearing = atan2(east, north)
881
+ let coordinate = center.wgsCoord
882
+ let latitude = coordinate.latitude * .pi / 180
883
+ let longitude = coordinate.longitude * .pi / 180
884
+ let angular = distance / shapeEarthRadius
885
+ let targetLatitude =
886
+ asin(sin(latitude) * cos(angular) + cos(latitude) * sin(angular) * cos(bearing))
887
+ let targetLongitude = longitude + atan2(
888
+ sin(bearing) * sin(angular) * cos(latitude),
889
+ cos(angular) - sin(latitude) * sin(targetLatitude)
890
+ )
891
+ return MapPoint(longitude: targetLongitude * 180 / .pi, latitude: targetLatitude * 180 / .pi)
892
+ }
893
+
894
+ private func validateShapeGeometry(_ geometry: [String: Any], styleCount: Int) throws {
895
+ let type = try geometry.string("type")
896
+ guard ["geographicPolygon", "modelPolygon", "geographicPolyline", "modelPolyline"].contains(type)
897
+ else { return }
898
+ let parts = try geometry.objectList("parts")
899
+ guard !parts.isEmpty else { throw shapeInvalid("Shape geometry requires parts.") }
900
+ for part in parts {
901
+ let index = try part.nonnegativeInt("styleIndex", default: 0)
902
+ guard index < styleCount else { throw shapeInvalid("Shape styleIndex is out of range.") }
903
+ let count = try (type.contains("Polygon") ? part.anyList("outer") : part.anyList("points"))
904
+ .count
905
+ let minimum = type.contains("Polygon") ? 3 : 2
906
+ guard count >= minimum
907
+ else { throw shapeInvalid("Shape part requires at least \(minimum) points.") }
908
+ if type == "geographicPolygon" {
909
+ for hole in try part.optionalCoordinateLists("holes") ?? [] {
910
+ guard hole.count >= 3
911
+ else { throw shapeInvalid("Shape polygon holes require at least 3 points.") }
912
+ }
913
+ } else if type == "modelPolygon" {
914
+ for hole in try part.optionalPointLists("holes") ?? [] {
915
+ guard hole.count >= 3
916
+ else { throw shapeInvalid("Shape polygon holes require at least 3 points.") }
917
+ }
918
+ }
919
+ }
920
+ switch type {
921
+ case "geographicPolygon":
922
+ _ = try parts.map(mapShapeMapPolygon)
923
+ case "modelPolygon":
924
+ _ = try geometry.coordinate("base")
925
+ _ = try geometry.double("orientation", default: 0)
926
+ _ = try parts.map(mapShapeModelPolygon)
927
+ case "geographicPolyline":
928
+ _ = try parts.map(mapPolyline)
929
+ case "modelPolyline":
930
+ _ = try geometry.coordinate("base")
931
+ _ = try geometry.double("orientation", default: 0)
932
+ _ = try parts.map(modelPolyline)
933
+ default:
934
+ break
935
+ }
936
+ }
937
+
938
+ private func shapeLayerPass(_ value: String) throws
939
+ -> ShapeLayerPassType
940
+ {
941
+ switch value {
942
+ case "default": .default; case "overlay": .overlay; case "route": .route; default: throw shapeInvalid(
943
+ "Unknown shape layer pass \(value)."
944
+ )
945
+ }
946
+ }
947
+
948
+ private func polylineCap(_ value: String) throws
949
+ -> PolylineCapType
950
+ {
951
+ switch value {
952
+ case "butt": .butt; case "round": .round; case "square": .square; default: throw shapeInvalid(
953
+ "Unknown polyline cap \(value)."
954
+ )
955
+ }
956
+ }
957
+
958
+ private func shapeInterpolation(
959
+ duration: Int,
960
+ method value: String
961
+ ) throws -> AnimationInterpolation {
962
+ let method: InterpolationMethodType
963
+ switch value {
964
+ case "linear": method = .linear
965
+ case "cubicIn": method = .cubicIn
966
+ case "cubicOut": method = .cubicOut
967
+ case "cubicInOut": method = .cubicInOut
968
+ default: throw shapeInvalid("Unknown interpolation \(value).")
969
+ }
970
+ return AnimationInterpolation(duration: UInt32(duration), method: method)
971
+ }
972
+
973
+ func mapShapePolygonStyleSet(_ spec: [String: Any]) throws -> PolygonStyleSet {
974
+ try PolygonStyleSet(
975
+ styleSetID: spec.string("id"),
976
+ styles: spec.objectList("styles").map { style in
977
+ let levels = try style.objectList("levels")
978
+ guard !levels.isEmpty else { throw shapeInvalid("Polygon styles require levels.") }
979
+ return try PolygonStyle(styles: levels.map { level in
980
+ try PerLevelPolygonStyle(
981
+ color: level.color("fillColor"),
982
+ strokeWidth: shapePixelUInt(level.nonnegativeInt("strokeWidth", default: 0)),
983
+ strokeColor: level.color("strokeColor", default: .clear),
984
+ level: level.int("level")
985
+ )
986
+ })
987
+ }
988
+ )
989
+ }
990
+
991
+ func mapShapeWaveEffect(_ animation: [String: Any]) throws -> WaveAnimationEffect {
992
+ let frames = try animation.objectList("frames")
993
+ guard !frames.isEmpty else { throw shapeInvalid("Shape wave animation requires frames.") }
994
+ let effect = try WaveAnimationEffect(datas: frames.map { frame in
995
+ try WaveAnimationData(
996
+ startAlpha: Float(frame.unitDouble("startAlpha")),
997
+ endAlpha: Float(frame.unitDouble("endAlpha")),
998
+ startRadius: shapePixelFloat(frame.nonnegativeDouble("startRadius")),
999
+ endRadius: shapePixelFloat(frame.nonnegativeDouble("endRadius")),
1000
+ level: frame.int("level")
1001
+ )
1002
+ })
1003
+ effect.interpolation = try shapeInterpolation(
1004
+ duration: animation.minimumInt("duration", minimum: 1),
1005
+ method: animation.optionalString("interpolation") ?? "linear"
1006
+ )
1007
+ effect.playCount = try UInt(animation.minimumInt("repeat", default: 1, minimum: 1))
1008
+ effect.hideAtStop = try animation.bool("hideAtStop", default: false)
1009
+ return effect
1010
+ }
1011
+
1012
+ private func requireUniqueShape(_ values: [[String: Any]], noun: String) throws {
1013
+ let ids = try values.map { try $0.string("id") }; guard Set(ids).count == ids.count
1014
+ else { throw shapeInvalid("Duplicate \(noun) ID.") }
1015
+ guard !ids.contains(where: \.isEmpty)
1016
+ else { throw shapeInvalid("\(noun) ID must not be empty.") }
1017
+ }
1018
+
1019
+ private let defaultShapeLayerZIndex = 10000
1020
+
1021
+ private func shapeInvalid(_ message: String) -> KakaoMapException {
1022
+ KakaoMapException(
1023
+ "ERR_KAKAO_MAP_SHAPE_INVALID_ARGUMENT",
1024
+ message
1025
+ )
1026
+ }
1027
+
1028
+ private func shapeUnsupported(_ message: String) -> KakaoMapException {
1029
+ KakaoMapException(
1030
+ "ERR_KAKAO_MAP_SHAPE_UNSUPPORTED",
1031
+ message
1032
+ )
1033
+ }
1034
+
1035
+ private func shapeNotFound(_ noun: String, id: String) -> KakaoMapException {
1036
+ KakaoMapException(
1037
+ "ERR_KAKAO_MAP_SHAPE_NOT_FOUND",
1038
+ "Unknown \(noun) \(id)."
1039
+ )
1040
+ }
1041
+
1042
+ private extension [String: Any] {
1043
+ func object(_ key: String) throws
1044
+ -> [String: Any]
1045
+ {
1046
+ guard let value = self[key] as? [String: Any]
1047
+ else { throw shapeInvalid("\(key) must be an object.") }; return value
1048
+ }
1049
+
1050
+ func objectList(_ key: String) throws -> [[String: Any]] {
1051
+ guard let raw = self[key]
1052
+ else { return [] }; guard let value = raw as? [[String: Any]]
1053
+ else { throw shapeInvalid("\(key) must be an array of objects.") }; return value
1054
+ }
1055
+
1056
+ func anyList(_ key: String) throws -> [Any] {
1057
+ guard let value = self[key] as? [Any]
1058
+ else { throw shapeInvalid("\(key) must be an array.") }; return value
1059
+ }
1060
+
1061
+ func string(_ key: String) throws -> String {
1062
+ guard let value = self[key] as? String
1063
+ else { throw shapeInvalid("\(key) must be a string.") }; return value
1064
+ }
1065
+
1066
+ func optionalString(_ key: String) throws -> String? {
1067
+ guard let raw = self[key] else { return nil }
1068
+ guard let value = raw as? String
1069
+ else { throw shapeInvalid("\(key) must be a string.") }
1070
+ return value
1071
+ }
1072
+
1073
+ func stringList(_ key: String) throws -> [String] {
1074
+ guard let value = self[key] as? [String]
1075
+ else { throw shapeInvalid("\(key) must be an array of strings.") }; return value
1076
+ }
1077
+
1078
+ func bool(
1079
+ _ key: String,
1080
+ default fallback: Bool? = nil
1081
+ ) throws
1082
+ -> Bool
1083
+ {
1084
+ guard let raw = self[key] else {
1085
+ guard let fallback else { throw shapeInvalid("\(key) must be a boolean.") }
1086
+ return fallback
1087
+ }
1088
+ guard CFGetTypeID(raw as CFTypeRef) == CFBooleanGetTypeID(),
1089
+ let value = raw as? Bool
1090
+ else { throw shapeInvalid("\(key) must be a boolean.") }
1091
+ return value
1092
+ }
1093
+
1094
+ func double(
1095
+ _ key: String,
1096
+ default fallback: Double? = nil
1097
+ ) throws -> Double {
1098
+ guard let raw = self[key] else {
1099
+ guard let fallback else { throw shapeInvalid("\(key) must be finite.") }
1100
+ return fallback
1101
+ }
1102
+ guard let value = raw as? NSNumber,
1103
+ CFGetTypeID(value) != CFBooleanGetTypeID(),
1104
+ value.doubleValue.isFinite
1105
+ else { throw shapeInvalid("\(key) must be finite.") }
1106
+ return value.doubleValue
1107
+ }
1108
+
1109
+ func int(_ key: String, default fallback: Int? = nil) throws -> Int {
1110
+ let value = try double(
1111
+ key,
1112
+ default: fallback.map(Double.init)
1113
+ ); guard let integer = Int(exactly: value)
1114
+ else { throw shapeInvalid("\(key) must be an integer.") }; return integer
1115
+ }
1116
+
1117
+ func nonnegativeInt(
1118
+ _ key: String,
1119
+ default fallback: Int? = nil
1120
+ ) throws -> Int {
1121
+ let value = try int(
1122
+ key,
1123
+ default: fallback
1124
+ ); guard value >= 0 else { throw shapeInvalid("\(key) must be non-negative.") }; return value
1125
+ }
1126
+
1127
+ func minimumInt(
1128
+ _ key: String,
1129
+ default fallback: Int? = nil,
1130
+ minimum: Int
1131
+ ) throws -> Int {
1132
+ let value = try int(
1133
+ key,
1134
+ default: fallback
1135
+ ); guard value >= minimum
1136
+ else { throw shapeInvalid("\(key) must be at least \(minimum).") }; return value
1137
+ }
1138
+
1139
+ func positiveDouble(_ key: String) throws -> Double {
1140
+ let value = try double(key); guard value >
1141
+ 0 else { throw shapeInvalid("\(key) must be positive.") }; return value
1142
+ }
1143
+
1144
+ func nonnegativeDouble(_ key: String) throws -> Double {
1145
+ let value = try double(
1146
+ key
1147
+ ); guard value >=
1148
+ 0 else { throw shapeInvalid("\(key) must be non-negative.") }; return value
1149
+ }
1150
+
1151
+ func unitDouble(_ key: String) throws -> Double {
1152
+ let value = try double(key); guard (0 ... 1)
1153
+ .contains(value) else { throw shapeInvalid("\(key) must be between 0 and 1.") }; return value
1154
+ }
1155
+
1156
+ func color(
1157
+ _ key: String,
1158
+ default fallback: UIColor? = nil
1159
+ ) throws
1160
+ -> UIColor
1161
+ {
1162
+ guard let raw = self[key] else {
1163
+ guard let fallback else { throw shapeInvalid("\(key) must be a color.") }
1164
+ return fallback
1165
+ }
1166
+ guard let value = raw as? String
1167
+ else { throw shapeInvalid("\(key) must be a color.") }
1168
+ return try parseShapeColor(value)
1169
+ }
1170
+
1171
+ func coordinate(_ key: String) throws -> MapPoint {
1172
+ try object(key).coordinateValue()
1173
+ }
1174
+
1175
+ func coordinateValue() throws
1176
+ -> MapPoint
1177
+ {
1178
+ let latitude = try double("latitude"); let longitude =
1179
+ try double("longitude"); guard (-90 ... 90).contains(latitude),
1180
+ (-180 ... 180).contains(longitude)
1181
+ else { throw shapeInvalid("Invalid coordinate.") }; return MapPoint(
1182
+ longitude: longitude,
1183
+ latitude: latitude
1184
+ )
1185
+ }
1186
+
1187
+ func coordinateList(_ key: String) throws -> [MapPoint] {
1188
+ try anyList(key)
1189
+ .map {
1190
+ guard let value = $0 as? [String: Any]
1191
+ else { throw shapeInvalid("Coordinate must be an object.") }; return try value
1192
+ .coordinateValue()
1193
+ }
1194
+ }
1195
+
1196
+ func optionalCoordinateLists(_ key: String) throws -> [[MapPoint]]? {
1197
+ guard let raw = self[key]
1198
+ else { return nil }; guard let values = raw as? [[Any]]
1199
+ else { throw shapeInvalid("\(key) must be an array of arrays.") }; return try values
1200
+ .map {
1201
+ try $0
1202
+ .map {
1203
+ guard let value = $0 as? [String: Any]
1204
+ else { throw shapeInvalid("Coordinate must be an object.") }; return try value
1205
+ .coordinateValue()
1206
+ }
1207
+ }
1208
+ }
1209
+
1210
+ func pointList(_ key: String) throws -> [CGPoint] {
1211
+ try anyList(key)
1212
+ .map {
1213
+ guard let value = $0 as? [String: Any]
1214
+ else { throw shapeInvalid("Point must be an object.") }
1215
+ return try shapeModelMetersToSDKPoint(
1216
+ x: value.double("x"),
1217
+ y: value.double("y")
1218
+ )
1219
+ }
1220
+ }
1221
+
1222
+ func optionalPointLists(_ key: String) throws -> [[CGPoint]]? {
1223
+ guard let raw = self[key]
1224
+ else { return nil }; guard let values = raw as? [[Any]]
1225
+ else { throw shapeInvalid("\(key) must be an array of arrays.") }; return try values
1226
+ .map {
1227
+ try $0
1228
+ .map {
1229
+ guard let value = $0 as? [String: Any]
1230
+ else { throw shapeInvalid("Point must be an object.") }
1231
+ return try shapeModelMetersToSDKPoint(
1232
+ x: value.double("x"),
1233
+ y: value.double("y")
1234
+ )
1235
+ }
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ private let shapeEarthRadius = 6378137.0
1241
+ private let shapeModelDegreesPerMeter = 180 / (.pi * shapeEarthRadius)
1242
+ private let shapeModelPointConversionMessage = "Model point conversion must be finite."
1243
+
1244
+ private func shapeModelMetersToSDKPoint(x: Double, y: Double) throws -> CGPoint {
1245
+ let sdkX = x * shapeModelDegreesPerMeter
1246
+ let sdkY = y * shapeModelDegreesPerMeter
1247
+ guard sdkX.isFinite, sdkY.isFinite else {
1248
+ throw shapeInvalid(shapeModelPointConversionMessage)
1249
+ }
1250
+ return CGPoint(x: sdkX, y: sdkY)
1251
+ }
1252
+
1253
+ private func shapeSDKModelPointToMeters(_ point: CGPoint) throws -> CGPoint {
1254
+ let x = Double(point.x) / shapeModelDegreesPerMeter
1255
+ let y = Double(point.y) / shapeModelDegreesPerMeter
1256
+ guard x.isFinite, y.isFinite else {
1257
+ throw shapeInvalid(shapeModelPointConversionMessage)
1258
+ }
1259
+ return CGPoint(x: x, y: y)
1260
+ }
1261
+
1262
+ private func shapePixels(_ points: Double) throws -> Double {
1263
+ let pixels = MapUnits.pixels(points)
1264
+ guard pixels.isFinite else { throw shapeInvalid("Pixel dimensions must be finite.") }
1265
+ return pixels
1266
+ }
1267
+
1268
+ private func shapePixelUInt(_ points: Int) throws -> UInt {
1269
+ let pixels = try shapePixels(Double(points)).rounded()
1270
+ guard let integer = UInt(exactly: pixels)
1271
+ else { throw shapeInvalid("Pixel dimensions are out of range.") }
1272
+ return integer
1273
+ }
1274
+
1275
+ private func shapePixelFloat(_ points: Double) throws -> Float {
1276
+ let pixels = try shapePixels(points)
1277
+ guard abs(pixels) <= Double(Float.greatestFiniteMagnitude)
1278
+ else { throw shapeInvalid("Pixel dimensions are out of range.") }
1279
+ return Float(pixels)
1280
+ }
1281
+
1282
+ private func parseShapeColor(_ value: String) throws -> UIColor {
1283
+ guard value.range(of: "^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$", options: .regularExpression) != nil
1284
+ else { throw shapeInvalid("Colors must be #RRGGBB or #RRGGBBAA.") }
1285
+ let raw = String(value.dropFirst())
1286
+ let rgb = UInt64(raw.prefix(6), radix: 16) ?? 0
1287
+ let alpha = raw.count == 8 ? UInt64(raw.suffix(2), radix: 16) ?? 255 : 255
1288
+ return UIColor(
1289
+ red: CGFloat((rgb >> 16) & 255) / 255,
1290
+ green: CGFloat((rgb >> 8) & 255) / 255,
1291
+ blue: CGFloat(rgb & 255) / 255,
1292
+ alpha: CGFloat(alpha) / 255
1293
+ )
1294
+ }