@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.
- package/CHANGELOG.md +14 -0
- package/README.md +5 -38
- package/RNCKakaoMap.podspec +16 -52
- package/android/build.gradle +12 -107
- package/android/consumer-rules.pro +2 -0
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/net/mjstudio/rnkakao/map/CameraOptions.kt +61 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapModule.kt +131 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapView.kt +1257 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapCameraCommands.kt +306 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapDimScreen.kt +431 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapGui.kt +899 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapImageLoader.kt +232 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapLabels.kt +1572 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapOptions.kt +360 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapRoutes.kt +805 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapShapes.kt +1096 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapUtilities.kt +495 -0
- package/expo-module.config.json +5 -0
- package/ios/CameraOptions.swift +41 -0
- package/ios/KakaoMapModule.swift +204 -0
- package/ios/KakaoMapView.swift +1018 -0
- package/ios/MapCameraCommands.swift +451 -0
- package/ios/MapDimScreen.swift +463 -0
- package/ios/MapGui.swift +1099 -0
- package/ios/MapImageLoader.swift +177 -0
- package/ios/MapLabels.swift +1676 -0
- package/ios/MapOptions.swift +374 -0
- package/ios/MapRoutes.swift +903 -0
- package/ios/MapShapes.swift +1294 -0
- package/ios/MapUnits.swift +20 -0
- package/ios/MapUtilities.swift +365 -0
- package/lib/commonjs/index.js +180 -29
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +18 -0
- package/lib/commonjs/index.web.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types-assets.js +2 -0
- package/lib/commonjs/types-assets.js.map +1 -0
- package/lib/commonjs/types-camera.js +6 -0
- package/lib/commonjs/types-camera.js.map +1 -0
- package/lib/commonjs/types-dim.js +6 -0
- package/lib/commonjs/types-dim.js.map +1 -0
- package/lib/commonjs/types-gui.js +6 -0
- package/lib/commonjs/types-gui.js.map +1 -0
- package/lib/commonjs/types-labels.js +6 -0
- package/lib/commonjs/types-labels.js.map +1 -0
- package/lib/commonjs/types-routes.js +6 -0
- package/lib/commonjs/types-routes.js.map +1 -0
- package/lib/commonjs/types-shapes.js +6 -0
- package/lib/commonjs/types-shapes.js.map +1 -0
- package/lib/commonjs/types-utilities.js +6 -0
- package/lib/commonjs/types-utilities.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/index.js +180 -22
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +13 -0
- package/lib/module/index.web.js.map +1 -0
- package/lib/module/types-assets.js +2 -0
- package/lib/module/types-assets.js.map +1 -0
- package/lib/module/types-camera.js +4 -0
- package/lib/module/types-camera.js.map +1 -0
- package/lib/module/types-dim.js +4 -0
- package/lib/module/types-dim.js.map +1 -0
- package/lib/module/types-gui.js +4 -0
- package/lib/module/types-gui.js.map +1 -0
- package/lib/module/types-labels.js +4 -0
- package/lib/module/types-labels.js.map +1 -0
- package/lib/module/types-routes.js +4 -0
- package/lib/module/types-routes.js.map +1 -0
- package/lib/module/types-shapes.js +4 -0
- package/lib/module/types-shapes.js.map +1 -0
- package/lib/module/types-utilities.js +4 -0
- package/lib/module/types-utilities.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/src/index.d.ts +59 -9
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types-assets.d.ts +9 -0
- package/lib/typescript/src/types-assets.d.ts.map +1 -0
- package/lib/typescript/src/types-camera.d.ts +123 -0
- package/lib/typescript/src/types-camera.d.ts.map +1 -0
- package/lib/typescript/src/types-dim.d.ts +54 -0
- package/lib/typescript/src/types-dim.d.ts.map +1 -0
- package/lib/typescript/src/types-gui.d.ts +189 -0
- package/lib/typescript/src/types-gui.d.ts.map +1 -0
- package/lib/typescript/src/types-labels.d.ts +246 -0
- package/lib/typescript/src/types-labels.d.ts.map +1 -0
- package/lib/typescript/src/types-routes.d.ts +162 -0
- package/lib/typescript/src/types-routes.d.ts.map +1 -0
- package/lib/typescript/src/types-shapes.d.ts +253 -0
- package/lib/typescript/src/types-shapes.d.ts.map +1 -0
- package/lib/typescript/src/types-utilities.d.ts +130 -0
- package/lib/typescript/src/types-utilities.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +222 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +24 -77
- package/src/index.tsx +322 -0
- package/src/index.web.tsx +46 -0
- package/src/types-assets.ts +8 -0
- package/src/types-camera.ts +97 -0
- package/src/types-dim.ts +54 -0
- package/src/types-gui.ts +171 -0
- package/src/types-labels.ts +207 -0
- package/src/types-routes.ts +118 -0
- package/src/types-shapes.ts +244 -0
- package/src/types-utilities.ts +115 -0
- package/src/types.ts +223 -0
- package/android/gradle.properties +0 -1
- package/android/src/main/AndroidManifestNew.xml +0 -2
- package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapModule.kt +0 -25
- package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapPackage.kt +0 -35
- package/android/src/newarch/KakaoMapSpec.kt +0 -7
- package/android/src/oldarch/KakaoMapSpec.kt +0 -14
- package/ios/Component/Map/RNCKakaoMapView.h +0 -23
- package/ios/Component/Map/RNCKakaoMapView.mm +0 -209
- package/ios/Component/Map/RNCKakaoMapViewManager.mm +0 -19
- package/ios/NativeModule/RNCKakaoMap.h +0 -7
- package/ios/NativeModule/RNCKakaoMap.mm +0 -19
- package/ios/NativeModule/RNCKakaoMapManager.swift +0 -7
- package/ios/Util/Util.h +0 -97
- package/lib/commonjs/component/KakaoMapView.js +0 -54
- package/lib/commonjs/component/KakaoMapView.js.map +0 -1
- package/lib/commonjs/spec/NativeKakaoMap.js +0 -9
- package/lib/commonjs/spec/NativeKakaoMap.js.map +0 -1
- package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js +0 -15
- package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
- package/lib/commonjs/type/type.js +0 -2
- package/lib/commonjs/type/type.js.map +0 -1
- package/lib/commonjs/util/Const.js +0 -14
- package/lib/commonjs/util/Const.js.map +0 -1
- package/lib/commonjs/util/Util.js +0 -15
- package/lib/commonjs/util/Util.js.map +0 -1
- package/lib/module/component/KakaoMapView.js +0 -48
- package/lib/module/component/KakaoMapView.js.map +0 -1
- package/lib/module/spec/NativeKakaoMap.js +0 -3
- package/lib/module/spec/NativeKakaoMap.js.map +0 -1
- package/lib/module/spec/RNCKakaoMapViewNativeComponent.js +0 -10
- package/lib/module/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
- package/lib/module/type/type.js +0 -2
- package/lib/module/type/type.js.map +0 -1
- package/lib/module/util/Const.js +0 -8
- package/lib/module/util/Const.js.map +0 -1
- package/lib/module/util/Util.js +0 -9
- package/lib/module/util/Util.js.map +0 -1
- package/lib/typescript/src/component/KakaoMapView.d.ts +0 -25
- package/lib/typescript/src/component/KakaoMapView.d.ts.map +0 -1
- package/lib/typescript/src/spec/NativeKakaoMap.d.ts +0 -7
- package/lib/typescript/src/spec/NativeKakaoMap.d.ts.map +0 -1
- package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts +0 -32
- package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts.map +0 -1
- package/lib/typescript/src/type/type.d.ts +0 -8
- package/lib/typescript/src/type/type.d.ts.map +0 -1
- package/lib/typescript/src/util/Const.d.ts +0 -8
- package/lib/typescript/src/util/Const.d.ts.map +0 -1
- package/lib/typescript/src/util/Util.d.ts +0 -3
- package/lib/typescript/src/util/Util.d.ts.map +0 -1
- package/src/component/KakaoMapView.tsx +0 -82
- package/src/index.ts +0 -43
- package/src/spec/NativeKakaoMap.ts +0 -8
- package/src/spec/RNCKakaoMapViewNativeComponent.ts +0 -38
- package/src/type/type.ts +0 -7
- package/src/util/Const.ts +0 -7
- package/src/util/Util.ts +0 -11
|
@@ -0,0 +1,1676 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import KakaoMapsSDK
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
let labelInvalidArgument = "ERR_KAKAO_MAP_LABEL_INVALID_ARGUMENT"
|
|
6
|
+
let labelNotFound = "ERR_KAKAO_MAP_LABEL_NOT_FOUND"
|
|
7
|
+
let labelUnsupported = "ERR_KAKAO_MAP_LABEL_UNSUPPORTED"
|
|
8
|
+
let labelApplyCancelled = "ERR_KAKAO_MAP_LABEL_APPLY_CANCELLED"
|
|
9
|
+
private let defaultLabelLayerZOrder = 1000
|
|
10
|
+
|
|
11
|
+
struct MapLabelPoint: Record {
|
|
12
|
+
@Field var latitude: Double = .nan
|
|
13
|
+
@Field var longitude: Double = .nan
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
struct MapLabelVector: Record {
|
|
17
|
+
@Field var x: Double = .nan
|
|
18
|
+
@Field var y: Double = .nan
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
struct MapLabelImage: Record {
|
|
22
|
+
@Field var uri = ""
|
|
23
|
+
@Field var width: Double?
|
|
24
|
+
@Field var height: Double?
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
struct MapLabelTextStyleRecord: Record {
|
|
28
|
+
@Field var fontFamily: String?
|
|
29
|
+
@Field var fontSize: Double = .nan
|
|
30
|
+
@Field var color = ""
|
|
31
|
+
@Field var strokeColor: String?
|
|
32
|
+
@Field var strokeWidth = 0.0
|
|
33
|
+
@Field var characterSpacing = 0.0
|
|
34
|
+
@Field var lineSpacing = 1.0
|
|
35
|
+
@Field var aspectRatio = 1.0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
struct MapLabelTransitionRecord: Record {
|
|
39
|
+
@Field var entrance = "none"
|
|
40
|
+
@Field var exit = "none"
|
|
41
|
+
@Field var enableEntrance = true
|
|
42
|
+
@Field var enableExit = true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
struct MapLabelBadgeStyleRecord: Record {
|
|
46
|
+
@Field var id = ""
|
|
47
|
+
@Field var image = MapLabelImage()
|
|
48
|
+
@Field var offset: MapLabelVector?
|
|
49
|
+
@Field var zOrder = 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
struct MapLabelPerLevelStyleRecord: Record {
|
|
53
|
+
@Field var level = 0
|
|
54
|
+
@Field var image: MapLabelImage?
|
|
55
|
+
@Field var anchor: MapLabelVector?
|
|
56
|
+
@Field var padding = 0.0
|
|
57
|
+
@Field var iconTransition: MapLabelTransitionRecord?
|
|
58
|
+
@Field var textTransition: MapLabelTransitionRecord?
|
|
59
|
+
@Field var textLayout = "center"
|
|
60
|
+
@Field var textLines: [MapLabelTextStyleRecord] = []
|
|
61
|
+
@Field var badges: [MapLabelBadgeStyleRecord] = []
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
struct MapLabelStyleRecord: Record {
|
|
65
|
+
@Field var id = ""
|
|
66
|
+
@Field var levels: [MapLabelPerLevelStyleRecord] = []
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
struct MapPathTextLevelStyleRecord: Record {
|
|
70
|
+
@Field var level = 0
|
|
71
|
+
@Field var text = MapLabelTextStyleRecord()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
struct MapPathTextStyleRecord: Record {
|
|
75
|
+
@Field var id = ""
|
|
76
|
+
@Field var levels: [MapPathTextLevelStyleRecord] = []
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
struct MapLabelLayerRecord: Record {
|
|
80
|
+
@Field var id = ""
|
|
81
|
+
@Field var visible = true
|
|
82
|
+
@Field var clickable = true
|
|
83
|
+
@Field var zOrder = defaultLabelLayerZOrder
|
|
84
|
+
@Field var competition = "none"
|
|
85
|
+
@Field var competitionUnit = "iconAndText"
|
|
86
|
+
@Field var ordering = "rank"
|
|
87
|
+
@Field var competitionExceptions: [String] = []
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
struct MapLodLabelLayerRecord: Record {
|
|
91
|
+
@Field var id = ""
|
|
92
|
+
@Field var visible = true
|
|
93
|
+
@Field var clickable = true
|
|
94
|
+
@Field var zOrder = defaultLabelLayerZOrder
|
|
95
|
+
@Field var competition = "none"
|
|
96
|
+
@Field var competitionUnit = "iconAndText"
|
|
97
|
+
@Field var ordering = "rank"
|
|
98
|
+
@Field var competitionExceptions: [String] = []
|
|
99
|
+
@Field var radius = 20.0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
struct MapLabelTextRecord: Record {
|
|
103
|
+
@Field var value = ""
|
|
104
|
+
@Field var styleIndex = 0
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
struct MapLabelBadgeRecord: Record {
|
|
108
|
+
@Field var id = ""
|
|
109
|
+
@Field var image = MapLabelImage()
|
|
110
|
+
@Field var offset: MapLabelVector?
|
|
111
|
+
@Field var zOrder = 0
|
|
112
|
+
@Field var visible = true
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
struct MapLabelRecord: Record {
|
|
116
|
+
@Field var id = ""
|
|
117
|
+
@Field var layerId = ""
|
|
118
|
+
@Field var styleId = ""
|
|
119
|
+
@Field var coordinate = MapLabelPoint()
|
|
120
|
+
@Field var text: [MapLabelTextRecord] = []
|
|
121
|
+
@Field var rank = 0.0
|
|
122
|
+
@Field var clickable = false
|
|
123
|
+
@Field var visible = true
|
|
124
|
+
@Field var transform = "default"
|
|
125
|
+
@Field var pixelOffset: MapLabelVector?
|
|
126
|
+
@Field var orientation = 0.0
|
|
127
|
+
@Field var scale: MapLabelVector?
|
|
128
|
+
@Field var badges: [MapLabelBadgeRecord] = []
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
struct MapLodLabelRecord: Record {
|
|
132
|
+
@Field var id = ""
|
|
133
|
+
@Field var layerId = ""
|
|
134
|
+
@Field var styleId = ""
|
|
135
|
+
@Field var coordinate = MapLabelPoint()
|
|
136
|
+
@Field var text: [MapLabelTextRecord] = []
|
|
137
|
+
@Field var rank = 0.0
|
|
138
|
+
@Field var clickable = false
|
|
139
|
+
@Field var visible = true
|
|
140
|
+
@Field var transform = "default"
|
|
141
|
+
@Field var badges: [MapLabelBadgeRecord] = []
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
struct MapPathLabelRecord: Record {
|
|
145
|
+
@Field var id = ""
|
|
146
|
+
@Field var layerId = ""
|
|
147
|
+
@Field var styleId = ""
|
|
148
|
+
@Field var points: [MapLabelPoint] = []
|
|
149
|
+
@Field var text = ""
|
|
150
|
+
@Field var rank = 0.0
|
|
151
|
+
@Field var visible = true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
struct MapLabelAnimationKeyframeRecord: Record {
|
|
155
|
+
@Field var duration = 0.0
|
|
156
|
+
@Field var interpolation = "linear"
|
|
157
|
+
@Field var alpha: Double?
|
|
158
|
+
@Field var scale: MapLabelVector?
|
|
159
|
+
@Field var translation: MapLabelVector?
|
|
160
|
+
@Field var rotation: Double?
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
struct MapLabelAnimationRecord: Record {
|
|
164
|
+
@Field var id = ""
|
|
165
|
+
@Field var labelIds: [String] = []
|
|
166
|
+
@Field var effect = ""
|
|
167
|
+
@Field var keyframes: [MapLabelAnimationKeyframeRecord] = []
|
|
168
|
+
@Field var repeatCount = 1
|
|
169
|
+
@Field var hideAtStop = false
|
|
170
|
+
@Field var removeAtStop = false
|
|
171
|
+
@Field var resetToInitialState = false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
struct MapLabelSceneRecord: Record {
|
|
175
|
+
@Field var styles: [MapLabelStyleRecord] = []
|
|
176
|
+
@Field var pathTextStyles: [MapPathTextStyleRecord] = []
|
|
177
|
+
@Field var layers: [MapLabelLayerRecord] = []
|
|
178
|
+
@Field var lodLayers: [MapLodLabelLayerRecord] = []
|
|
179
|
+
@Field var labels: [MapLabelRecord] = []
|
|
180
|
+
@Field var lodLabels: [MapLodLabelRecord] = []
|
|
181
|
+
@Field var pathLabels: [MapPathLabelRecord] = []
|
|
182
|
+
@Field var animations: [MapLabelAnimationRecord] = []
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
struct MapLabelCommandRecord: Record {
|
|
186
|
+
@Field var type = ""
|
|
187
|
+
@Field var id: String?
|
|
188
|
+
@Field var sourceId: String?
|
|
189
|
+
@Field var targetId: String?
|
|
190
|
+
@Field var badgeId: String?
|
|
191
|
+
@Field var style = false
|
|
192
|
+
@Field var visible: Bool?
|
|
193
|
+
@Field var enabled: Bool?
|
|
194
|
+
@Field var coordinate: MapLabelPoint?
|
|
195
|
+
@Field var points: [MapLabelPoint] = []
|
|
196
|
+
@Field var duration: Double?
|
|
197
|
+
@Field var cornerRadius = 0.0
|
|
198
|
+
@Field var jumpThreshold = 0.0
|
|
199
|
+
@Field var rotate = false
|
|
200
|
+
@Field var autoMove = false
|
|
201
|
+
@Field var baseRotation = 0.0
|
|
202
|
+
@Field var rotation: Double?
|
|
203
|
+
@Field var scale: MapLabelVector?
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
final class MapLabels {
|
|
207
|
+
private struct ActiveAnimationRun {
|
|
208
|
+
let nativeID: String
|
|
209
|
+
let epoch: Int
|
|
210
|
+
let definition: MapLabelAnimationRecord
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private let map: KakaoMap
|
|
214
|
+
private let nativeNamespace = UUID().uuidString
|
|
215
|
+
.replacingOccurrences(of: "-", with: "").lowercased()
|
|
216
|
+
private let manager: LabelManager
|
|
217
|
+
private let trackingManager: TrackingManager
|
|
218
|
+
private let imageLoader: MapImageLoader
|
|
219
|
+
private let emitClick: ([String: Any]) -> Void
|
|
220
|
+
private let emitAnimationStopped: ([String: Any]) -> Void
|
|
221
|
+
private let resolveShape: (String) -> (any KakaoMapsSDK.Shape)?
|
|
222
|
+
private var labels: [String: Poi] = [:]
|
|
223
|
+
private var labelsWithReleasedSharing: Set<String> = []
|
|
224
|
+
private var lodLabels: [String: LodPoi] = [:]
|
|
225
|
+
private var coordinates: [String: MapLabelPoint] = [:]
|
|
226
|
+
private var pathLabels: [String: WaveText] = [:]
|
|
227
|
+
private var labelLayers: [String: KakaoMapsSDK.LabelLayer] = [:]
|
|
228
|
+
private var lodLabelLayers: [String: KakaoMapsSDK.LodLabelLayer] = [:]
|
|
229
|
+
private var publicLayerIDsByNative: [String: String] = [:]
|
|
230
|
+
private var eventHandlers: [DisposableEventHandler] = []
|
|
231
|
+
private var styleIds: Set<String> = []
|
|
232
|
+
private var waveTextStyleFingerprints: [String: String] = [:]
|
|
233
|
+
private var nativeWaveTextStyleIDs: [String: String] = [:]
|
|
234
|
+
private var nextNativeObjectID = 0
|
|
235
|
+
private var animationDefinitions: [String: MapLabelAnimationRecord] = [:]
|
|
236
|
+
private var activeAnimationRuns: [String: ActiveAnimationRun] = [:]
|
|
237
|
+
private var nextAnimationEpoch = 0
|
|
238
|
+
private var imageRequests: [MapImageLoadToken] = []
|
|
239
|
+
private var pendingApplyCompletion: ((Result<Void, Error>) -> Void)?
|
|
240
|
+
private var generation = 0
|
|
241
|
+
private var isDisposed = false
|
|
242
|
+
|
|
243
|
+
init(
|
|
244
|
+
map: KakaoMap,
|
|
245
|
+
imageLoader: MapImageLoader,
|
|
246
|
+
emitClick: @escaping ([String: Any]) -> Void,
|
|
247
|
+
emitAnimationStopped: @escaping ([String: Any]) -> Void,
|
|
248
|
+
resolveShape: @escaping (String) -> (any KakaoMapsSDK.Shape)?
|
|
249
|
+
) {
|
|
250
|
+
self.map = map
|
|
251
|
+
manager = map.getLabelManager()
|
|
252
|
+
trackingManager = map.getTrackingManager()
|
|
253
|
+
self.imageLoader = imageLoader
|
|
254
|
+
self.emitClick = emitClick
|
|
255
|
+
self.emitAnimationStopped = emitAnimationStopped
|
|
256
|
+
self.resolveShape = resolveShape
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func apply(
|
|
260
|
+
_ scene: MapLabelSceneRecord,
|
|
261
|
+
completion: @escaping (Result<Void, Error>) -> Void
|
|
262
|
+
) throws {
|
|
263
|
+
try requireActive()
|
|
264
|
+
try validate(scene)
|
|
265
|
+
cancelPendingApply("A newer label scene replaced this apply operation.")
|
|
266
|
+
pendingApplyCompletion = completion
|
|
267
|
+
generation += 1
|
|
268
|
+
let currentGeneration = generation
|
|
269
|
+
cancelImageRequests()
|
|
270
|
+
loadImages(scene.imageUris, generation: currentGeneration) { [weak self] result in
|
|
271
|
+
guard let self, !isDisposed, currentGeneration == generation else { return }
|
|
272
|
+
do {
|
|
273
|
+
let images = try result.get()
|
|
274
|
+
try rebuild(scene, images: images)
|
|
275
|
+
finishApply(.success(()))
|
|
276
|
+
} catch {
|
|
277
|
+
finishApply(.failure(error))
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
func command(_ command: MapLabelCommandRecord) throws {
|
|
283
|
+
try requireActive()
|
|
284
|
+
let duration = try (command.duration ?? 0).duration()
|
|
285
|
+
switch command.type {
|
|
286
|
+
case "show":
|
|
287
|
+
let visible = try command.visible.required("visible")
|
|
288
|
+
guard duration == 0 else {
|
|
289
|
+
throw labelError(labelUnsupported, "iOS label auto-move does not expose a duration.")
|
|
290
|
+
}
|
|
291
|
+
try show(command.requiredID(), visible: visible, autoMove: command.autoMove)
|
|
292
|
+
case "move":
|
|
293
|
+
let id = try command.requiredID()
|
|
294
|
+
let poi = try label(id)
|
|
295
|
+
let point = try command.coordinate.required("coordinate").mapPoint
|
|
296
|
+
if duration == 0 {
|
|
297
|
+
poi.position = point
|
|
298
|
+
} else if labelsWithReleasedSharing.contains(id) {
|
|
299
|
+
// Kakao Maps 2.12.19 ignores moveAt on former sharing followers.
|
|
300
|
+
poi.moveOnPath(
|
|
301
|
+
[poi.position, point],
|
|
302
|
+
duration: UInt(duration),
|
|
303
|
+
cornerRadius: 0,
|
|
304
|
+
jumpThreshold: 0
|
|
305
|
+
)
|
|
306
|
+
} else {
|
|
307
|
+
poi.moveAt(point, duration: UInt(duration))
|
|
308
|
+
}
|
|
309
|
+
case "movePath":
|
|
310
|
+
_ = try command.duration.required("duration")
|
|
311
|
+
guard command.points.count >= 2 else {
|
|
312
|
+
throw labelError(labelInvalidArgument, "movePath requires at least two points.")
|
|
313
|
+
}
|
|
314
|
+
let points = try command.points.map { try $0.mapPoint }
|
|
315
|
+
guard points.count >= 2 else { throw labelError(
|
|
316
|
+
labelInvalidArgument,
|
|
317
|
+
"A path needs at least two points."
|
|
318
|
+
) }
|
|
319
|
+
guard command.cornerRadius >= 0, command.jumpThreshold >= 0 else {
|
|
320
|
+
throw labelError(
|
|
321
|
+
labelInvalidArgument,
|
|
322
|
+
"cornerRadius and jumpThreshold must be non-negative."
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
let poi = try label(command.requiredID())
|
|
326
|
+
if command.rotate {
|
|
327
|
+
try poi.moveAndRotateOnPath(
|
|
328
|
+
points,
|
|
329
|
+
baseRadian: command.baseRotation.finiteFloat("baseRotation") * .pi / 180,
|
|
330
|
+
duration: UInt(duration),
|
|
331
|
+
cornerRadius: command.cornerRadius.finiteFloat("cornerRadius"),
|
|
332
|
+
jumpThreshold: command.jumpThreshold.finiteFloat("jumpThreshold")
|
|
333
|
+
)
|
|
334
|
+
} else {
|
|
335
|
+
try poi.moveOnPath(
|
|
336
|
+
points,
|
|
337
|
+
duration: UInt(duration),
|
|
338
|
+
cornerRadius: command.cornerRadius.finiteFloat("cornerRadius"),
|
|
339
|
+
jumpThreshold: command.jumpThreshold.finiteFloat("jumpThreshold")
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
case "rotate":
|
|
343
|
+
let poi = try label(command.requiredID())
|
|
344
|
+
let rotation = try Double(command.rotation.required("rotation").finiteFloat("rotation")) *
|
|
345
|
+
.pi / 180
|
|
346
|
+
if duration == 0 {
|
|
347
|
+
poi.orientation = rotation
|
|
348
|
+
} else {
|
|
349
|
+
poi.rotateAt(rotation, duration: UInt(duration))
|
|
350
|
+
}
|
|
351
|
+
case "scale":
|
|
352
|
+
let scale = try command.scale.required("scale")
|
|
353
|
+
try scale.validateScale()
|
|
354
|
+
throw labelError(
|
|
355
|
+
labelUnsupported,
|
|
356
|
+
"The iOS Kakao Maps SDK has no direct POI scale command; use a scale animation."
|
|
357
|
+
)
|
|
358
|
+
case "sharePosition":
|
|
359
|
+
try share(command, transform: false)
|
|
360
|
+
case "shareTransform":
|
|
361
|
+
try share(command, transform: true)
|
|
362
|
+
case "shareShapeTransform":
|
|
363
|
+
let isEnabled = try command.enabled.required("enabled")
|
|
364
|
+
let source = try label(command.sourceId.required("sourceId"))
|
|
365
|
+
let targetID = try command.targetId.required("targetId")
|
|
366
|
+
guard let target = resolveShape(targetID) else { throw notFound("shape", targetID) }
|
|
367
|
+
isEnabled ? source.shareTransformWithShape(target) : source
|
|
368
|
+
.removeShareTransformWithShape(target)
|
|
369
|
+
case "setBadgeVisibility":
|
|
370
|
+
try setBadgeVisibility(command)
|
|
371
|
+
case "startAnimation":
|
|
372
|
+
let id = try command.requiredID()
|
|
373
|
+
try startAnimation(id)
|
|
374
|
+
case "stopAnimation":
|
|
375
|
+
let id = try command.requiredID()
|
|
376
|
+
guard animationDefinitions[id] != nil else {
|
|
377
|
+
throw labelError(labelNotFound, "Animation not found: \(id)")
|
|
378
|
+
}
|
|
379
|
+
if let run = activeAnimationRuns[id] {
|
|
380
|
+
finishAnimationRun(id: id, run: run, finished: false, stopNative: true)
|
|
381
|
+
}
|
|
382
|
+
case "startTracking":
|
|
383
|
+
try trackingManager.startTrackingPoi(label(command.requiredID()))
|
|
384
|
+
trackingManager.isTrackingRoll = command.rotate
|
|
385
|
+
case "stopTracking":
|
|
386
|
+
trackingManager.stopTracking()
|
|
387
|
+
default:
|
|
388
|
+
throw labelError(labelInvalidArgument, "Unknown label command: \(command.type)")
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
func capabilities() -> [String: Any] {
|
|
393
|
+
[
|
|
394
|
+
"pathText": "waveText",
|
|
395
|
+
"pathTextRank": true,
|
|
396
|
+
"pathTextStyleAddition": true,
|
|
397
|
+
"pathTextStyleDefinitionUpdate": false,
|
|
398
|
+
"pathTextStyleRegistrationRemoval": false,
|
|
399
|
+
"lodPositionMutation": false,
|
|
400
|
+
"lodAutoMove": true,
|
|
401
|
+
"autoMoveDuration": false,
|
|
402
|
+
"labelScaleCommand": false,
|
|
403
|
+
"sharedShapeTransform": true,
|
|
404
|
+
"sharedShapeTarget": "shape",
|
|
405
|
+
"competitionExceptions": false,
|
|
406
|
+
"supportedTransforms": [
|
|
407
|
+
"absoluteRotation", "default", "absoluteRotationKeepUpright",
|
|
408
|
+
"absoluteRotationDecal", "decal"
|
|
409
|
+
],
|
|
410
|
+
"keyframeRepeatCount": false,
|
|
411
|
+
"trackingStateSource": "native",
|
|
412
|
+
"remoteImages": true
|
|
413
|
+
]
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
func state() -> [String: Any] {
|
|
417
|
+
guard !isDisposed else {
|
|
418
|
+
return [
|
|
419
|
+
"labels": [],
|
|
420
|
+
"animationIds": [],
|
|
421
|
+
"tracking": ["active": false, "source": "native"]
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
var entries: [[String: Any]] = []
|
|
425
|
+
let liveLabelIDs = Set(labels.compactMap { id, cached -> String? in
|
|
426
|
+
guard let native = labelLayers[cached.layerID]?.getPoi(poiID: id) else { return nil }
|
|
427
|
+
let position = native.position.wgsCoord
|
|
428
|
+
entries.append([
|
|
429
|
+
"id": id,
|
|
430
|
+
"kind": "label",
|
|
431
|
+
"visible": native.isShow,
|
|
432
|
+
"coordinate": [
|
|
433
|
+
"latitude": position.latitude,
|
|
434
|
+
"longitude": position.longitude
|
|
435
|
+
]
|
|
436
|
+
])
|
|
437
|
+
return id
|
|
438
|
+
})
|
|
439
|
+
let liveLodLabelIDs = Set(lodLabels.compactMap { id, cached -> String? in
|
|
440
|
+
guard let native = lodLabelLayers[cached.layerID]?.getLodPoi(poiID: id) else { return nil }
|
|
441
|
+
entries.append(["id": id, "kind": "lodLabel", "visible": native.isShow])
|
|
442
|
+
return id
|
|
443
|
+
})
|
|
444
|
+
let livePathLabelIDs = Set(pathLabels.compactMap { id, cached -> String? in
|
|
445
|
+
guard let native = labelLayers[cached.layerID]?.getWaveText(waveTextID: id)
|
|
446
|
+
else { return nil }
|
|
447
|
+
entries.append(["id": id, "kind": "pathLabel", "visible": native.isShow])
|
|
448
|
+
return id
|
|
449
|
+
})
|
|
450
|
+
labels = labels.filter { liveLabelIDs.contains($0.key) }
|
|
451
|
+
lodLabels = lodLabels.filter { liveLodLabelIDs.contains($0.key) }
|
|
452
|
+
pathLabels = pathLabels.filter { livePathLabelIDs.contains($0.key) }
|
|
453
|
+
coordinates = coordinates
|
|
454
|
+
.filter { liveLabelIDs.contains($0.key) || liveLodLabelIDs.contains($0.key) }
|
|
455
|
+
return [
|
|
456
|
+
"labels": entries.sorted { ($0["id"] as? String ?? "") < ($1["id"] as? String ?? "") },
|
|
457
|
+
"animationIds": animationDefinitions.keys.sorted(),
|
|
458
|
+
"tracking": ["active": trackingManager.isTracking, "source": "native"]
|
|
459
|
+
]
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
func handlesPoiTap(layerId: String, id: String) -> Bool {
|
|
463
|
+
guard !isDisposed, let cached = labels[id], cached.layerID == layerId else { return false }
|
|
464
|
+
return labelLayers[layerId]?.getPoi(poiID: id) != nil
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
func dispose() {
|
|
468
|
+
guard !isDisposed else { return }
|
|
469
|
+
isDisposed = true
|
|
470
|
+
generation += 1
|
|
471
|
+
clearAnimationRuns()
|
|
472
|
+
cancelImageRequests()
|
|
473
|
+
cancelPendingApply("The map view was destroyed.")
|
|
474
|
+
trackingManager.stopTracking()
|
|
475
|
+
clearHandlers()
|
|
476
|
+
clearLayersAndStyles()
|
|
477
|
+
labels.removeAll()
|
|
478
|
+
lodLabels.removeAll()
|
|
479
|
+
coordinates.removeAll()
|
|
480
|
+
pathLabels.removeAll()
|
|
481
|
+
labelLayers.removeAll()
|
|
482
|
+
lodLabelLayers.removeAll()
|
|
483
|
+
publicLayerIDsByNative.removeAll()
|
|
484
|
+
animationDefinitions.removeAll()
|
|
485
|
+
waveTextStyleFingerprints.removeAll()
|
|
486
|
+
nativeWaveTextStyleIDs.removeAll()
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private func rebuild(_ scene: MapLabelSceneRecord, images: [String: UIImage]) throws {
|
|
490
|
+
let nativePoiStyles = try scene.styles.map { style in
|
|
491
|
+
let nativeID = makeNativeID("poi_style")
|
|
492
|
+
return try (publicID: style.id, native: style.native(images, styleID: nativeID))
|
|
493
|
+
}
|
|
494
|
+
var nextWaveTextStyleFingerprints = waveTextStyleFingerprints
|
|
495
|
+
var nativeWaveTextStyles: [WaveTextStyle] = []
|
|
496
|
+
for style in scene.pathTextStyles {
|
|
497
|
+
let fingerprint = style.fingerprint
|
|
498
|
+
if let registered = waveTextStyleFingerprints[style.id] {
|
|
499
|
+
guard registered == fingerprint else {
|
|
500
|
+
throw labelError(
|
|
501
|
+
labelUnsupported,
|
|
502
|
+
"iOS Kakao Maps SDK cannot replace the registered WaveText style: \(style.id)"
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
let nativeID = makeNativeID("wave_style")
|
|
507
|
+
nativeWaveTextStyleIDs[style.id] = nativeID
|
|
508
|
+
try nativeWaveTextStyles.append(style.native(styleID: nativeID))
|
|
509
|
+
nextWaveTextStyleFingerprints[style.id] = fingerprint
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
let labelLayerOptions = try scene.layers.map { layer in
|
|
513
|
+
try layer.options(layerID: makeNativeID("layer"))
|
|
514
|
+
}
|
|
515
|
+
let lodLabelLayerOptions = try scene.lodLayers.map { layer in
|
|
516
|
+
try layer.options(layerID: makeNativeID("lod_layer"))
|
|
517
|
+
}
|
|
518
|
+
clearAnimationRuns()
|
|
519
|
+
trackingManager.stopTracking()
|
|
520
|
+
clearHandlers()
|
|
521
|
+
clearLayersAndStyles()
|
|
522
|
+
labels.removeAll()
|
|
523
|
+
lodLabels.removeAll()
|
|
524
|
+
coordinates.removeAll()
|
|
525
|
+
pathLabels.removeAll()
|
|
526
|
+
labelLayers.removeAll()
|
|
527
|
+
lodLabelLayers.removeAll()
|
|
528
|
+
publicLayerIDsByNative.removeAll()
|
|
529
|
+
animationDefinitions.removeAll()
|
|
530
|
+
|
|
531
|
+
var poiStyleIDs: [String: String] = [:]
|
|
532
|
+
for style in nativePoiStyles {
|
|
533
|
+
manager.addPoiStyle(style.native)
|
|
534
|
+
styleIds.insert(style.native.styleID)
|
|
535
|
+
poiStyleIDs[style.publicID] = style.native.styleID
|
|
536
|
+
}
|
|
537
|
+
for style in nativeWaveTextStyles {
|
|
538
|
+
manager.addWaveTextStyle(style)
|
|
539
|
+
}
|
|
540
|
+
waveTextStyleFingerprints = nextWaveTextStyleFingerprints
|
|
541
|
+
|
|
542
|
+
var layers: [String: LabelLayer] = [:]
|
|
543
|
+
for (layer, options) in zip(scene.layers, labelLayerOptions) {
|
|
544
|
+
guard let native = manager.addLabelLayer(option: options) else {
|
|
545
|
+
throw labelError(labelInvalidArgument, "Could not create label layer \(layer.id).")
|
|
546
|
+
}
|
|
547
|
+
native.visible = layer.visible
|
|
548
|
+
native.setClickable(layer.clickable)
|
|
549
|
+
layers[layer.id] = native
|
|
550
|
+
labelLayers[native.layerID] = native
|
|
551
|
+
publicLayerIDsByNative[native.layerID] = layer.id
|
|
552
|
+
}
|
|
553
|
+
var lodLayers: [String: LodLabelLayer] = [:]
|
|
554
|
+
for (layer, options) in zip(scene.lodLayers, lodLabelLayerOptions) {
|
|
555
|
+
guard let native = manager.addLodLabelLayer(option: options) else {
|
|
556
|
+
throw labelError(labelInvalidArgument, "Could not create LOD label layer \(layer.id).")
|
|
557
|
+
}
|
|
558
|
+
native.visible = layer.visible
|
|
559
|
+
native.setClickable(layer.clickable)
|
|
560
|
+
lodLayers[layer.id] = native
|
|
561
|
+
lodLabelLayers[native.layerID] = native
|
|
562
|
+
publicLayerIDsByNative[native.layerID] = layer.id
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
for item in scene.labels {
|
|
566
|
+
guard let layer = layers[item.layerId] else { throw notFound("layer", item.layerId) }
|
|
567
|
+
guard let styleID = poiStyleIDs[item.styleId] else { throw notFound("style", item.styleId) }
|
|
568
|
+
let option = try item.options(styleID: styleID)
|
|
569
|
+
guard let poi = try layer.addPoi(option: option, at: item.coordinate.mapPoint) else {
|
|
570
|
+
throw labelError(labelInvalidArgument, "Could not create label \(item.id).")
|
|
571
|
+
}
|
|
572
|
+
poi.pixelOffset = try item.pixelOffset?.pixelPoint ?? .zero
|
|
573
|
+
poi.orientation = item.orientation * .pi / 180
|
|
574
|
+
for badge in item.badges {
|
|
575
|
+
let native = try badge.native(images)
|
|
576
|
+
poi.addBadge(native)
|
|
577
|
+
if !badge.visible {
|
|
578
|
+
poi.hideBadge(badgeID: badge.id)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
item.visible ? poi.show() : poi.hide()
|
|
582
|
+
eventHandlers.append(poi.addPoiTappedEventHandler(target: self) { target in
|
|
583
|
+
{ param in target.emit(param.poiItem, kind: "label") }
|
|
584
|
+
})
|
|
585
|
+
labels[item.id] = poi
|
|
586
|
+
coordinates[item.id] = item.coordinate
|
|
587
|
+
}
|
|
588
|
+
for item in scene.lodLabels {
|
|
589
|
+
guard let layer = lodLayers[item.layerId] else { throw notFound("LOD layer", item.layerId) }
|
|
590
|
+
guard let styleID = poiStyleIDs[item.styleId] else { throw notFound("style", item.styleId) }
|
|
591
|
+
let option = try item.options(styleID: styleID)
|
|
592
|
+
guard let poi = try layer.addLodPoi(option: option, at: item.coordinate.mapPoint)
|
|
593
|
+
else {
|
|
594
|
+
throw labelError(labelInvalidArgument, "Could not create LOD label \(item.id).")
|
|
595
|
+
}
|
|
596
|
+
for badge in item.badges {
|
|
597
|
+
let native = try badge.native(images)
|
|
598
|
+
poi.addBadge(native)
|
|
599
|
+
if !badge.visible {
|
|
600
|
+
poi.hideBadge(badgeID: badge.id)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
item.visible ? poi.show() : poi.hide()
|
|
604
|
+
eventHandlers.append(poi.addPoiTappedEventHandler(target: self) { target in
|
|
605
|
+
{ param in target.emit(param.poiItem, kind: "lodLabel") }
|
|
606
|
+
})
|
|
607
|
+
lodLabels[item.id] = poi
|
|
608
|
+
coordinates[item.id] = item.coordinate
|
|
609
|
+
}
|
|
610
|
+
for item in scene.pathLabels {
|
|
611
|
+
guard let layer = layers[item.layerId] else { throw notFound("layer", item.layerId) }
|
|
612
|
+
guard let styleID = nativeWaveTextStyleIDs[item.styleId] else {
|
|
613
|
+
throw notFound("path text style", item.styleId)
|
|
614
|
+
}
|
|
615
|
+
let option = WaveTextOptions(styleID: styleID, waveTextID: item.id)
|
|
616
|
+
option.points = try item.points.map { try $0.mapPoint }
|
|
617
|
+
option.text = item.text
|
|
618
|
+
option.rank = try UInt(item.rank.nonnegativeInteger("rank"))
|
|
619
|
+
guard let text = layer.addWaveText(option) else {
|
|
620
|
+
throw labelError(labelInvalidArgument, "Could not create path label \(item.id).")
|
|
621
|
+
}
|
|
622
|
+
item.visible ? text.show() : text.hide()
|
|
623
|
+
pathLabels[item.id] = text
|
|
624
|
+
}
|
|
625
|
+
for animation in scene.animations {
|
|
626
|
+
try addAnimation(animation)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private func addAnimation(_ animation: MapLabelAnimationRecord) throws {
|
|
631
|
+
animationDefinitions[animation.id] = animation
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private func startAnimation(_ id: String) throws {
|
|
635
|
+
guard var definition = animationDefinitions[id] else {
|
|
636
|
+
throw labelError(labelNotFound, "Animation not found: \(id)")
|
|
637
|
+
}
|
|
638
|
+
if let run = activeAnimationRuns[id] {
|
|
639
|
+
finishAnimationRun(id: id, run: run, finished: false, stopNative: true)
|
|
640
|
+
guard let currentDefinition = animationDefinitions[id] else {
|
|
641
|
+
throw labelError(labelNotFound, "Animation not found: \(id)")
|
|
642
|
+
}
|
|
643
|
+
definition = currentDefinition
|
|
644
|
+
}
|
|
645
|
+
let targets = try definition.labelIds.map(label)
|
|
646
|
+
nextAnimationEpoch += 1
|
|
647
|
+
let epoch = nextAnimationEpoch
|
|
648
|
+
let nativeID = makeNativeID("animation")
|
|
649
|
+
let effect = try definition.effectObject()
|
|
650
|
+
guard let animator = manager.addPoiAnimator(
|
|
651
|
+
animatorID: nativeID,
|
|
652
|
+
effect: effect
|
|
653
|
+
) else {
|
|
654
|
+
throw labelError(labelInvalidArgument, "Could not create animation \(id).")
|
|
655
|
+
}
|
|
656
|
+
do {
|
|
657
|
+
try animator.addPois(targets)
|
|
658
|
+
} catch {
|
|
659
|
+
manager.removePoiAnimator(animatorID: nativeID)
|
|
660
|
+
throw error
|
|
661
|
+
}
|
|
662
|
+
let run = ActiveAnimationRun(nativeID: nativeID, epoch: epoch, definition: definition)
|
|
663
|
+
activeAnimationRuns[id] = run
|
|
664
|
+
animator.setStopCallback { [weak self] _ in
|
|
665
|
+
guard let self, !isDisposed, let active = activeAnimationRuns[id],
|
|
666
|
+
active.nativeID == nativeID, active.epoch == epoch
|
|
667
|
+
else { return }
|
|
668
|
+
finishAnimationRun(id: id, run: active, finished: true, stopNative: false)
|
|
669
|
+
}
|
|
670
|
+
animator.start()
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
private func finishAnimationRun(
|
|
674
|
+
id: String,
|
|
675
|
+
run: ActiveAnimationRun,
|
|
676
|
+
finished: Bool,
|
|
677
|
+
stopNative: Bool
|
|
678
|
+
) {
|
|
679
|
+
guard let active = activeAnimationRuns[id], active.nativeID == run.nativeID,
|
|
680
|
+
active.epoch == run.epoch
|
|
681
|
+
else { return }
|
|
682
|
+
activeAnimationRuns.removeValue(forKey: id)
|
|
683
|
+
if stopNative {
|
|
684
|
+
manager.getPoiAnimator(animatorID: run.nativeID)?.stop()
|
|
685
|
+
}
|
|
686
|
+
manager.removePoiAnimator(animatorID: run.nativeID)
|
|
687
|
+
if run.definition.removeAtStop {
|
|
688
|
+
removeAnimationTargets(run.definition.labelIds)
|
|
689
|
+
}
|
|
690
|
+
emitAnimationStopped(["id": id, "finished": finished])
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private func clearAnimationRuns() {
|
|
694
|
+
let runs = Array(activeAnimationRuns.values)
|
|
695
|
+
activeAnimationRuns.removeAll()
|
|
696
|
+
for run in runs {
|
|
697
|
+
manager.getPoiAnimator(animatorID: run.nativeID)?.stop()
|
|
698
|
+
manager.removePoiAnimator(animatorID: run.nativeID)
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private func removeAnimationTargets(_ ids: [String]) {
|
|
703
|
+
var removedIDs = Set(ids)
|
|
704
|
+
guard !removedIDs.isEmpty else { return }
|
|
705
|
+
var canceledRuns: [(id: String, run: ActiveAnimationRun)] = []
|
|
706
|
+
while true {
|
|
707
|
+
let invalidDefinitionIDs = animationDefinitions.compactMap { id, definition in
|
|
708
|
+
definition.labelIds.contains(where: removedIDs.contains) ? id : nil
|
|
709
|
+
}
|
|
710
|
+
guard !invalidDefinitionIDs.isEmpty else { break }
|
|
711
|
+
var didAddRemovedID = false
|
|
712
|
+
for id in invalidDefinitionIDs {
|
|
713
|
+
animationDefinitions.removeValue(forKey: id)
|
|
714
|
+
if let run = activeAnimationRuns.removeValue(forKey: id) {
|
|
715
|
+
canceledRuns.append((id, run))
|
|
716
|
+
if run.definition.removeAtStop {
|
|
717
|
+
for targetID in run.definition.labelIds where removedIDs.insert(targetID).inserted {
|
|
718
|
+
didAddRemovedID = true
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if !didAddRemovedID {
|
|
724
|
+
break
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
for canceled in canceledRuns {
|
|
728
|
+
manager.getPoiAnimator(animatorID: canceled.run.nativeID)?.stop()
|
|
729
|
+
manager.removePoiAnimator(animatorID: canceled.run.nativeID)
|
|
730
|
+
emitAnimationStopped(["id": canceled.id, "finished": false])
|
|
731
|
+
}
|
|
732
|
+
for id in removedIDs {
|
|
733
|
+
labels.removeValue(forKey: id)
|
|
734
|
+
coordinates.removeValue(forKey: id)
|
|
735
|
+
}
|
|
736
|
+
trackingManager.stopTracking()
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private func show(_ id: String, visible: Bool, autoMove: Bool) throws {
|
|
740
|
+
if let poi = labels[id] {
|
|
741
|
+
if visible, autoMove {
|
|
742
|
+
poi.showWithAutoMove()
|
|
743
|
+
} else {
|
|
744
|
+
visible ? poi.show() : poi.hide()
|
|
745
|
+
}
|
|
746
|
+
return
|
|
747
|
+
}
|
|
748
|
+
if let poi = lodLabels[id] {
|
|
749
|
+
if visible, autoMove {
|
|
750
|
+
poi.showWithAutoMove()
|
|
751
|
+
} else {
|
|
752
|
+
visible ? poi.show() : poi.hide()
|
|
753
|
+
}
|
|
754
|
+
return
|
|
755
|
+
}
|
|
756
|
+
if let text = pathLabels[id] {
|
|
757
|
+
if autoMove {
|
|
758
|
+
throw labelError(labelUnsupported, "Path labels do not expose auto-move.")
|
|
759
|
+
}
|
|
760
|
+
visible ? text.show() : text.hide(); return
|
|
761
|
+
}
|
|
762
|
+
throw notFound("label", id)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private func share(_ command: MapLabelCommandRecord, transform: Bool) throws {
|
|
766
|
+
let isEnabled = try command.enabled.required("enabled")
|
|
767
|
+
let source = try label(command.sourceId.required("sourceId"))
|
|
768
|
+
let targetID = try command.targetId.required("targetId")
|
|
769
|
+
let target = try label(targetID)
|
|
770
|
+
if transform {
|
|
771
|
+
isEnabled ? source.shareTransformWithPoi(target) : source
|
|
772
|
+
.removeShareTransformWithPoi(target)
|
|
773
|
+
} else {
|
|
774
|
+
isEnabled ? source.sharePositionWithPoi(target) : source
|
|
775
|
+
.removeSharePositionWithPoi(target)
|
|
776
|
+
}
|
|
777
|
+
if !isEnabled {
|
|
778
|
+
labelsWithReleasedSharing.insert(targetID)
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
private func setBadgeVisibility(_ command: MapLabelCommandRecord) throws {
|
|
783
|
+
let isVisible = try command.visible.required("visible")
|
|
784
|
+
let id = try command.requiredID()
|
|
785
|
+
let badgeID = try command.badgeId.required("badgeId")
|
|
786
|
+
if let poi = labels[id] {
|
|
787
|
+
if command.style {
|
|
788
|
+
isVisible ? poi.showStyleBadge(badgeID: badgeID) : poi
|
|
789
|
+
.hideStyleBadge(badgeID: badgeID)
|
|
790
|
+
} else {
|
|
791
|
+
isVisible ? poi.showBadge(badgeID: badgeID) : poi.hideBadge(badgeID: badgeID)
|
|
792
|
+
}
|
|
793
|
+
return
|
|
794
|
+
}
|
|
795
|
+
if let poi = lodLabels[id] {
|
|
796
|
+
if command.style {
|
|
797
|
+
isVisible ? poi.showStyleBadge(badgeID: badgeID) : poi
|
|
798
|
+
.hideStyleBadge(badgeID: badgeID)
|
|
799
|
+
} else {
|
|
800
|
+
isVisible ? poi.showBadge(badgeID: badgeID) : poi.hideBadge(badgeID: badgeID)
|
|
801
|
+
}
|
|
802
|
+
return
|
|
803
|
+
}
|
|
804
|
+
throw notFound("label", id)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
private func label(_ id: String) throws -> Poi {
|
|
808
|
+
if let label = labels[id] {
|
|
809
|
+
return label
|
|
810
|
+
}
|
|
811
|
+
if lodLabels[id] != nil {
|
|
812
|
+
throw labelError(
|
|
813
|
+
labelUnsupported,
|
|
814
|
+
"LOD label \(id) cannot be moved, transformed, animated, or tracked."
|
|
815
|
+
)
|
|
816
|
+
}
|
|
817
|
+
throw notFound("label", id)
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
private func emit(_ label: any KakaoMapsSDK.Label, kind: String) {
|
|
821
|
+
guard !isDisposed, let coordinate = coordinates[label.itemID] else { return }
|
|
822
|
+
if let poi = label as? Poi {
|
|
823
|
+
guard let current = labels[label.itemID],
|
|
824
|
+
let native = labelLayers[label.layerID]?.getPoi(poiID: label.itemID),
|
|
825
|
+
current === poi, native === poi
|
|
826
|
+
else { return }
|
|
827
|
+
} else if let poi = label as? LodPoi {
|
|
828
|
+
guard let current = lodLabels[label.itemID],
|
|
829
|
+
let native = lodLabelLayers[label.layerID]?.getLodPoi(poiID: label.itemID),
|
|
830
|
+
current === poi, native === poi
|
|
831
|
+
else { return }
|
|
832
|
+
} else {
|
|
833
|
+
return
|
|
834
|
+
}
|
|
835
|
+
let latitude: Double
|
|
836
|
+
let longitude: Double
|
|
837
|
+
if let poi = label as? Poi {
|
|
838
|
+
latitude = poi.position.wgsCoord.latitude
|
|
839
|
+
longitude = poi.position.wgsCoord.longitude
|
|
840
|
+
} else {
|
|
841
|
+
latitude = coordinate.latitude
|
|
842
|
+
longitude = coordinate.longitude
|
|
843
|
+
}
|
|
844
|
+
emitClick([
|
|
845
|
+
"id": label.itemID,
|
|
846
|
+
"layerId": publicLayerIDsByNative[label.layerID] ?? label.layerID,
|
|
847
|
+
"kind": kind,
|
|
848
|
+
"coordinate": [
|
|
849
|
+
"latitude": latitude,
|
|
850
|
+
"longitude": longitude
|
|
851
|
+
]
|
|
852
|
+
])
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
private func clearHandlers() {
|
|
856
|
+
eventHandlers.forEach { $0.dispose() }
|
|
857
|
+
eventHandlers.removeAll()
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
private func clearLayersAndStyles() {
|
|
861
|
+
labelsWithReleasedSharing.removeAll()
|
|
862
|
+
labelLayers.keys.forEach(manager.removeLabelLayer)
|
|
863
|
+
lodLabelLayers.keys.forEach(manager.removeLodLabelLayer)
|
|
864
|
+
styleIds.forEach(manager.removePoiStyle)
|
|
865
|
+
styleIds.removeAll()
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
private func makeNativeID(_ kind: String) -> String {
|
|
869
|
+
nextNativeObjectID += 1
|
|
870
|
+
return "__rnk_\(nativeNamespace)_\(kind)_\(nextNativeObjectID)"
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
private func requireActive() throws {
|
|
874
|
+
if isDisposed {
|
|
875
|
+
throw labelError("ERR_KAKAO_MAP_VIEW_DESTROYED", "The map view was destroyed.")
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
private func loadImages(
|
|
880
|
+
_ uris: Set<String>,
|
|
881
|
+
generation currentGeneration: Int,
|
|
882
|
+
completion: @escaping (Result<[String: UIImage], Error>) -> Void
|
|
883
|
+
) {
|
|
884
|
+
guard !uris.isEmpty else {
|
|
885
|
+
completion(.success([:]))
|
|
886
|
+
return
|
|
887
|
+
}
|
|
888
|
+
var images: [String: UIImage] = [:]
|
|
889
|
+
var remaining = uris.count
|
|
890
|
+
var didFinish = false
|
|
891
|
+
for uri in uris {
|
|
892
|
+
let token = imageLoader.load(uri) { [weak self] result in
|
|
893
|
+
guard let self, !isDisposed, currentGeneration == generation, !didFinish else { return }
|
|
894
|
+
switch result {
|
|
895
|
+
case let .success(image):
|
|
896
|
+
images[uri] = image
|
|
897
|
+
remaining -= 1
|
|
898
|
+
if remaining == 0 {
|
|
899
|
+
didFinish = true
|
|
900
|
+
imageRequests.removeAll()
|
|
901
|
+
completion(.success(images))
|
|
902
|
+
}
|
|
903
|
+
case let .failure(error):
|
|
904
|
+
didFinish = true
|
|
905
|
+
cancelImageRequests()
|
|
906
|
+
completion(.failure(error))
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
imageRequests.append(token)
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
private func cancelImageRequests() {
|
|
914
|
+
imageRequests.forEach { $0.cancel() }
|
|
915
|
+
imageRequests.removeAll()
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
private func finishApply(_ result: Result<Void, Error>) {
|
|
919
|
+
guard let completion = pendingApplyCompletion else { return }
|
|
920
|
+
pendingApplyCompletion = nil
|
|
921
|
+
completion(result)
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private func cancelPendingApply(_ message: String) {
|
|
925
|
+
finishApply(.failure(labelError(labelApplyCancelled, message)))
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
private func validate(_ scene: MapLabelSceneRecord) throws {
|
|
930
|
+
try unique("style", scene.styles.map(\.id))
|
|
931
|
+
try unique("path text style", scene.pathTextStyles.map(\.id))
|
|
932
|
+
try unique("layer", scene.layers.map(\.id) + scene.lodLayers.map(\.id))
|
|
933
|
+
try unique(
|
|
934
|
+
"label",
|
|
935
|
+
scene.labels.map(\.id) + scene.lodLabels.map(\.id) + scene.pathLabels.map(\.id)
|
|
936
|
+
)
|
|
937
|
+
try unique("animation", scene.animations.map(\.id))
|
|
938
|
+
let styles = Dictionary(uniqueKeysWithValues: scene.styles.map { ($0.id, $0) })
|
|
939
|
+
let layerIDs = Set(scene.layers.map(\.id))
|
|
940
|
+
let lodLayerIDs = Set(scene.lodLayers.map(\.id))
|
|
941
|
+
let regularLabelIDs = Set(scene.labels.map(\.id))
|
|
942
|
+
let allIDs = scene.styles.map(\.id) + scene.pathTextStyles.map(\.id) + Array(layerIDs) +
|
|
943
|
+
Array(lodLayerIDs) + Array(regularLabelIDs) + scene.lodLabels.map(\.id) +
|
|
944
|
+
scene.pathLabels.map(\.id) + scene.animations.map(\.id)
|
|
945
|
+
if allIDs.contains(where: \.isEmpty) {
|
|
946
|
+
throw labelError(labelInvalidArgument, "IDs must not be empty.")
|
|
947
|
+
}
|
|
948
|
+
for style in scene.styles {
|
|
949
|
+
guard !style.levels.isEmpty else { throw labelError(
|
|
950
|
+
labelInvalidArgument,
|
|
951
|
+
"Style \(style.id) needs at least one level."
|
|
952
|
+
) }
|
|
953
|
+
try unique("style level", style.levels.map { String($0.level) })
|
|
954
|
+
for level in style.levels {
|
|
955
|
+
guard level.padding >= 0 else {
|
|
956
|
+
throw labelError(labelInvalidArgument, "Style padding must be non-negative.")
|
|
957
|
+
}
|
|
958
|
+
try unique("style badge", level.badges.map(\.id))
|
|
959
|
+
try level.image?.validate()
|
|
960
|
+
try level.badges.forEach { try $0.image.validate() }
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
for style in scene.pathTextStyles {
|
|
964
|
+
guard !style.levels.isEmpty else {
|
|
965
|
+
throw labelError(labelInvalidArgument, "Path style \(style.id) needs at least one level.")
|
|
966
|
+
}
|
|
967
|
+
try unique("path style level", style.levels.map { String($0.level) })
|
|
968
|
+
}
|
|
969
|
+
if scene.layers.contains(where: { !$0.competitionExceptions.isEmpty }) ||
|
|
970
|
+
scene.lodLayers.contains(where: { !$0.competitionExceptions.isEmpty })
|
|
971
|
+
{
|
|
972
|
+
throw labelError(
|
|
973
|
+
labelUnsupported,
|
|
974
|
+
"iOS Kakao Maps SDK does not expose layer competition exceptions."
|
|
975
|
+
)
|
|
976
|
+
}
|
|
977
|
+
for layer in scene.lodLayers where layer.radius < 0 {
|
|
978
|
+
throw labelError(labelInvalidArgument, "LOD radius must be non-negative.")
|
|
979
|
+
}
|
|
980
|
+
for item in scene.labels {
|
|
981
|
+
_ = try item.coordinate.mapPoint
|
|
982
|
+
guard layerIDs.contains(item.layerId) else { throw notFound("layer", item.layerId) }
|
|
983
|
+
guard let style = styles[item.styleId] else { throw notFound("style", item.styleId) }
|
|
984
|
+
try validateTextIndices(item.text, style: style, labelID: item.id)
|
|
985
|
+
_ = try item.rank.integer("rank")
|
|
986
|
+
_ = try transformType(item.transform)
|
|
987
|
+
_ = try item.pixelOffset?.point
|
|
988
|
+
_ = try item.orientation.finiteFloat("orientation")
|
|
989
|
+
try item.scale?.validateScale()
|
|
990
|
+
try unique("label badge", item.badges.map(\.id))
|
|
991
|
+
try item.badges.forEach {
|
|
992
|
+
try $0.image.validate()
|
|
993
|
+
_ = try $0.offset?.point
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
for item in scene.lodLabels {
|
|
997
|
+
_ = try item.coordinate.mapPoint
|
|
998
|
+
guard lodLayerIDs.contains(item.layerId) else { throw notFound("LOD layer", item.layerId) }
|
|
999
|
+
guard let style = styles[item.styleId] else { throw notFound("style", item.styleId) }
|
|
1000
|
+
try validateTextIndices(item.text, style: style, labelID: item.id)
|
|
1001
|
+
_ = try item.rank.integer("rank")
|
|
1002
|
+
_ = try transformType(item.transform)
|
|
1003
|
+
try unique("label badge", item.badges.map(\.id))
|
|
1004
|
+
try item.badges.forEach {
|
|
1005
|
+
try $0.image.validate()
|
|
1006
|
+
_ = try $0.offset?.point
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
for item in scene.pathLabels {
|
|
1010
|
+
guard item.points.count >= 2 else {
|
|
1011
|
+
throw labelError(labelInvalidArgument, "Path label \(item.id) needs at least two points.")
|
|
1012
|
+
}
|
|
1013
|
+
_ = try item.points.map { try $0.mapPoint }
|
|
1014
|
+
guard layerIDs.contains(item.layerId) else { throw notFound("layer", item.layerId) }
|
|
1015
|
+
guard scene.pathTextStyles.contains(where: { $0.id == item.styleId }) else {
|
|
1016
|
+
throw notFound("path text style", item.styleId)
|
|
1017
|
+
}
|
|
1018
|
+
_ = try item.rank.nonnegativeInteger("rank")
|
|
1019
|
+
}
|
|
1020
|
+
for animation in scene.animations {
|
|
1021
|
+
guard !animation.labelIds.isEmpty, !animation.keyframes.isEmpty else {
|
|
1022
|
+
throw labelError(labelInvalidArgument, "Animation \(animation.id) cannot be empty.")
|
|
1023
|
+
}
|
|
1024
|
+
try unique("animation label", animation.labelIds)
|
|
1025
|
+
for id in animation.labelIds where !regularLabelIDs.contains(id) {
|
|
1026
|
+
throw notFound("animatable label", id)
|
|
1027
|
+
}
|
|
1028
|
+
guard animation.repeatCount >= 0 else {
|
|
1029
|
+
throw labelError(labelInvalidArgument, "Animation repeatCount must be non-negative.")
|
|
1030
|
+
}
|
|
1031
|
+
guard ["drop", "alpha", "scale", "scaleAlpha", "transform"].contains(animation.effect) else {
|
|
1032
|
+
throw labelError(labelInvalidArgument, "Unknown label animation effect: \(animation.effect)")
|
|
1033
|
+
}
|
|
1034
|
+
if animation.effect != "drop", animation.repeatCount != 1 {
|
|
1035
|
+
throw labelError(
|
|
1036
|
+
labelUnsupported,
|
|
1037
|
+
"Keyframe label animations do not expose repeatCount on iOS."
|
|
1038
|
+
)
|
|
1039
|
+
}
|
|
1040
|
+
if animation.effect == "drop", animation.keyframes.count != 1 {
|
|
1041
|
+
throw labelError(labelInvalidArgument, "Drop animation requires exactly one keyframe.")
|
|
1042
|
+
}
|
|
1043
|
+
for frame in animation.keyframes {
|
|
1044
|
+
_ = try frame.duration.duration()
|
|
1045
|
+
_ = try interpolation(frame.interpolation, duration: frame.duration)
|
|
1046
|
+
_ = try frame.alpha?.unitFloat("alpha")
|
|
1047
|
+
try frame.scale?.validateScale()
|
|
1048
|
+
_ = try frame.translation?.point
|
|
1049
|
+
_ = try frame.rotation?.finiteFloat("rotation")
|
|
1050
|
+
if ["scale", "scaleAlpha"].contains(animation.effect), frame.scale == nil {
|
|
1051
|
+
throw labelError(
|
|
1052
|
+
labelInvalidArgument,
|
|
1053
|
+
"\(animation.effect) animation requires scale in every keyframe."
|
|
1054
|
+
)
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
private func validateTextIndices(
|
|
1061
|
+
_ texts: [MapLabelTextRecord],
|
|
1062
|
+
style: MapLabelStyleRecord,
|
|
1063
|
+
labelID: String
|
|
1064
|
+
) throws {
|
|
1065
|
+
for text in texts where text.styleIndex < 0 || style.levels.contains(where: {
|
|
1066
|
+
text.styleIndex >= $0.textLines.count
|
|
1067
|
+
}) {
|
|
1068
|
+
throw labelError(
|
|
1069
|
+
labelInvalidArgument,
|
|
1070
|
+
"Label \(labelID) has a text style index that is unavailable at one or more levels."
|
|
1071
|
+
)
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
private extension MapLabelSceneRecord {
|
|
1076
|
+
var imageUris: Set<String> {
|
|
1077
|
+
var result = Set<String>()
|
|
1078
|
+
for level in styles.flatMap(\.levels) {
|
|
1079
|
+
if let image = level.image {
|
|
1080
|
+
result.insert(image.uri)
|
|
1081
|
+
}
|
|
1082
|
+
level.badges.forEach { result.insert($0.image.uri) }
|
|
1083
|
+
}
|
|
1084
|
+
(labels.flatMap(\.badges) + lodLabels.flatMap(\.badges)).forEach { result.insert($0.image.uri) }
|
|
1085
|
+
return result
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
private extension MapLabelStyleRecord {
|
|
1090
|
+
func native(_ images: [String: UIImage], styleID: String) throws -> PoiStyle {
|
|
1091
|
+
let styles = try levels.sorted { $0.level < $1.level }.map { level -> PerLevelPoiStyle in
|
|
1092
|
+
let icon: PoiIconStyle? = try level.image.map { image in
|
|
1093
|
+
guard let native = images[image.uri] else { throw labelError(
|
|
1094
|
+
labelInvalidArgument,
|
|
1095
|
+
"Image was not loaded: \(image.uri)"
|
|
1096
|
+
) }
|
|
1097
|
+
let transition = level.iconTransition ?? MapLabelTransitionRecord()
|
|
1098
|
+
return try PoiIconStyle(
|
|
1099
|
+
symbol: image.resized(native),
|
|
1100
|
+
anchorPoint: level.anchor?.point ?? CGPoint(x: 0.5, y: 0.5),
|
|
1101
|
+
transition: transition.native(),
|
|
1102
|
+
enableEntranceTransition: transition.enableEntrance,
|
|
1103
|
+
enableExitTransition: transition.enableExit,
|
|
1104
|
+
badges: level.badges.map { try $0.native(images) }
|
|
1105
|
+
)
|
|
1106
|
+
}
|
|
1107
|
+
let text: PoiTextStyle? = try level.textLines.isEmpty ? nil : {
|
|
1108
|
+
let transition = level.textTransition ?? MapLabelTransitionRecord()
|
|
1109
|
+
let result = try PoiTextStyle(
|
|
1110
|
+
transition: transition.native(),
|
|
1111
|
+
enableEntranceTransition: transition.enableEntrance,
|
|
1112
|
+
enableExitTransition: transition.enableExit,
|
|
1113
|
+
textLineStyles: level.textLines.map { try PoiTextLineStyle(textStyle: $0.native()) }
|
|
1114
|
+
)
|
|
1115
|
+
result.textLayouts = try Array(
|
|
1116
|
+
repeating: textLayout(level.textLayout),
|
|
1117
|
+
count: level.textLines.count
|
|
1118
|
+
)
|
|
1119
|
+
return result
|
|
1120
|
+
}()
|
|
1121
|
+
if let icon, let text {
|
|
1122
|
+
return try PerLevelPoiStyle(
|
|
1123
|
+
iconStyle: icon,
|
|
1124
|
+
textStyle: text,
|
|
1125
|
+
padding: level.padding.pixelFloat("padding"),
|
|
1126
|
+
level: level.level
|
|
1127
|
+
)
|
|
1128
|
+
}
|
|
1129
|
+
if let icon {
|
|
1130
|
+
return try PerLevelPoiStyle(
|
|
1131
|
+
iconStyle: icon,
|
|
1132
|
+
padding: level.padding.pixelFloat("padding"),
|
|
1133
|
+
level: level.level
|
|
1134
|
+
)
|
|
1135
|
+
}
|
|
1136
|
+
if let text {
|
|
1137
|
+
return try PerLevelPoiStyle(
|
|
1138
|
+
textStyle: text,
|
|
1139
|
+
padding: level.padding.pixelFloat("padding"),
|
|
1140
|
+
level: level.level
|
|
1141
|
+
)
|
|
1142
|
+
}
|
|
1143
|
+
throw labelError(
|
|
1144
|
+
labelInvalidArgument,
|
|
1145
|
+
"Style \(id) level \(level.level) needs an image or text style."
|
|
1146
|
+
)
|
|
1147
|
+
}
|
|
1148
|
+
return PoiStyle(styleID: styleID, styles: styles)
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
private extension MapPathTextStyleRecord {
|
|
1153
|
+
var fingerprint: String {
|
|
1154
|
+
levels.sorted { $0.level < $1.level }.map { level in
|
|
1155
|
+
let text = level.text
|
|
1156
|
+
return [
|
|
1157
|
+
String(level.level),
|
|
1158
|
+
fingerprintPart(text.fontFamily),
|
|
1159
|
+
String(text.fontSize.bitPattern),
|
|
1160
|
+
fingerprintPart(text.color),
|
|
1161
|
+
fingerprintPart(text.strokeColor),
|
|
1162
|
+
String(text.strokeWidth.bitPattern),
|
|
1163
|
+
String(text.characterSpacing.bitPattern),
|
|
1164
|
+
String(text.lineSpacing.bitPattern),
|
|
1165
|
+
String(text.aspectRatio.bitPattern)
|
|
1166
|
+
].joined(separator: "|")
|
|
1167
|
+
}.joined(separator: ";")
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
func native(styleID: String) throws -> WaveTextStyle {
|
|
1171
|
+
try WaveTextStyle(styleID: styleID, styles: levels.sorted { $0.level < $1.level }.map {
|
|
1172
|
+
try PerLevelWaveTextStyle(textStyle: $0.text.native(), level: $0.level)
|
|
1173
|
+
})
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
private func fingerprintPart(_ value: String?) -> String {
|
|
1178
|
+
guard let value else { return "-" }
|
|
1179
|
+
return "\(value.utf8.count):\(value)"
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
private extension MapLabelTextStyleRecord {
|
|
1183
|
+
func native() throws -> TextStyle {
|
|
1184
|
+
let nativeLineSpace = try lineSpacing.finiteFloat("lineSpacing")
|
|
1185
|
+
guard nativeLineSpace >= 1 else {
|
|
1186
|
+
throw labelError(labelInvalidArgument, "lineSpacing must be at least 1.")
|
|
1187
|
+
}
|
|
1188
|
+
return try TextStyle(
|
|
1189
|
+
fontSize: UInt(fontSize.positivePixelInteger("fontSize")),
|
|
1190
|
+
fontColor: UIColor(hex: color),
|
|
1191
|
+
strokeThickness: UInt(strokeWidth.nonnegativePixelInteger("strokeWidth")),
|
|
1192
|
+
strokeColor: UIColor(hex: strokeColor ?? "#00000000"),
|
|
1193
|
+
font: fontFamily ?? "",
|
|
1194
|
+
charSpace: characterSpacing.pixelInteger("characterSpacing"),
|
|
1195
|
+
lineSpace: nativeLineSpace,
|
|
1196
|
+
aspectRatio: aspectRatio.finiteFloat("aspectRatio")
|
|
1197
|
+
)
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
private extension MapLabelTransitionRecord {
|
|
1202
|
+
func native() throws -> PoiTransition {
|
|
1203
|
+
try PoiTransition(
|
|
1204
|
+
entrance: transition(entrance),
|
|
1205
|
+
exit: transition(exit)
|
|
1206
|
+
)
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
private extension MapLabelBadgeStyleRecord {
|
|
1211
|
+
func native(_ images: [String: UIImage]) throws -> PoiBadge {
|
|
1212
|
+
guard let native = images[image.uri] else { throw labelError(
|
|
1213
|
+
labelInvalidArgument,
|
|
1214
|
+
"Image was not loaded: \(image.uri)"
|
|
1215
|
+
) }
|
|
1216
|
+
return try PoiBadge(
|
|
1217
|
+
badgeID: id,
|
|
1218
|
+
image: image.resized(native),
|
|
1219
|
+
offset: offset?.pixelPoint ?? .zero,
|
|
1220
|
+
zOrder: zOrder
|
|
1221
|
+
)
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
private extension MapLabelBadgeRecord {
|
|
1226
|
+
func native(_ images: [String: UIImage]) throws -> PoiBadge {
|
|
1227
|
+
guard let native = images[image.uri] else { throw labelError(
|
|
1228
|
+
labelInvalidArgument,
|
|
1229
|
+
"Image was not loaded: \(image.uri)"
|
|
1230
|
+
) }
|
|
1231
|
+
return try PoiBadge(
|
|
1232
|
+
badgeID: id,
|
|
1233
|
+
image: image.resized(native),
|
|
1234
|
+
offset: offset?.pixelPoint ?? .zero,
|
|
1235
|
+
zOrder: zOrder
|
|
1236
|
+
)
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
private extension MapLabelLayerRecord {
|
|
1241
|
+
func options(layerID: String) throws -> LabelLayerOptions {
|
|
1242
|
+
if !competitionExceptions.isEmpty {
|
|
1243
|
+
throw labelError(
|
|
1244
|
+
labelUnsupported,
|
|
1245
|
+
"iOS Kakao Maps SDK does not expose layer competition exceptions."
|
|
1246
|
+
)
|
|
1247
|
+
}
|
|
1248
|
+
return try LabelLayerOptions(
|
|
1249
|
+
layerID: layerID,
|
|
1250
|
+
competitionType: competitionType(competition),
|
|
1251
|
+
competitionUnit: labelCompetitionUnit(competitionUnit),
|
|
1252
|
+
orderType: labelOrdering(ordering),
|
|
1253
|
+
zOrder: zOrder
|
|
1254
|
+
)
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
private extension MapLodLabelLayerRecord {
|
|
1259
|
+
func options(layerID: String) throws -> LodLabelLayerOptions {
|
|
1260
|
+
if !competitionExceptions.isEmpty {
|
|
1261
|
+
throw labelError(
|
|
1262
|
+
labelUnsupported,
|
|
1263
|
+
"iOS Kakao Maps SDK does not expose layer competition exceptions."
|
|
1264
|
+
)
|
|
1265
|
+
}
|
|
1266
|
+
return try LodLabelLayerOptions(
|
|
1267
|
+
layerID: layerID,
|
|
1268
|
+
competitionType: competitionType(competition),
|
|
1269
|
+
competitionUnit: labelCompetitionUnit(competitionUnit),
|
|
1270
|
+
orderType: labelOrdering(ordering),
|
|
1271
|
+
zOrder: zOrder,
|
|
1272
|
+
radius: radius.pixelFloat("radius")
|
|
1273
|
+
)
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
private extension MapLabelRecord {
|
|
1278
|
+
func options(styleID: String) throws -> PoiOptions {
|
|
1279
|
+
let option = PoiOptions(styleID: styleID, poiID: id)
|
|
1280
|
+
option.rank = try rank.integer("rank")
|
|
1281
|
+
option.clickable = clickable
|
|
1282
|
+
option.transformType = try transformType(transform)
|
|
1283
|
+
for line in text {
|
|
1284
|
+
try option.addText(PoiText(
|
|
1285
|
+
text: line.value,
|
|
1286
|
+
styleIndex: UInt(Double(line.styleIndex).nonnegativeInteger("styleIndex"))
|
|
1287
|
+
))
|
|
1288
|
+
}
|
|
1289
|
+
return option
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
private extension MapLodLabelRecord {
|
|
1294
|
+
func options(styleID: String) throws -> PoiOptions {
|
|
1295
|
+
let option = PoiOptions(styleID: styleID, poiID: id)
|
|
1296
|
+
option.rank = try rank.integer("rank")
|
|
1297
|
+
option.clickable = clickable
|
|
1298
|
+
option.transformType = try transformType(transform)
|
|
1299
|
+
for line in text {
|
|
1300
|
+
try option.addText(PoiText(
|
|
1301
|
+
text: line.value,
|
|
1302
|
+
styleIndex: UInt(Double(line.styleIndex).nonnegativeInteger("styleIndex"))
|
|
1303
|
+
))
|
|
1304
|
+
}
|
|
1305
|
+
return option
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
private extension MapLabelAnimationRecord {
|
|
1310
|
+
func effectObject() throws -> any PoiAnimationEffect {
|
|
1311
|
+
let effect: any PoiAnimationEffect
|
|
1312
|
+
switch self.effect {
|
|
1313
|
+
case "drop":
|
|
1314
|
+
let value = try DropAnimationEffect(pixelHeight: (keyframes.first?.translation?.y ?? 0)
|
|
1315
|
+
.pixelFloat("translation.y"))
|
|
1316
|
+
value.playCount = UInt(max(0, repeatCount))
|
|
1317
|
+
value.interpolation = try interpolation(
|
|
1318
|
+
keyframes.first?.interpolation ?? "linear",
|
|
1319
|
+
duration: keyframes.first?.duration ?? 0
|
|
1320
|
+
)
|
|
1321
|
+
effect = value
|
|
1322
|
+
case "alpha":
|
|
1323
|
+
let value = AlphaAnimationEffect()
|
|
1324
|
+
for frame in keyframes {
|
|
1325
|
+
try value.addKeyframe(AlphaAnimationKeyFrame(
|
|
1326
|
+
alpha: (frame.alpha ?? 1).unitFloat("alpha"),
|
|
1327
|
+
interpolation: interpolation(frame.interpolation, duration: frame.duration)
|
|
1328
|
+
))
|
|
1329
|
+
}
|
|
1330
|
+
value.resetToInitialState = resetToInitialState
|
|
1331
|
+
effect = value
|
|
1332
|
+
case "scale":
|
|
1333
|
+
let value = ScaleAnimationEffect()
|
|
1334
|
+
for frame in keyframes {
|
|
1335
|
+
let scale = try frame.scale.required("scale")
|
|
1336
|
+
try value.addKeyframe(ScaleAnimationKeyFrame(
|
|
1337
|
+
scale: Vector2(x: scale.x.finiteDouble("scale.x"), y: scale.y.finiteDouble("scale.y")),
|
|
1338
|
+
interpolation: interpolation(frame.interpolation, duration: frame.duration)
|
|
1339
|
+
))
|
|
1340
|
+
}
|
|
1341
|
+
value.resetToInitialState = resetToInitialState
|
|
1342
|
+
effect = value
|
|
1343
|
+
case "scaleAlpha":
|
|
1344
|
+
let value = ScaleAlphaAnimationEffect()
|
|
1345
|
+
for frame in keyframes {
|
|
1346
|
+
let scale = try frame.scale.required("scale")
|
|
1347
|
+
try value.addKeyframe(ScaleAlphaAnimationKeyFrame(
|
|
1348
|
+
scale: Vector2(x: scale.x.finiteDouble("scale.x"), y: scale.y.finiteDouble("scale.y")),
|
|
1349
|
+
alpha: (frame.alpha ?? 1).unitFloat("alpha"),
|
|
1350
|
+
interpolation: interpolation(frame.interpolation, duration: frame.duration)
|
|
1351
|
+
))
|
|
1352
|
+
}
|
|
1353
|
+
value.resetToInitialState = resetToInitialState
|
|
1354
|
+
effect = value
|
|
1355
|
+
case "transform":
|
|
1356
|
+
let value = TransformAnimationEffect()
|
|
1357
|
+
for frame in keyframes {
|
|
1358
|
+
try value.addKeyframe(TransformAnimationKeyFrame(
|
|
1359
|
+
translation: Vector2(
|
|
1360
|
+
x: (frame.translation?.x ?? 0).pixelDouble("translation.x"),
|
|
1361
|
+
y: (frame.translation?.y ?? 0).pixelDouble("translation.y")
|
|
1362
|
+
),
|
|
1363
|
+
rotation: ((frame.rotation ?? 0) * .pi / 180).finiteFloat("rotation"),
|
|
1364
|
+
scale: Vector2(
|
|
1365
|
+
x: (frame.scale?.x ?? 1).finiteDouble("scale.x"),
|
|
1366
|
+
y: (frame.scale?.y ?? 1).finiteDouble("scale.y")
|
|
1367
|
+
),
|
|
1368
|
+
alpha: (frame.alpha ?? 1).unitFloat("alpha"),
|
|
1369
|
+
interpolation: interpolation(frame.interpolation, duration: frame.duration)
|
|
1370
|
+
))
|
|
1371
|
+
}
|
|
1372
|
+
value.resetToInitialState = resetToInitialState
|
|
1373
|
+
effect = value
|
|
1374
|
+
default:
|
|
1375
|
+
throw labelError(labelInvalidArgument, "Unknown label animation effect: \(self.effect)")
|
|
1376
|
+
}
|
|
1377
|
+
effect.hideAtStop = hideAtStop
|
|
1378
|
+
effect.removeAtStop = removeAtStop
|
|
1379
|
+
return effect
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
private extension MapLabelPoint {
|
|
1384
|
+
var mapPoint: MapPoint {
|
|
1385
|
+
get throws {
|
|
1386
|
+
guard latitude.isFinite, longitude.isFinite, (-90 ... 90).contains(latitude),
|
|
1387
|
+
(-180 ... 180).contains(longitude)
|
|
1388
|
+
else {
|
|
1389
|
+
throw labelError(labelInvalidArgument, "Invalid coordinate.")
|
|
1390
|
+
}
|
|
1391
|
+
return MapPoint(longitude: longitude, latitude: latitude)
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
private extension MapLabelVector {
|
|
1397
|
+
var point: CGPoint {
|
|
1398
|
+
get throws {
|
|
1399
|
+
_ = try x.finiteFloat("point.x")
|
|
1400
|
+
_ = try y.finiteFloat("point.y")
|
|
1401
|
+
return CGPoint(x: x, y: y)
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
var pixelPoint: CGPoint {
|
|
1406
|
+
get throws {
|
|
1407
|
+
try CGPoint(
|
|
1408
|
+
x: x.pixelDouble("point.x"),
|
|
1409
|
+
y: y.pixelDouble("point.y")
|
|
1410
|
+
)
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
func validateScale() throws {
|
|
1415
|
+
guard try x.finiteFloat("scale.x") >= 0, try y.finiteFloat("scale.y") >= 0 else {
|
|
1416
|
+
throw labelError(labelInvalidArgument, "Scale must be non-negative.")
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
private extension MapLabelImage {
|
|
1422
|
+
func validate() throws {
|
|
1423
|
+
guard !uri.isEmpty else {
|
|
1424
|
+
throw labelError(labelInvalidArgument, "Image URI must not be empty.")
|
|
1425
|
+
}
|
|
1426
|
+
_ = try width?.positiveSize("image.width")
|
|
1427
|
+
_ = try height?.positiveSize("image.height")
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
func resized(_ source: UIImage) throws -> UIImage {
|
|
1431
|
+
guard width != nil || height != nil else { return source }
|
|
1432
|
+
let requestedWidth = try width.map { try $0.positiveSize("image.width") }
|
|
1433
|
+
let requestedHeight = try height.map { try $0.positiveSize("image.height") }
|
|
1434
|
+
guard source.size.width > 0, source.size.height > 0 else {
|
|
1435
|
+
throw labelError(labelInvalidArgument, "The image has no dimensions: \(uri)")
|
|
1436
|
+
}
|
|
1437
|
+
let ratio = source.size.width / source.size.height
|
|
1438
|
+
let size = CGSize(
|
|
1439
|
+
width: requestedWidth ?? requestedHeight! * ratio,
|
|
1440
|
+
height: requestedHeight ?? requestedWidth! / ratio
|
|
1441
|
+
)
|
|
1442
|
+
return UIGraphicsImageRenderer(size: size, format: MapUnits.imageRendererFormat).image { _ in
|
|
1443
|
+
source.draw(in: CGRect(origin: .zero, size: size))
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
private func textLayout(_ value: String) throws -> PoiTextLayout {
|
|
1449
|
+
switch value {
|
|
1450
|
+
case "center": return .center
|
|
1451
|
+
case "top": return .top
|
|
1452
|
+
case "bottom": return .bottom
|
|
1453
|
+
case "left": return .left
|
|
1454
|
+
case "right": return .right
|
|
1455
|
+
default: throw labelError(labelInvalidArgument, "Unknown label text layout: \(value)")
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
private func transition(_ value: String) throws -> TransitionType {
|
|
1460
|
+
switch value {
|
|
1461
|
+
case "none": return .none; case "alpha": return .alpha; case "scale": return .scale; default: throw labelError(
|
|
1462
|
+
labelInvalidArgument,
|
|
1463
|
+
"Unknown transition: \(value)"
|
|
1464
|
+
)
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
private func competitionType(_ value: String) throws -> CompetitionType {
|
|
1469
|
+
switch value {
|
|
1470
|
+
case "none": return .none; case "all": return .all; case "upper": return .upper; case "upperLower": return .upperLower; case "upperSame": return .upperSame; case "same": return .same; case "sameLower": return .sameLower; case "lower": return .lower; default: throw labelError(
|
|
1471
|
+
labelInvalidArgument,
|
|
1472
|
+
"Unknown competition: \(value)"
|
|
1473
|
+
)
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
private func labelCompetitionUnit(_ value: String) throws -> CompetitionUnit {
|
|
1478
|
+
switch value {
|
|
1479
|
+
case "iconAndText": return .poi; case "iconFirst": return .symbolFirst; default: throw labelError(
|
|
1480
|
+
labelInvalidArgument,
|
|
1481
|
+
"Unknown competition unit: \(value)"
|
|
1482
|
+
)
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
private func labelOrdering(_ value: String) throws -> OrderingType {
|
|
1487
|
+
switch value {
|
|
1488
|
+
case "rank": return .rank; case "leftBottom": return .closerFromLeftBottom; default: throw labelError(
|
|
1489
|
+
labelInvalidArgument,
|
|
1490
|
+
"Unknown ordering: \(value)"
|
|
1491
|
+
)
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
private func transformType(_ value: String) throws -> PoiTransformType {
|
|
1496
|
+
switch value {
|
|
1497
|
+
case "absoluteRotation": return .absoluteRotation; case "default": return .default; case "absoluteRotationKeepUpright": return .keepUpright; case "absoluteRotationDecal": return .absoluteRotationDecal; case "decal": return .decal; case "none": throw labelError(
|
|
1498
|
+
labelUnsupported,
|
|
1499
|
+
"iOS has no 'none' POI transform."
|
|
1500
|
+
); default: throw labelError(labelInvalidArgument, "Unknown transform: \(value)")
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
private func interpolation(_ value: String, duration: Double) throws -> AnimationInterpolation {
|
|
1505
|
+
let method: InterpolationMethodType
|
|
1506
|
+
switch value {
|
|
1507
|
+
case "linear": method = .linear; case "cubicIn": method = .cubicIn; case "cubicOut": method =
|
|
1508
|
+
.cubicOut; case "cubicInOut": method = .cubicInOut; default: throw labelError(
|
|
1509
|
+
labelInvalidArgument,
|
|
1510
|
+
"Unknown interpolation: \(value)"
|
|
1511
|
+
)
|
|
1512
|
+
}
|
|
1513
|
+
return try AnimationInterpolation(duration: UInt32(duration.duration()), method: method)
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
private func unique(_ kind: String, _ values: [String]) throws {
|
|
1517
|
+
var seen = Set<String>()
|
|
1518
|
+
if let duplicate = values.first(where: { !seen.insert($0).inserted }) {
|
|
1519
|
+
throw labelError(
|
|
1520
|
+
labelInvalidArgument,
|
|
1521
|
+
"Duplicate \(kind) id: \(duplicate)"
|
|
1522
|
+
)
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
private func notFound(_ kind: String, _ id: String) -> KakaoMapException {
|
|
1527
|
+
labelError(
|
|
1528
|
+
labelNotFound,
|
|
1529
|
+
"\(kind) not found: \(id)"
|
|
1530
|
+
)
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
private func labelError(_ code: String, _ message: String) -> KakaoMapException {
|
|
1534
|
+
KakaoMapException(
|
|
1535
|
+
code,
|
|
1536
|
+
message
|
|
1537
|
+
)
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
private extension Optional {
|
|
1541
|
+
func required(_ name: String) throws -> Wrapped {
|
|
1542
|
+
guard let self
|
|
1543
|
+
else { throw labelError(
|
|
1544
|
+
labelInvalidArgument,
|
|
1545
|
+
"\(name) is required."
|
|
1546
|
+
) }; return self
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
private extension MapLabelCommandRecord {
|
|
1551
|
+
func requiredID() throws -> String {
|
|
1552
|
+
try id.required("id")
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
private extension Double {
|
|
1557
|
+
func duration() throws -> Int {
|
|
1558
|
+
guard isFinite, self >= 0, self <= Double(UInt32.max),
|
|
1559
|
+
rounded() == self
|
|
1560
|
+
else { throw labelError(
|
|
1561
|
+
labelInvalidArgument,
|
|
1562
|
+
"Duration must be a non-negative 32-bit integer."
|
|
1563
|
+
) }; return Int(self)
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
func finiteFloat(_ name: String) throws -> Float {
|
|
1567
|
+
guard isFinite,
|
|
1568
|
+
self <=
|
|
1569
|
+
Double(Float.greatestFiniteMagnitude),
|
|
1570
|
+
self >=
|
|
1571
|
+
-Double(Float.greatestFiniteMagnitude)
|
|
1572
|
+
else { throw labelError(
|
|
1573
|
+
labelInvalidArgument,
|
|
1574
|
+
"\(name) must be finite."
|
|
1575
|
+
) }; return Float(self)
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
func finiteDouble(_ name: String) throws -> Double {
|
|
1579
|
+
guard isFinite else {
|
|
1580
|
+
throw labelError(labelInvalidArgument, "\(name) must be finite.")
|
|
1581
|
+
}
|
|
1582
|
+
return self
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
func pixelDouble(_ name: String) throws -> Double {
|
|
1586
|
+
try MapUnits.pixels(finiteDouble(name)).finiteDouble(name)
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
func pixelFloat(_ name: String) throws -> Float {
|
|
1590
|
+
try pixelDouble(name).finiteFloat(name)
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
func unitFloat(_ name: String) throws -> Float {
|
|
1594
|
+
let value = try finiteFloat(name)
|
|
1595
|
+
guard (0 ... 1).contains(value) else {
|
|
1596
|
+
throw labelError(labelInvalidArgument, "\(name) must be between 0 and 1.")
|
|
1597
|
+
}
|
|
1598
|
+
return value
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
func integer(_ name: String) throws -> Int {
|
|
1602
|
+
guard isFinite, rounded() == self,
|
|
1603
|
+
self <= Double(Int.max),
|
|
1604
|
+
self >= Double(Int.min)
|
|
1605
|
+
else { throw labelError(
|
|
1606
|
+
labelInvalidArgument,
|
|
1607
|
+
"\(name) must be an integer."
|
|
1608
|
+
) }; return Int(self)
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
func pixelInteger(_ name: String) throws -> Int {
|
|
1612
|
+
_ = try finiteDouble(name)
|
|
1613
|
+
return try MapUnits.pixels(self).rounded().integer(name)
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
func nonnegativeInteger(_ name: String) throws
|
|
1617
|
+
-> Int
|
|
1618
|
+
{
|
|
1619
|
+
let value = try integer(name); guard value >= 0
|
|
1620
|
+
else { throw labelError(
|
|
1621
|
+
labelInvalidArgument,
|
|
1622
|
+
"\(name) must be non-negative."
|
|
1623
|
+
) }; return value
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
func nonnegativePixelInteger(_ name: String) throws -> Int {
|
|
1627
|
+
guard try finiteDouble(name) >= 0 else {
|
|
1628
|
+
throw labelError(labelInvalidArgument, "\(name) must be non-negative.")
|
|
1629
|
+
}
|
|
1630
|
+
return try MapUnits.pixels(self).rounded().nonnegativeInteger(name)
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
func positiveInteger(_ name: String) throws -> Int {
|
|
1634
|
+
let value = try integer(name); guard value >
|
|
1635
|
+
0
|
|
1636
|
+
else { throw labelError(
|
|
1637
|
+
labelInvalidArgument,
|
|
1638
|
+
"\(name) must be positive."
|
|
1639
|
+
) }; return value
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
func positivePixelInteger(_ name: String) throws -> Int {
|
|
1643
|
+
guard try finiteDouble(name) > 0 else {
|
|
1644
|
+
throw labelError(labelInvalidArgument, "\(name) must be positive.")
|
|
1645
|
+
}
|
|
1646
|
+
return try MapUnits.pixels(self).rounded().positiveInteger(name)
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
func positiveSize(_ name: String) throws -> CGFloat {
|
|
1650
|
+
guard isFinite, self > 0, self <= Double(CGFloat.greatestFiniteMagnitude) else {
|
|
1651
|
+
throw labelError(labelInvalidArgument, "\(name) must be positive.")
|
|
1652
|
+
}
|
|
1653
|
+
return CGFloat(self)
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
private extension UIColor {
|
|
1658
|
+
convenience init(hex: String) throws {
|
|
1659
|
+
var value = hex
|
|
1660
|
+
if value.hasPrefix("#") {
|
|
1661
|
+
value.removeFirst()
|
|
1662
|
+
}
|
|
1663
|
+
guard value.count == 6 || value.count == 8,
|
|
1664
|
+
let number = UInt64(value, radix: 16)
|
|
1665
|
+
else { throw labelError(
|
|
1666
|
+
labelInvalidArgument,
|
|
1667
|
+
"Invalid color: \(hex)"
|
|
1668
|
+
) }
|
|
1669
|
+
let hasAlpha = value.count == 8
|
|
1670
|
+
let red = CGFloat((number >> (hasAlpha ? 24 : 16)) & 0xFF) / 255
|
|
1671
|
+
let green = CGFloat((number >> (hasAlpha ? 16 : 8)) & 0xFF) / 255
|
|
1672
|
+
let blue = CGFloat((number >> (hasAlpha ? 8 : 0)) & 0xFF) / 255
|
|
1673
|
+
let alpha = hasAlpha ? CGFloat(number & 0xFF) / 255 : 1
|
|
1674
|
+
self.init(red: red, green: green, blue: blue, alpha: alpha)
|
|
1675
|
+
}
|
|
1676
|
+
}
|