@react-native-kakao/map 2.2.6 → 3.1.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +5 -38
  3. package/RNCKakaoMap.podspec +16 -52
  4. package/android/build.gradle +12 -107
  5. package/android/consumer-rules.pro +2 -0
  6. package/android/src/main/AndroidManifest.xml +3 -1
  7. package/android/src/main/java/net/mjstudio/rnkakao/map/CameraOptions.kt +61 -0
  8. package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapModule.kt +131 -0
  9. package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapView.kt +1257 -0
  10. package/android/src/main/java/net/mjstudio/rnkakao/map/MapCameraCommands.kt +306 -0
  11. package/android/src/main/java/net/mjstudio/rnkakao/map/MapDimScreen.kt +431 -0
  12. package/android/src/main/java/net/mjstudio/rnkakao/map/MapGui.kt +899 -0
  13. package/android/src/main/java/net/mjstudio/rnkakao/map/MapImageLoader.kt +232 -0
  14. package/android/src/main/java/net/mjstudio/rnkakao/map/MapLabels.kt +1572 -0
  15. package/android/src/main/java/net/mjstudio/rnkakao/map/MapOptions.kt +360 -0
  16. package/android/src/main/java/net/mjstudio/rnkakao/map/MapRoutes.kt +805 -0
  17. package/android/src/main/java/net/mjstudio/rnkakao/map/MapShapes.kt +1096 -0
  18. package/android/src/main/java/net/mjstudio/rnkakao/map/MapUtilities.kt +495 -0
  19. package/expo-module.config.json +5 -0
  20. package/ios/CameraOptions.swift +41 -0
  21. package/ios/KakaoMapModule.swift +204 -0
  22. package/ios/KakaoMapView.swift +1018 -0
  23. package/ios/MapCameraCommands.swift +451 -0
  24. package/ios/MapDimScreen.swift +463 -0
  25. package/ios/MapGui.swift +1099 -0
  26. package/ios/MapImageLoader.swift +177 -0
  27. package/ios/MapLabels.swift +1676 -0
  28. package/ios/MapOptions.swift +374 -0
  29. package/ios/MapRoutes.swift +903 -0
  30. package/ios/MapShapes.swift +1294 -0
  31. package/ios/MapUnits.swift +20 -0
  32. package/ios/MapUtilities.swift +365 -0
  33. package/lib/commonjs/index.js +180 -29
  34. package/lib/commonjs/index.js.map +1 -1
  35. package/lib/commonjs/index.web.js +18 -0
  36. package/lib/commonjs/index.web.js.map +1 -0
  37. package/lib/commonjs/package.json +1 -0
  38. package/lib/commonjs/types-assets.js +2 -0
  39. package/lib/commonjs/types-assets.js.map +1 -0
  40. package/lib/commonjs/types-camera.js +6 -0
  41. package/lib/commonjs/types-camera.js.map +1 -0
  42. package/lib/commonjs/types-dim.js +6 -0
  43. package/lib/commonjs/types-dim.js.map +1 -0
  44. package/lib/commonjs/types-gui.js +6 -0
  45. package/lib/commonjs/types-gui.js.map +1 -0
  46. package/lib/commonjs/types-labels.js +6 -0
  47. package/lib/commonjs/types-labels.js.map +1 -0
  48. package/lib/commonjs/types-routes.js +6 -0
  49. package/lib/commonjs/types-routes.js.map +1 -0
  50. package/lib/commonjs/types-shapes.js +6 -0
  51. package/lib/commonjs/types-shapes.js.map +1 -0
  52. package/lib/commonjs/types-utilities.js +6 -0
  53. package/lib/commonjs/types-utilities.js.map +1 -0
  54. package/lib/commonjs/types.js +6 -0
  55. package/lib/commonjs/types.js.map +1 -0
  56. package/lib/module/index.js +180 -22
  57. package/lib/module/index.js.map +1 -1
  58. package/lib/module/index.web.js +13 -0
  59. package/lib/module/index.web.js.map +1 -0
  60. package/lib/module/types-assets.js +2 -0
  61. package/lib/module/types-assets.js.map +1 -0
  62. package/lib/module/types-camera.js +4 -0
  63. package/lib/module/types-camera.js.map +1 -0
  64. package/lib/module/types-dim.js +4 -0
  65. package/lib/module/types-dim.js.map +1 -0
  66. package/lib/module/types-gui.js +4 -0
  67. package/lib/module/types-gui.js.map +1 -0
  68. package/lib/module/types-labels.js +4 -0
  69. package/lib/module/types-labels.js.map +1 -0
  70. package/lib/module/types-routes.js +4 -0
  71. package/lib/module/types-routes.js.map +1 -0
  72. package/lib/module/types-shapes.js +4 -0
  73. package/lib/module/types-shapes.js.map +1 -0
  74. package/lib/module/types-utilities.js +4 -0
  75. package/lib/module/types-utilities.js.map +1 -0
  76. package/lib/module/types.js +4 -0
  77. package/lib/module/types.js.map +1 -0
  78. package/lib/typescript/src/index.d.ts +59 -9
  79. package/lib/typescript/src/index.d.ts.map +1 -1
  80. package/lib/typescript/src/types-assets.d.ts +9 -0
  81. package/lib/typescript/src/types-assets.d.ts.map +1 -0
  82. package/lib/typescript/src/types-camera.d.ts +123 -0
  83. package/lib/typescript/src/types-camera.d.ts.map +1 -0
  84. package/lib/typescript/src/types-dim.d.ts +54 -0
  85. package/lib/typescript/src/types-dim.d.ts.map +1 -0
  86. package/lib/typescript/src/types-gui.d.ts +189 -0
  87. package/lib/typescript/src/types-gui.d.ts.map +1 -0
  88. package/lib/typescript/src/types-labels.d.ts +246 -0
  89. package/lib/typescript/src/types-labels.d.ts.map +1 -0
  90. package/lib/typescript/src/types-routes.d.ts +162 -0
  91. package/lib/typescript/src/types-routes.d.ts.map +1 -0
  92. package/lib/typescript/src/types-shapes.d.ts +253 -0
  93. package/lib/typescript/src/types-shapes.d.ts.map +1 -0
  94. package/lib/typescript/src/types-utilities.d.ts +130 -0
  95. package/lib/typescript/src/types-utilities.d.ts.map +1 -0
  96. package/lib/typescript/src/types.d.ts +222 -0
  97. package/lib/typescript/src/types.d.ts.map +1 -0
  98. package/package.json +24 -77
  99. package/src/index.tsx +322 -0
  100. package/src/index.web.tsx +46 -0
  101. package/src/types-assets.ts +8 -0
  102. package/src/types-camera.ts +97 -0
  103. package/src/types-dim.ts +54 -0
  104. package/src/types-gui.ts +171 -0
  105. package/src/types-labels.ts +207 -0
  106. package/src/types-routes.ts +118 -0
  107. package/src/types-shapes.ts +244 -0
  108. package/src/types-utilities.ts +115 -0
  109. package/src/types.ts +223 -0
  110. package/android/gradle.properties +0 -1
  111. package/android/src/main/AndroidManifestNew.xml +0 -2
  112. package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapModule.kt +0 -25
  113. package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapPackage.kt +0 -35
  114. package/android/src/newarch/KakaoMapSpec.kt +0 -7
  115. package/android/src/oldarch/KakaoMapSpec.kt +0 -14
  116. package/ios/Component/Map/RNCKakaoMapView.h +0 -23
  117. package/ios/Component/Map/RNCKakaoMapView.mm +0 -209
  118. package/ios/Component/Map/RNCKakaoMapViewManager.mm +0 -19
  119. package/ios/NativeModule/RNCKakaoMap.h +0 -7
  120. package/ios/NativeModule/RNCKakaoMap.mm +0 -19
  121. package/ios/NativeModule/RNCKakaoMapManager.swift +0 -7
  122. package/ios/Util/Util.h +0 -97
  123. package/lib/commonjs/component/KakaoMapView.js +0 -54
  124. package/lib/commonjs/component/KakaoMapView.js.map +0 -1
  125. package/lib/commonjs/spec/NativeKakaoMap.js +0 -9
  126. package/lib/commonjs/spec/NativeKakaoMap.js.map +0 -1
  127. package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js +0 -15
  128. package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
  129. package/lib/commonjs/type/type.js +0 -2
  130. package/lib/commonjs/type/type.js.map +0 -1
  131. package/lib/commonjs/util/Const.js +0 -14
  132. package/lib/commonjs/util/Const.js.map +0 -1
  133. package/lib/commonjs/util/Util.js +0 -15
  134. package/lib/commonjs/util/Util.js.map +0 -1
  135. package/lib/module/component/KakaoMapView.js +0 -48
  136. package/lib/module/component/KakaoMapView.js.map +0 -1
  137. package/lib/module/spec/NativeKakaoMap.js +0 -3
  138. package/lib/module/spec/NativeKakaoMap.js.map +0 -1
  139. package/lib/module/spec/RNCKakaoMapViewNativeComponent.js +0 -10
  140. package/lib/module/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
  141. package/lib/module/type/type.js +0 -2
  142. package/lib/module/type/type.js.map +0 -1
  143. package/lib/module/util/Const.js +0 -8
  144. package/lib/module/util/Const.js.map +0 -1
  145. package/lib/module/util/Util.js +0 -9
  146. package/lib/module/util/Util.js.map +0 -1
  147. package/lib/typescript/src/component/KakaoMapView.d.ts +0 -25
  148. package/lib/typescript/src/component/KakaoMapView.d.ts.map +0 -1
  149. package/lib/typescript/src/spec/NativeKakaoMap.d.ts +0 -7
  150. package/lib/typescript/src/spec/NativeKakaoMap.d.ts.map +0 -1
  151. package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts +0 -32
  152. package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts.map +0 -1
  153. package/lib/typescript/src/type/type.d.ts +0 -8
  154. package/lib/typescript/src/type/type.d.ts.map +0 -1
  155. package/lib/typescript/src/util/Const.d.ts +0 -8
  156. package/lib/typescript/src/util/Const.d.ts.map +0 -1
  157. package/lib/typescript/src/util/Util.d.ts +0 -3
  158. package/lib/typescript/src/util/Util.d.ts.map +0 -1
  159. package/src/component/KakaoMapView.tsx +0 -82
  160. package/src/index.ts +0 -43
  161. package/src/spec/NativeKakaoMap.ts +0 -8
  162. package/src/spec/RNCKakaoMapViewNativeComponent.ts +0 -38
  163. package/src/type/type.ts +0 -7
  164. package/src/util/Const.ts +0 -7
  165. package/src/util/Util.ts +0 -11
