@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,451 @@
1
+ import CoreFoundation
2
+ import CoreGraphics
3
+ import Foundation
4
+ import KakaoMapsSDK
5
+
6
+ private let cameraUnsupported = "ERR_KAKAO_MAP_UNSUPPORTED_PLATFORM_FEATURE"
7
+ private let cameraDestroyed = "ERR_KAKAO_MAP_VIEW_DESTROYED"
8
+ private let maximumPendingCameraOperations = 64
9
+
10
+ final class MapCameraCommands {
11
+ private weak var map: KakaoMap?
12
+ private let emitOperation: ([String: Any]) -> Void
13
+ private var generation: UInt = 0
14
+ private var operations: [String: UInt] = [:]
15
+ private var nextToken: UInt = 0
16
+ private var disposed = false
17
+
18
+ init(map: KakaoMap, emitOperation: @escaping ([String: Any]) -> Void) {
19
+ self.map = map
20
+ self.emitOperation = emitOperation
21
+ }
22
+
23
+ func command(_ command: [String: Any]) throws -> [String: Any] {
24
+ try requireMainThread()
25
+ let type = try command.string("type")
26
+ switch type {
27
+ case "getCapabilities": return capabilities()
28
+ case "getState": return try ["type": "state", "camera": state(requireMap())]
29
+ case "moveToHeight": return try moveToHeight(command)
30
+ case "transform": return try transform(command)
31
+ case "fit": return try fit(command)
32
+ case "canShow": return try canShow(command)
33
+ case "heightAtLevel": return try heightAtLevel(command)
34
+ case "resetOrientation": return try resetOrientation(command)
35
+ case "setBackToNorthDuringAutoElevation": return try setBackToNorth(command)
36
+ case "setReferencePointLock": return try setReferencePointLock(command)
37
+ default: throw invalidCamera("Unsupported advanced camera command: \(type).")
38
+ }
39
+ }
40
+
41
+ func dispose() {
42
+ precondition(Thread.isMainThread, "MapCameraCommands.dispose() must run on the main thread.")
43
+ disposed = true
44
+ generation &+= 1
45
+ operations.removeAll()
46
+ map = nil
47
+ }
48
+
49
+ func supersedePendingOperations() {
50
+ supersedeOperations(except: nil)
51
+ }
52
+
53
+ private func moveToHeight(_ command: [String: Any]) throws -> [String: Any] {
54
+ let camera = try command.object("camera")
55
+ let latitude = try camera.finiteNumber("latitude")
56
+ let longitude = try camera.finiteNumber("longitude")
57
+ let height = try camera.finiteNumber("height")
58
+ let tilt = try camera.finiteNumber("tilt")
59
+ let rotation = try camera.finiteNumber("rotation")
60
+ try validateCoordinate(latitude: latitude, longitude: longitude)
61
+ guard height > 0, (0 ... 90).contains(tilt) else {
62
+ throw invalidCamera(
63
+ "Camera height must be positive and tilt must be between 0 and 90 degrees."
64
+ )
65
+ }
66
+ let position = CameraPosition(
67
+ target: MapPoint(longitude: longitude, latitude: latitude), height: height,
68
+ rotation: rotation.normalizedDegrees * .pi / 180, tilt: tilt * .pi / 180
69
+ )
70
+ return try dispatch(CameraUpdate.make(cameraPosition: position), command: command)
71
+ }
72
+
73
+ private func transform(_ command: [String: Any]) throws -> [String: Any] {
74
+ let delta = try command.object("delta")
75
+ let supportedFields: Set = ["latitude", "longitude", "height", "rotation", "tilt"]
76
+ guard !delta.isEmpty, Set(delta.keys).isSubset(of: supportedFields) else {
77
+ throw invalidCamera("Camera transform delta must contain only supported transform fields.")
78
+ }
79
+ let transform = try CameraTransform(
80
+ deltaPos: CameraTransformDelta(
81
+ deltaLon: delta.optionalFiniteNumber("longitude") ?? 0,
82
+ deltaLat: delta.optionalFiniteNumber("latitude") ?? 0
83
+ ),
84
+ deltaHeight: delta.optionalFiniteNumber("height") ?? 0,
85
+ deltaRotation: (delta.optionalFiniteNumber("rotation") ?? 0) * (.pi / 180),
86
+ deltaTilt: (delta.optionalFiniteNumber("tilt") ?? 0) * (.pi / 180)
87
+ )
88
+ return try dispatch(CameraUpdate.make(transform: transform), command: command)
89
+ }
90
+
91
+ private func fit(_ command: [String: Any]) throws -> [String: Any] {
92
+ let points = try command.coordinates("coordinates")
93
+ if let padding = try command.optionalFiniteNumber("padding"), padding != 0 {
94
+ throw unsupportedCamera("Fit padding is not exposed by Kakao Maps iOS SDK 2.12.19.")
95
+ }
96
+ let map = try requireMap()
97
+ let levelLimit = try command.optionalInteger("levelLimit")
98
+ if let levelLimit, !(map.cameraMinLevel ... map.cameraMaxLevel).contains(levelLimit) {
99
+ throw invalidCamera("Camera fit levelLimit is outside the configured camera range.")
100
+ }
101
+ return try dispatch(
102
+ CameraUpdate.make(area: AreaRect(points: points), levelLimit: levelLimit ?? -1),
103
+ command: command
104
+ )
105
+ }
106
+
107
+ private func canShow(_ command: [String: Any]) throws -> [String: Any] {
108
+ let map = try requireMap()
109
+ let level = try command.integer("zoomLevel")
110
+ guard (map.cameraMinLevel ... map.cameraMaxLevel).contains(level) else {
111
+ throw invalidCamera("canShow zoomLevel is outside the configured camera range.")
112
+ }
113
+ let rotation = try command.optionalFiniteNumber("rotation") ?? 0
114
+ let weight = try command.optionalFiniteNumber("weight") ?? 1
115
+ guard weight > 0, weight <= Double(Float.greatestFiniteMagnitude) else {
116
+ throw invalidCamera("canShow weight must be a positive finite Float value.")
117
+ }
118
+ let rect: CGRect? = try command["rect"].map { raw in
119
+ guard let value = raw as? [String: Any]
120
+ else { throw invalidCamera("rect must be an object.") }
121
+ let x = try value.finiteNumber("x")
122
+ let y = try value.finiteNumber("y")
123
+ let width = try value.finiteNumber("width")
124
+ let height = try value.finiteNumber("height")
125
+ guard x >= 0, y >= 0, width >= 0, height >= 0,
126
+ x + width <= map.viewRect.width, y + height <= map.viewRect.height
127
+ else {
128
+ throw invalidCamera("rect must be inside the map viewport.")
129
+ }
130
+ return CGRect(x: x, y: y, width: width, height: height)
131
+ }
132
+ return try [
133
+ "type": "canShow",
134
+ "value": map.canShow(
135
+ mapPoints: command.coordinates("coordinates"), atLevel: level,
136
+ rotationAngle: rotation.normalizedDegrees * .pi / 180, weight: Float(weight), inRect: rect
137
+ )
138
+ ]
139
+ }
140
+
141
+ private func heightAtLevel(_ command: [String: Any]) throws -> [String: Any] {
142
+ let map = try requireMap()
143
+ let level = try command.integer("zoomLevel")
144
+ guard (map.minLevel ... map.maxLevel).contains(level) else {
145
+ throw invalidCamera("heightAtLevel zoomLevel is outside the SDK map range.")
146
+ }
147
+ return ["type": "heightAtLevel", "height": map.heightAtLevel(level)]
148
+ }
149
+
150
+ private func resetOrientation(_ command: [String: Any]) throws -> [String: Any] {
151
+ let map = try requireMap()
152
+ let parsed = try animation(command)
153
+ let operation = try beginOperation(command, animation: parsed)
154
+ if parsed.options.consecutive.boolValue == false {
155
+ supersedeOperations(except: operation?.id)
156
+ }
157
+ map.resetCameraOrientation(parsed.options) { [weak self] in
158
+ self?.complete(operation)
159
+ }
160
+ return dispatched(operation?.id)
161
+ }
162
+
163
+ private func setBackToNorth(_ command: [String: Any]) throws -> [String: Any] {
164
+ try requireMap().setBackToNorthDuringAutoElevation(command.bool("enabled"))
165
+ return ["type": "applied"]
166
+ }
167
+
168
+ private func setReferencePointLock(_ command: [String: Any]) throws -> [String: Any] {
169
+ let map = try requireMap()
170
+ let gesture = try referenceGesture(command.string("gesture"))
171
+ if try command.bool("locked") {
172
+ map.lockReferencePoint(forGesture: gesture)
173
+ } else {
174
+ map.unlockReferencePoint(forGesture: gesture)
175
+ }
176
+ return ["type": "applied"]
177
+ }
178
+
179
+ private func dispatch(_ update: CameraUpdate, command: [String: Any]) throws -> [String: Any] {
180
+ let map = try requireMap()
181
+ let parsed = try animation(command)
182
+ let operation = try beginOperation(command, animation: parsed)
183
+ if parsed.options.durationInMillis == 0 || parsed.options.consecutive.boolValue == false {
184
+ supersedeOperations(except: operation?.id)
185
+ }
186
+ let callback: () -> Void = { [weak self] in
187
+ self?.complete(operation)
188
+ }
189
+ if parsed.options.durationInMillis == 0 {
190
+ map.moveCamera(update, callback: callback)
191
+ } else {
192
+ map.animateCamera(cameraUpdate: update, options: parsed.options, callback: callback)
193
+ }
194
+ return dispatched(operation?.id)
195
+ }
196
+
197
+ private func animation(_ command: [String: Any]) throws -> (
198
+ options: CameraAnimationOptions,
199
+ supplied: Bool
200
+ ) {
201
+ guard let raw = command["animation"] else {
202
+ return (
203
+ CameraAnimationOptions(
204
+ autoElevation: ObjCBool(false),
205
+ consecutive: ObjCBool(false),
206
+ durationInMillis: 0
207
+ ),
208
+ false
209
+ )
210
+ }
211
+ guard let value = raw as? [String: Any] else {
212
+ throw invalidCamera("animation must be an object.")
213
+ }
214
+ let duration = try value.integer("duration")
215
+ guard (0 ... Int(Int32.max)).contains(duration) else {
216
+ throw invalidCamera(
217
+ "Animation duration must be an integer between 0 and 2147483647 milliseconds."
218
+ )
219
+ }
220
+ return try (
221
+ CameraAnimationOptions(
222
+ autoElevation: ObjCBool(value.optionalBool("autoElevation") ?? false),
223
+ consecutive: ObjCBool(value.optionalBool("consecutive") ?? false),
224
+ durationInMillis: UInt(duration)
225
+ ),
226
+ true
227
+ )
228
+ }
229
+
230
+ private func beginOperation(
231
+ _ command: [String: Any],
232
+ animation: (options: CameraAnimationOptions, supplied: Bool)
233
+ ) throws -> (id: String, token: UInt, generation: UInt)? {
234
+ guard let rawID = command["operationId"] else { return nil }
235
+ guard let id = rawID as? String, !id.isEmpty else {
236
+ throw invalidCamera("operationId must be a non-empty string.")
237
+ }
238
+ guard animation.supplied, animation.options.durationInMillis > 0 else {
239
+ throw invalidCamera("operationId requires an animation with a positive duration.")
240
+ }
241
+ guard operations[id] == nil else {
242
+ throw invalidCamera("operationId must be unique while pending.")
243
+ }
244
+ guard operations.count < maximumPendingCameraOperations else {
245
+ throw KakaoMapException(
246
+ "ERR_KAKAO_MAP_CAMERA_OPERATION_LIMIT",
247
+ "Too many camera operations are pending."
248
+ )
249
+ }
250
+ nextToken &+= 1
251
+ operations[id] = nextToken
252
+ return (id, nextToken, generation)
253
+ }
254
+
255
+ private func supersedeOperations(except preservedID: String?) {
256
+ for id in Array(operations.keys) where id != preservedID {
257
+ operations[id] = nil
258
+ emitOperation(["operationId": id, "status": "superseded"])
259
+ }
260
+ }
261
+
262
+ private func complete(_ operation: (id: String, token: UInt, generation: UInt)?) {
263
+ guard let operation else { return }
264
+ let body = { [weak self] in
265
+ guard let self, !self.disposed, generation == operation.generation,
266
+ operations[operation.id] == operation.token else { return }
267
+ operations[operation.id] = nil
268
+ guard let map else { return }
269
+ emitOperation([
270
+ "operationId": operation.id,
271
+ "status": "completed",
272
+ "camera": state(map)
273
+ ])
274
+ }
275
+ if Thread.isMainThread {
276
+ body()
277
+ } else {
278
+ DispatchQueue.main.async(execute: body)
279
+ }
280
+ }
281
+
282
+ private func state(_ map: KakaoMap) -> [String: Any] {
283
+ let margins = map.margins
284
+ let center = CGPoint(
285
+ x: (margins.left + map.viewRect.width - margins.right) / 2,
286
+ y: (margins.top + map.viewRect.height - margins.bottom) / 2
287
+ )
288
+ let coordinate = map.getPosition(center).wgsCoord
289
+ return [
290
+ "latitude": coordinate.latitude,
291
+ "longitude": coordinate.longitude,
292
+ "zoomLevel": map.zoomLevel,
293
+ "height": map.cameraHeight,
294
+ "tilt": map.tiltAngle * 180 / .pi,
295
+ "rotation": (map.rotationAngle * 180 / .pi).normalizedDegrees
296
+ ]
297
+ }
298
+
299
+ private func capabilities() -> [String: Any] {
300
+ [
301
+ "type": "capabilities",
302
+ "capabilities": [
303
+ "transform": true,
304
+ "absoluteHeight": true,
305
+ "heightAtLevel": true,
306
+ "fit": ["padding": false, "levelLimit": true],
307
+ "canShow": ["basic": true, "rotation": true, "weight": true, "rect": true],
308
+ "resetOrientation": true,
309
+ "backToNorthDuringAutoElevation": true,
310
+ "animation": [
311
+ "autoElevation": true,
312
+ "consecutive": true,
313
+ "operationCompletion": true
314
+ ],
315
+ "referencePoint": ["supported": true, "gestures": referenceGestures]
316
+ ]
317
+ ]
318
+ }
319
+
320
+ private func requireMap() throws -> KakaoMap {
321
+ guard !disposed, let map else {
322
+ throw KakaoMapException(cameraDestroyed, "The map view was destroyed.")
323
+ }
324
+ return map
325
+ }
326
+ }
327
+
328
+ private let referenceGestures = ["rotate", "zoom", "tilt", "rotateZoom", "oneFingerZoom"]
329
+
330
+ private func referenceGesture(_ value: String) throws -> GestureType {
331
+ switch value {
332
+ case "rotate": .rotate
333
+ case "zoom": .zoom
334
+ case "tilt": .tilt
335
+ case "rotateZoom": .rotateZoom
336
+ case "oneFingerZoom": .oneFingerZoom
337
+ default: throw invalidCamera("Unsupported reference-point gesture: \(value).")
338
+ }
339
+ }
340
+
341
+ private func dispatched(_ operationID: String?) -> [String: Any] {
342
+ var payload: [String: Any] = ["type": "dispatched"]
343
+ if let operationID {
344
+ payload["operationId"] = operationID
345
+ }
346
+ return payload
347
+ }
348
+
349
+ private func validateCoordinate(latitude: Double, longitude: Double) throws {
350
+ guard (-90 ... 90).contains(latitude), (-180 ... 180).contains(longitude) else {
351
+ throw invalidCamera("Coordinate latitude or longitude is outside its valid range.")
352
+ }
353
+ }
354
+
355
+ private extension [String: Any] {
356
+ func string(_ name: String) throws -> String {
357
+ guard let value = self[name] as? String else {
358
+ throw invalidCamera("\(name) must be a string.")
359
+ }
360
+ return value
361
+ }
362
+
363
+ func bool(_ name: String) throws -> Bool {
364
+ guard let value = self[name] as? NSNumber, CFGetTypeID(value) == CFBooleanGetTypeID() else {
365
+ throw invalidCamera("\(name) must be a boolean.")
366
+ }
367
+ return value.boolValue
368
+ }
369
+
370
+ func optionalBool(_ name: String) throws -> Bool? {
371
+ guard let raw = self[name] else { return nil }
372
+ guard let value = raw as? NSNumber, CFGetTypeID(value) == CFBooleanGetTypeID() else {
373
+ throw invalidCamera("\(name) must be a boolean.")
374
+ }
375
+ return value.boolValue
376
+ }
377
+
378
+ func object(_ name: String) throws -> [String: Any] {
379
+ guard let value = self[name] as? [String: Any] else {
380
+ throw invalidCamera("\(name) must be an object.")
381
+ }
382
+ return value
383
+ }
384
+
385
+ func finiteNumber(_ name: String) throws -> Double {
386
+ guard let value = try optionalFiniteNumber(name) else {
387
+ throw invalidCamera("\(name) must be a finite number.")
388
+ }
389
+ return value
390
+ }
391
+
392
+ func optionalFiniteNumber(_ name: String) throws -> Double? {
393
+ guard let raw = self[name] else { return nil }
394
+ guard let number = raw as? NSNumber, CFGetTypeID(number) != CFBooleanGetTypeID() else {
395
+ throw invalidCamera("\(name) must be a finite number.")
396
+ }
397
+ let value = number.doubleValue
398
+ guard value.isFinite else { throw invalidCamera("\(name) must be a finite number.") }
399
+ return value
400
+ }
401
+
402
+ func integer(_ name: String) throws -> Int {
403
+ guard let value = try optionalInteger(name) else {
404
+ throw invalidCamera("\(name) must be an integer.")
405
+ }
406
+ return value
407
+ }
408
+
409
+ func optionalInteger(_ name: String) throws -> Int? {
410
+ guard let value = try optionalFiniteNumber(name) else { return nil }
411
+ guard let integer = Int(exactly: value) else {
412
+ throw invalidCamera("\(name) must be an integer.")
413
+ }
414
+ return integer
415
+ }
416
+
417
+ func coordinates(_ name: String) throws -> [MapPoint] {
418
+ guard let values = self[name] as? [[String: Any]], !values.isEmpty else {
419
+ throw invalidCamera("\(name) must contain at least one coordinate.")
420
+ }
421
+ return try values.map { coordinate in
422
+ let latitude = try coordinate.finiteNumber("latitude")
423
+ let longitude = try coordinate.finiteNumber("longitude")
424
+ try validateCoordinate(latitude: latitude, longitude: longitude)
425
+ return MapPoint(longitude: longitude, latitude: latitude)
426
+ }
427
+ }
428
+ }
429
+
430
+ private extension Double {
431
+ var normalizedDegrees: Double {
432
+ (truncatingRemainder(dividingBy: 360) + 360).truncatingRemainder(dividingBy: 360)
433
+ }
434
+ }
435
+
436
+ private func requireMainThread() throws {
437
+ guard Thread.isMainThread else {
438
+ throw KakaoMapException(
439
+ "ERR_KAKAO_MAP_WRONG_THREAD",
440
+ "Advanced camera commands must run on the main thread."
441
+ )
442
+ }
443
+ }
444
+
445
+ private func invalidCamera(_ message: String) -> KakaoMapException {
446
+ KakaoMapException(invalidCameraArgument, message)
447
+ }
448
+
449
+ private func unsupportedCamera(_ message: String) -> KakaoMapException {
450
+ KakaoMapException(cameraUnsupported, message)
451
+ }