@@ -0,0 +1,463 @@
1
+ import CoreFoundation
2
+ import KakaoMapsSDK
3
+ import UIKit
4
+
5
+ private let mapDimInvalidArgument = "ERR_KAKAO_MAP_DIM_INVALID_ARGUMENT"
6
+ private let mapDimNotFound = "ERR_KAKAO_MAP_DIM_NOT_FOUND"
7
+ private let mapDimUnsupported = "ERR_KAKAO_MAP_UNSUPPORTED_PLATFORM_FEATURE"
8
+
9
+ final class MapDimScreen {
10
+ typealias EventEmitter = ([String: Any]) -> Void
11
+
12
+ private weak var map: KakaoMap?
13
+ private let emit: EventEmitter
14
+ private var geographicHighlights: [String: MapPolygonShape] = [:]
15
+ private var modelHighlights: [String: PolygonShape] = [:]
16
+ private var animations: [String: ShapeAnimator] = [:]
17
+ private var styleFingerprints: [String: String] = [:]
18
+ private var isDisposed = false
19
+
20
+ init(map: KakaoMap, emit: @escaping EventEmitter) {
21
+ self.map = map
22
+ self.emit = emit
23
+ }
24
+
25
+ func apply(_ options: [String: Any]) throws {
26
+ guard !isDisposed, let dimScreen = map?.dimScreen else {
27
+ throw KakaoMapException(mapDimNotFound, "The map is no longer available.")
28
+ }
29
+ let parsed = try parse(options)
30
+ var stylesToAdd: [(id: String, fingerprint: String, native: PolygonStyleSet)] = []
31
+ for style in parsed.styleSets {
32
+ let id = try string(style, "id")
33
+ let fingerprint = try fingerprint(style)
34
+ if let current = styleFingerprints[id], current != fingerprint {
35
+ throw exception(
36
+ "Dim-screen style \(id) cannot be replaced during a map engine lifetime; use a new style ID."
37
+ )
38
+ }
39
+ if styleFingerprints[id] == nil {
40
+ try stylesToAdd.append((id, fingerprint, mapShapePolygonStyleSet(style)))
41
+ }
42
+ }
43
+ let availableStyleIds = Set(styleFingerprints.keys).union(stylesToAdd.map(\.id))
44
+ let highlightIds = try Set(parsed.highlights.map { try string($0, "id") })
45
+ try parsed.highlights
46
+ .forEach { try validateHighlight($0, availableStyleIds: availableStyleIds) }
47
+ try parsed.animations.forEach { animation in
48
+ _ = try mapShapeWaveEffect(animation)
49
+ let id = try string(animation, "animationId")
50
+ let shapeIds = try strings(animation, "shapeIds")
51
+ guard !shapeIds.isEmpty
52
+ else { throw exception("Dim-screen animation \(id) requires a target.") }
53
+ try unique(shapeIds, name: "animation target")
54
+ for shapeId in shapeIds where !highlightIds.contains(shapeId) {
55
+ throw exception("Dim-screen animation \(id) references unknown highlight \(shapeId).")
56
+ }
57
+ }
58
+ clearObjects()
59
+ do {
60
+ for style in stylesToAdd {
61
+ dimScreen.addPolygonStyleSet(style.native)
62
+ styleFingerprints[style.id] = style.fingerprint
63
+ }
64
+ for highlight in parsed.highlights {
65
+ try addHighlight(highlight, to: dimScreen)
66
+ }
67
+ for animation in parsed.animations {
68
+ try addAnimation(animation, to: dimScreen)
69
+ }
70
+ dimScreen.color = try color(parsed.color)
71
+ dimScreen.cover = try cover(parsed.cover)
72
+ dimScreen.isEnabled = parsed.enabled
73
+ } catch {
74
+ clearObjects()
75
+ throw error
76
+ }
77
+ }
78
+
79
+ func setHighlightVisible(id: String, visible: Bool) throws {
80
+ guard !isDisposed, map != nil else {
81
+ throw KakaoMapException(mapDimNotFound, "The dim-screen manager was disposed.")
82
+ }
83
+ if let shape = geographicHighlights[id] {
84
+ visible ? shape.show() : shape.hide()
85
+ return
86
+ }
87
+ if let shape = modelHighlights[id] {
88
+ visible ? shape.show() : shape.hide()
89
+ return
90
+ }
91
+ throw KakaoMapException(mapDimNotFound, "Dim-screen highlight \(id) was not found.")
92
+ }
93
+
94
+ func startAnimation(id: String) throws {
95
+ try requireAnimation(id).start()
96
+ }
97
+
98
+ func stopAnimation(id: String) throws {
99
+ try requireAnimation(id).stop()
100
+ }
101
+
102
+ func status() throws -> [String: Any] {
103
+ guard let dimScreen = map?.dimScreen else {
104
+ throw KakaoMapException(mapDimNotFound, "The map is no longer available.")
105
+ }
106
+ let geographic = (dimScreen.getAllHighlightMapPolygonShapes() ?? []).map { shape in
107
+ [
108
+ "id": shape.shapeID,
109
+ "visible": shape.isShow,
110
+ "geometryType": "geographicPolygon"
111
+ ] as [String: Any]
112
+ }
113
+ let model = (dimScreen.getAllHighlightPolygonShapes() ?? []).map { shape in
114
+ [
115
+ "id": shape.shapeID,
116
+ "visible": shape.isShow,
117
+ "geometryType": "modelPolygon"
118
+ ] as [String: Any]
119
+ }
120
+ let animationStatus = animations.keys.sorted().compactMap { id -> [String: Any]? in
121
+ guard let animation = dimScreen.getShapeAnimator(animatorID: id) else { return nil }
122
+ return ["id": animation.animatorID, "running": animation.isStart]
123
+ }
124
+ return [
125
+ "enabled": dimScreen.isEnabled,
126
+ "color": colorString(dimScreen.color),
127
+ "cover": coverName(dimScreen.cover),
128
+ "highlights": (geographic + model).sorted {
129
+ ($0["id"] as? String ?? "") < ($1["id"] as? String ?? "")
130
+ },
131
+ "animations": animationStatus,
132
+ "counts": [
133
+ "highlights": geographic.count + model.count,
134
+ "animations": animationStatus.count
135
+ ],
136
+ "capabilities": [
137
+ "coversRoute": false,
138
+ "modelHighlights": true,
139
+ "modelOrientation": true,
140
+ "animations": true,
141
+ "animationDuration": true,
142
+ "animationState": true,
143
+ "nativeConfigurationState": true
144
+ ]
145
+ ]
146
+ }
147
+
148
+ func dispose() {
149
+ guard !isDisposed else { return }
150
+ isDisposed = true
151
+ map?.dimScreen.isEnabled = false
152
+ clearObjects()
153
+ map = nil
154
+ }
155
+
156
+ private func addHighlight(_ value: [String: Any], to dimScreen: DimScreen) throws {
157
+ let id = try string(value, "id")
158
+ let styleSetId = try string(value, "styleSetId")
159
+ guard styleFingerprints[styleSetId] != nil else {
160
+ throw exception("Highlight \(id) references unknown style set \(styleSetId).")
161
+ }
162
+ let zIndex = try integer(value, "zIndex", default: 0)
163
+ let isVisible = try boolean(value, "visible", default: true)
164
+ let geometry = try dictionary(value, "geometry")
165
+ let type = try string(geometry, "type")
166
+ let parts = try dictionaries(geometry, "parts")
167
+ guard !parts.isEmpty
168
+ else { throw exception("Highlight \(id) requires at least one polygon part.") }
169
+ switch type {
170
+ case "geographicPolygon":
171
+ let option = MapPolygonShapeOptions(shapeID: id, styleID: styleSetId, zOrder: zIndex)
172
+ option.polygons = try parts.map(mapShapeMapPolygon)
173
+ guard let shape = dimScreen.addHighlightMapPolygonShape(option) else {
174
+ throw exception("Could not add geographic dim-screen highlight \(id).")
175
+ }
176
+ isVisible ? shape.show() : shape.hide()
177
+ geographicHighlights[id] = shape
178
+ case "modelPolygon":
179
+ let option = PolygonShapeOptions(shapeID: id, styleID: styleSetId, zOrder: zIndex)
180
+ let base = try dictionary(geometry, "base")
181
+ option.basePosition = try mapPoint(base)
182
+ option.polygons = try parts.map(mapShapeModelPolygon)
183
+ guard let shape = dimScreen.addHighlightPolygonShape(option) else {
184
+ throw exception("Could not add model dim-screen highlight \(id).")
185
+ }
186
+ shape.orientation = try orientation(geometry) * .pi / 180
187
+ isVisible ? shape.show() : shape.hide()
188
+ modelHighlights[id] = shape
189
+ default:
190
+ throw exception("Dim-screen highlights require geographicPolygon or modelPolygon geometry.")
191
+ }
192
+ }
193
+
194
+ private func validateHighlight(_ value: [String: Any], availableStyleIds: Set<String>) throws {
195
+ let id = try string(value, "id")
196
+ let styleSetId = try string(value, "styleSetId")
197
+ guard availableStyleIds.contains(styleSetId) else {
198
+ throw exception("Highlight \(id) references unknown style set \(styleSetId).")
199
+ }
200
+ _ = try boolean(value, "visible", default: true)
201
+ _ = try integer(value, "zIndex", default: 0)
202
+ let geometry = try dictionary(value, "geometry")
203
+ let type = try string(geometry, "type")
204
+ let parts = try dictionaries(geometry, "parts")
205
+ guard !parts.isEmpty
206
+ else { throw exception("Highlight \(id) requires at least one polygon part.") }
207
+ switch type {
208
+ case "geographicPolygon": _ = try parts.map(mapShapeMapPolygon)
209
+ case "modelPolygon":
210
+ _ = try mapPoint(dictionary(geometry, "base"))
211
+ _ = try orientation(geometry)
212
+ _ = try parts.map(mapShapeModelPolygon)
213
+ default: throw exception(
214
+ "Dim-screen highlights require geographicPolygon or modelPolygon geometry."
215
+ )
216
+ }
217
+ }
218
+
219
+ private func addAnimation(_ value: [String: Any], to dimScreen: DimScreen) throws {
220
+ let id = try string(value, "animationId")
221
+ guard let animator = try dimScreen.addShapeAnimator(
222
+ animatorID: id,
223
+ effect: mapShapeWaveEffect(value)
224
+ ) else {
225
+ throw exception("Could not add dim-screen animation \(id).")
226
+ }
227
+ let shapeIds = try strings(value, "shapeIds")
228
+ guard !shapeIds.isEmpty
229
+ else { throw exception("Dim-screen animation \(id) requires a target.") }
230
+ for shapeId in shapeIds {
231
+ if let shape = geographicHighlights[shapeId] {
232
+ animator.addMapPolygonShape(shape)
233
+ } else if let shape = modelHighlights[shapeId] {
234
+ animator.addPolygonShape(shape)
235
+ } else {
236
+ throw exception("Dim-screen animation \(id) references unknown highlight \(shapeId).")
237
+ }
238
+ }
239
+ let animatorIdentity = ObjectIdentifier(animator)
240
+ animator.setStopCallback { [weak self] _ in
241
+ guard let self, !self.isDisposed,
242
+ let active = animations[id],
243
+ ObjectIdentifier(active) == animatorIdentity
244
+ else { return }
245
+ emit(["type": "animationStopped", "id": id])
246
+ }
247
+ animations[id] = animator
248
+ }
249
+
250
+ private func clearObjects() {
251
+ guard let dimScreen = map?.dimScreen else { return }
252
+ let removedAnimations = Array(animations.values)
253
+ animations.removeAll()
254
+ removedAnimations.forEach { $0.stop() }
255
+ dimScreen.clearAllShapeAnimators()
256
+ dimScreen.clearAllHighlightShapes()
257
+ geographicHighlights.removeAll()
258
+ modelHighlights.removeAll()
259
+ }
260
+
261
+ private func requireAnimation(_ id: String) throws -> ShapeAnimator {
262
+ guard !isDisposed, map != nil else {
263
+ throw KakaoMapException(mapDimNotFound, "The dim-screen manager was disposed.")
264
+ }
265
+ guard let animation = animations[id] else {
266
+ throw KakaoMapException(mapDimNotFound, "Dim-screen animation \(id) was not found.")
267
+ }
268
+ return animation
269
+ }
270
+
271
+ private func parse(_ value: [String: Any]) throws -> (
272
+ enabled: Bool,
273
+ color: String,
274
+ cover: String,
275
+ styleSets: [[String: Any]],
276
+ highlights: [[String: Any]],
277
+ animations: [[String: Any]]
278
+ ) {
279
+ let enabled = try boolean(value, "enabled")
280
+ if value["zIndex"] != nil {
281
+ throw KakaoMapException(
282
+ mapDimUnsupported,
283
+ "iOS SDK 2.12.19 does not expose dim-screen z-order."
284
+ )
285
+ }
286
+ let color = try string(value, "color")
287
+ _ = try self.color(color)
288
+ let cover = try string(value, "cover")
289
+ if cover == "mapAndRoute" {
290
+ throw KakaoMapException(
291
+ mapDimUnsupported,
292
+ "iOS SDK 2.12.19 does not support mapAndRoute dimming."
293
+ )
294
+ }
295
+ _ = try self.cover(cover)
296
+ let styleSets = try optionalDictionaries(value, "styleSets")
297
+ let highlights = try optionalDictionaries(value, "highlights")
298
+ let animations = try optionalDictionaries(value, "animations")
299
+ try unique(styleSets.map { try string($0, "id") }, name: "dim-screen style")
300
+ try unique(highlights.map { try string($0, "id") }, name: "dim-screen highlight")
301
+ try unique(animations.map { try string($0, "animationId") }, name: "dim-screen animation")
302
+ return (enabled, color, cover, styleSets, highlights, animations)
303
+ }
304
+
305
+ private func cover(_ value: String) throws -> DimScreenCover {
306
+ switch value {
307
+ case "map": .map
308
+ case "mapAndLabel": .mapAndLabels
309
+ case "all": .all
310
+ case "mapAndRoute": throw KakaoMapException(
311
+ mapDimUnsupported,
312
+ "iOS SDK 2.12.19 does not support mapAndRoute dimming."
313
+ )
314
+ default: throw exception("Unknown dim-screen cover: \(value).")
315
+ }
316
+ }
317
+
318
+ private func coverName(_ value: DimScreenCover) -> String {
319
+ switch value {
320
+ case .map: "map"
321
+ case .mapAndLabels: "mapAndLabel"
322
+ case .all: "all"
323
+ default: "map"
324
+ }
325
+ }
326
+
327
+ private func mapPoint(_ value: [String: Any]) throws -> MapPoint {
328
+ guard let latitude = value["latitude"] as? Double,
329
+ let longitude = value["longitude"] as? Double,
330
+ latitude.isFinite, longitude.isFinite,
331
+ (-90 ... 90).contains(latitude), (-180 ... 180).contains(longitude)
332
+ else { throw exception("Invalid dim-screen base coordinate.") }
333
+ return MapPoint(longitude: longitude, latitude: latitude)
334
+ }
335
+
336
+ private func orientation(_ geometry: [String: Any]) throws -> Double {
337
+ guard let raw = geometry["orientation"] else { return 0 }
338
+ guard let numeric = raw as? NSNumber,
339
+ CFGetTypeID(numeric) != CFBooleanGetTypeID()
340
+ else { throw exception("Model-polygon orientation must be numeric degrees.") }
341
+ let orientation = numeric.doubleValue
342
+ guard orientation.isFinite else {
343
+ throw exception("Model-polygon orientation must be finite degrees.")
344
+ }
345
+ return orientation
346
+ }
347
+
348
+ private func color(_ value: String) throws -> UIColor {
349
+ guard value.hasPrefix("#") else {
350
+ throw exception("Colors must use #RRGGBB or #RRGGBBAA.")
351
+ }
352
+ let hex = String(value.dropFirst())
353
+ guard [6, 8].contains(hex.count), let number = UInt64(hex, radix: 16) else {
354
+ throw exception("Colors must use #RRGGBB or #RRGGBBAA.")
355
+ }
356
+ let alpha = hex.count == 8 ? CGFloat(number & 0xFF) / 255 : 1
357
+ let rgb = hex.count == 8 ? number >> 8 : number
358
+ return UIColor(
359
+ red: CGFloat((rgb >> 16) & 0xFF) / 255,
360
+ green: CGFloat((rgb >> 8) & 0xFF) / 255,
361
+ blue: CGFloat(rgb & 0xFF) / 255,
362
+ alpha: alpha
363
+ )
364
+ }
365
+
366
+ private func colorString(_ value: UIColor) -> String {
367
+ var red: CGFloat = 0
368
+ var green: CGFloat = 0
369
+ var blue: CGFloat = 0
370
+ var alpha: CGFloat = 0
371
+ guard value.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { return "#00000000" }
372
+ return String(
373
+ format: "#%02X%02X%02X%02X",
374
+ Int(red * 255), Int(green * 255), Int(blue * 255), Int(alpha * 255)
375
+ )
376
+ }
377
+
378
+ private func fingerprint(_ value: [String: Any]) throws -> String {
379
+ guard JSONSerialization.isValidJSONObject(value) else {
380
+ throw exception("Dim-screen styles must contain serializable finite values.")
381
+ }
382
+ do {
383
+ let data = try JSONSerialization.data(withJSONObject: value, options: [.sortedKeys])
384
+ return data.base64EncodedString()
385
+ } catch {
386
+ throw exception("Dim-screen styles could not be serialized: \(error.localizedDescription)")
387
+ }
388
+ }
389
+
390
+ private func string(_ value: [String: Any], _ key: String) throws -> String {
391
+ guard let result = value[key] as? String, !result.isEmpty else {
392
+ throw exception("\(key) must be a nonempty string.")
393
+ }
394
+ return result
395
+ }
396
+
397
+ private func strings(_ value: [String: Any], _ key: String) throws -> [String] {
398
+ guard let result = value[key] as? [String]
399
+ else { throw exception("\(key) must be a string array.") }
400
+ return result
401
+ }
402
+
403
+ private func integer(_ value: [String: Any], _ key: String, default fallback: Int) throws -> Int {
404
+ guard let number = value[key] else { return fallback }
405
+ guard let numeric = number as? NSNumber,
406
+ CFGetTypeID(numeric) != CFBooleanGetTypeID()
407
+ else { throw exception("\(key) must be an integer.") }
408
+ let double = numeric.doubleValue
409
+ guard double.isFinite, double.rounded() == double,
410
+ double >= Double(Int32.min), double <= Double(Int32.max)
411
+ else { throw exception("\(key) must be a 32-bit integer.") }
412
+ return Int(double)
413
+ }
414
+
415
+ private func dictionary(_ value: [String: Any], _ key: String) throws -> [String: Any] {
416
+ guard let result = value[key] as? [String: Any]
417
+ else { throw exception("\(key) must be an object.") }
418
+ return result
419
+ }
420
+
421
+ private func dictionaries(_ value: [String: Any], _ key: String) throws -> [[String: Any]] {
422
+ guard let result = value[key] as? [[String: Any]]
423
+ else { throw exception("\(key) must be an object array.") }
424
+ return result
425
+ }
426
+
427
+ private func optionalDictionaries(
428
+ _ value: [String: Any],
429
+ _ key: String
430
+ ) throws -> [[String: Any]] {
431
+ guard let raw = value[key] else { return [] }
432
+ guard let items = raw as? [Any] else { throw exception("\(key) must be an object array.") }
433
+ return try items.map { item in
434
+ guard let object = item as? [String: Any] else {
435
+ throw exception("\(key) must contain only objects.")
436
+ }
437
+ return object
438
+ }
439
+ }
440
+
441
+ private func boolean(
442
+ _ value: [String: Any],
443
+ _ key: String,
444
+ default fallback: Bool? = nil
445
+ ) throws -> Bool {
446
+ guard let raw = value[key] else {
447
+ guard let fallback else { throw exception("\(key) must be a boolean.") }
448
+ return fallback
449
+ }
450
+ guard let numeric = raw as? NSNumber,
451
+ CFGetTypeID(numeric) == CFBooleanGetTypeID()
452
+ else { throw exception("\(key) must be a boolean.") }
453
+ return numeric.boolValue
454
+ }
455
+
456
+ private func unique(_ values: [String], name: String) throws {
457
+ guard Set(values).count == values.count else { throw exception("Duplicate \(name) ID.") }
458
+ }
459
+
460
+ private func exception(_ message: String) -> KakaoMapException {
461
+ KakaoMapException(mapDimInvalidArgument, message)
462
+ }
463
+ }