@react-native-kakao/map 2.2.6 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -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
package/ios/MapGui.swift
ADDED
|
@@ -0,0 +1,1099 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import KakaoMapsSDK
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
private let mapGuiInvalidArgument = "ERR_KAKAO_MAP_GUI_INVALID_ARGUMENT"
|
|
6
|
+
private let mapGuiNotFound = "ERR_KAKAO_MAP_GUI_NOT_FOUND"
|
|
7
|
+
private let mapGuiUnsupported = "ERR_KAKAO_MAP_UNSUPPORTED_PLATFORM_FEATURE"
|
|
8
|
+
private let mapGuiApplyCancelled = "ERR_KAKAO_MAP_GUI_APPLY_CANCELLED"
|
|
9
|
+
|
|
10
|
+
struct MapGuiImageSourceRecord: Record {
|
|
11
|
+
@Field(.required) var uri = ""
|
|
12
|
+
@Field var width: Double?
|
|
13
|
+
@Field var height: Double?
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
struct MapGuiPointRecord: Record {
|
|
17
|
+
@Field(.required) var x = Double.nan
|
|
18
|
+
@Field(.required) var y = Double.nan
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
struct MapGuiSizeRecord: Record {
|
|
22
|
+
@Field(.required) var width = Double.nan
|
|
23
|
+
@Field(.required) var height = Double.nan
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
struct MapGuiStrokeRecord: Record {
|
|
27
|
+
@Field(.required) var color = ""
|
|
28
|
+
@Field(.required) var width = Double.nan
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
struct MapGuiSplitLineRecord: Record {
|
|
32
|
+
@Field(.required) var color = ""
|
|
33
|
+
@Field(.required) var width = Double.nan
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
struct MapGuiIosLayoutRecord: Record {
|
|
37
|
+
@Field var backgroundColor: String?
|
|
38
|
+
@Field var splitLine: MapGuiSplitLineRecord?
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
struct MapGuiAndroidLayoutRecord: Record {
|
|
42
|
+
@Field var background: MapGuiNodeRecord?
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
struct MapGuiPaddingRecord: Record {
|
|
46
|
+
@Field(.required) var top = Double.nan
|
|
47
|
+
@Field(.required) var right = Double.nan
|
|
48
|
+
@Field(.required) var bottom = Double.nan
|
|
49
|
+
@Field(.required) var left = Double.nan
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
struct MapGuiCoordinateRecord: Record {
|
|
53
|
+
@Field(.required) var latitude = Double.nan
|
|
54
|
+
@Field(.required) var longitude = Double.nan
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
final class MapGuiNodeRecord: Record {
|
|
58
|
+
@Field(.required) var id = ""
|
|
59
|
+
@Field(.required) var type = ""
|
|
60
|
+
@Field var platform: String?
|
|
61
|
+
@Field var clickable = false
|
|
62
|
+
@Field var padding: MapGuiPaddingRecord?
|
|
63
|
+
@Field var origin: String?
|
|
64
|
+
@Field var align: String?
|
|
65
|
+
@Field var orientation: String?
|
|
66
|
+
@Field var children: [MapGuiNodeRecord] = []
|
|
67
|
+
@Field var child: MapGuiNodeRecord?
|
|
68
|
+
@Field var source: MapGuiImageSourceRecord?
|
|
69
|
+
@Field var pressedSource: MapGuiImageSourceRecord?
|
|
70
|
+
@Field var sources: [MapGuiImageSourceRecord] = []
|
|
71
|
+
@Field var size: MapGuiSizeRecord?
|
|
72
|
+
@Field var pressedSize: MapGuiSizeRecord?
|
|
73
|
+
@Field var stretch: MapGuiPaddingRecord?
|
|
74
|
+
@Field var text: String?
|
|
75
|
+
@Field var textSize: Double?
|
|
76
|
+
@Field var color: String?
|
|
77
|
+
@Field var stroke: MapGuiStrokeRecord?
|
|
78
|
+
@Field var duration: Double?
|
|
79
|
+
@Field var `repeat`: Double?
|
|
80
|
+
@Field var autoStart = false
|
|
81
|
+
@Field var ios: MapGuiIosLayoutRecord?
|
|
82
|
+
@Field var android: MapGuiAndroidLayoutRecord?
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
struct MapGuiAutoMoveRecord: Record {
|
|
86
|
+
@Field(.required) var enabled = false
|
|
87
|
+
@Field var duration: Double?
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
struct MapGuiInfoWindowRecord: Record {
|
|
91
|
+
@Field(.required) var id = ""
|
|
92
|
+
@Field(.required) var position = MapGuiCoordinateRecord()
|
|
93
|
+
@Field(.required) var body = MapGuiNodeRecord()
|
|
94
|
+
@Field var tail: MapGuiNodeRecord?
|
|
95
|
+
@Field var bodyOffset: MapGuiPointRecord?
|
|
96
|
+
@Field var tailOffset: MapGuiPointRecord?
|
|
97
|
+
@Field var visible = true
|
|
98
|
+
@Field var zIndex = 0
|
|
99
|
+
@Field var autoMove: MapGuiAutoMoveRecord?
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
struct MapGuiWidgetPositionRecord: Record {
|
|
103
|
+
@Field(.required) var gravity = "topLeft"
|
|
104
|
+
@Field(.required) var x = Double.nan
|
|
105
|
+
@Field(.required) var y = Double.nan
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
struct MapGuiWidgetRecord: Record {
|
|
109
|
+
@Field(.required) var id = ""
|
|
110
|
+
@Field(.required) var position = MapGuiWidgetPositionRecord()
|
|
111
|
+
@Field(.required) var root = MapGuiNodeRecord()
|
|
112
|
+
@Field var visible = true
|
|
113
|
+
@Field var zIndex = 0
|
|
114
|
+
@Field var rotation = 0.0
|
|
115
|
+
@Field var ios: MapGuiIosLayoutRecord?
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
struct MapGuiDropAnimationRecord: Record {
|
|
119
|
+
@Field(.required) var id = ""
|
|
120
|
+
@Field(.required) var platform = "ios"
|
|
121
|
+
@Field(.required) var infoWindowIds: [String] = []
|
|
122
|
+
@Field(.required) var duration = Double.nan
|
|
123
|
+
@Field(.required) var pixelHeight = Double.nan
|
|
124
|
+
@Field var interpolation = "linear"
|
|
125
|
+
@Field var `repeat` = 1.0
|
|
126
|
+
@Field var hideAtStop = false
|
|
127
|
+
@Field var removeAtStop = false
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
struct MapGuiSceneRecord: Record {
|
|
131
|
+
@Field var infoWindows: [MapGuiInfoWindowRecord] = []
|
|
132
|
+
@Field var widgets: [MapGuiWidgetRecord] = []
|
|
133
|
+
@Field var animations: [MapGuiDropAnimationRecord] = []
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
final class MapGui {
|
|
137
|
+
typealias EventEmitter = ([String: Any]) -> Void
|
|
138
|
+
|
|
139
|
+
private static let transparentInfoWindowImage = UIGraphicsImageRenderer(
|
|
140
|
+
size: CGSize(width: 1, height: 1),
|
|
141
|
+
format: MapUnits.imageRendererFormat
|
|
142
|
+
).image { _ in }
|
|
143
|
+
|
|
144
|
+
private weak var map: KakaoMap?
|
|
145
|
+
private var imageLoader: MapImageLoader?
|
|
146
|
+
private let emit: EventEmitter
|
|
147
|
+
private var infoWindows: [String: InfoWindow] = [:]
|
|
148
|
+
private var widgets: [String: SpriteGui] = [:]
|
|
149
|
+
private var animations: [String: InfoWindowAnimator] = [:]
|
|
150
|
+
private var eventHandlers: [DisposableEventHandler] = []
|
|
151
|
+
private var imageTasks: [MapImageLoadToken] = []
|
|
152
|
+
private var applyGeneration = 0
|
|
153
|
+
private var pendingApply: ((Error?) -> Void)?
|
|
154
|
+
private var isDisposed = false
|
|
155
|
+
|
|
156
|
+
init(map: KakaoMap, imageLoader: MapImageLoader, emit: @escaping EventEmitter) {
|
|
157
|
+
self.map = map
|
|
158
|
+
self.imageLoader = imageLoader
|
|
159
|
+
self.emit = emit
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func apply(_ scene: MapGuiSceneRecord, completion: @escaping (Error?) -> Void) {
|
|
163
|
+
guard !isDisposed else {
|
|
164
|
+
completion(KakaoMapException(mapGuiNotFound, "The GUI manager was disposed."))
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
do {
|
|
168
|
+
try validate(scene)
|
|
169
|
+
} catch {
|
|
170
|
+
completion(error)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
applyGeneration += 1
|
|
175
|
+
pendingApply?(KakaoMapException(
|
|
176
|
+
mapGuiApplyCancelled,
|
|
177
|
+
"A newer GUI scene replaced this apply request."
|
|
178
|
+
))
|
|
179
|
+
pendingApply = completion
|
|
180
|
+
let generation = applyGeneration
|
|
181
|
+
imageTasks.forEach { $0.cancel() }
|
|
182
|
+
imageTasks.removeAll()
|
|
183
|
+
loadImages(scene, generation: generation) { [weak self] result in
|
|
184
|
+
guard let self, !self.isDisposed, generation == applyGeneration else { return }
|
|
185
|
+
switch result {
|
|
186
|
+
case let .success(images):
|
|
187
|
+
do {
|
|
188
|
+
try replaceScene(scene, images: images)
|
|
189
|
+
pendingApply = nil
|
|
190
|
+
completion(nil)
|
|
191
|
+
} catch {
|
|
192
|
+
clearScene()
|
|
193
|
+
pendingApply = nil
|
|
194
|
+
completion(error)
|
|
195
|
+
}
|
|
196
|
+
case let .failure(error):
|
|
197
|
+
pendingApply = nil
|
|
198
|
+
completion(error)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func showInfoWindow(id: String, autoMove: Bool, duration: Double?) throws {
|
|
204
|
+
if let duration, duration != 0 {
|
|
205
|
+
throw KakaoMapException(
|
|
206
|
+
mapGuiUnsupported,
|
|
207
|
+
"iOS SDK 2.12.19 does not expose an auto-move duration for info windows."
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
let infoWindow = try requireInfoWindow(id)
|
|
211
|
+
if autoMove {
|
|
212
|
+
infoWindow.showWithAutoMove()
|
|
213
|
+
} else {
|
|
214
|
+
infoWindow.show()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func hideInfoWindow(id: String) throws {
|
|
219
|
+
try requireInfoWindow(id).hide()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
func moveInfoWindow(id: String, position: MapGuiCoordinateRecord, duration: Double) throws {
|
|
223
|
+
try validateDuration(duration)
|
|
224
|
+
try requireInfoWindow(id).moveAt(mapPoint(position), duration: UInt(duration))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func showWidget(id: String) throws {
|
|
228
|
+
try requireWidget(id).show()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
func hideWidget(id: String) throws {
|
|
232
|
+
try requireWidget(id).hide()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
func positionWidget(id: String, position: MapGuiWidgetPositionRecord) throws {
|
|
236
|
+
try validatePoint(position.x, position.y, name: "widget position")
|
|
237
|
+
let widget = try requireWidget(id)
|
|
238
|
+
widget.origin = try alignment(position.gravity)
|
|
239
|
+
widget.position = try CGPoint(
|
|
240
|
+
x: pixelDouble(position.x, name: "widget position x"),
|
|
241
|
+
y: pixelDouble(position.y, name: "widget position y")
|
|
242
|
+
)
|
|
243
|
+
widget.updateGui()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func rotateWidget(id: String, rotation: Double) throws {
|
|
247
|
+
guard rotation.isFinite else { throw exception("Rotation must be finite.") }
|
|
248
|
+
let widget = try requireWidget(id)
|
|
249
|
+
widget.orientation = rotation * .pi / 180
|
|
250
|
+
widget.updateGui()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
func startAnimation(id: String) throws {
|
|
254
|
+
try requireAnimation(id).start()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
func stopAnimation(id: String) throws {
|
|
258
|
+
try requireAnimation(id).stop()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
func controlAnimatedImage(
|
|
262
|
+
ownerType: String,
|
|
263
|
+
ownerId: String,
|
|
264
|
+
componentId: String,
|
|
265
|
+
action: String
|
|
266
|
+
) throws {
|
|
267
|
+
let owner: GuiBase
|
|
268
|
+
switch ownerType {
|
|
269
|
+
case "infoWindow": owner = try requireInfoWindow(ownerId)
|
|
270
|
+
case "widget": owner = try requireWidget(ownerId)
|
|
271
|
+
default: throw exception("Unknown GUI owner type: \(ownerType).")
|
|
272
|
+
}
|
|
273
|
+
guard let image = owner.getChild(componentId) as? GuiAnimatedImage else {
|
|
274
|
+
throw KakaoMapException(mapGuiNotFound, "Animated image \(componentId) was not found.")
|
|
275
|
+
}
|
|
276
|
+
switch action {
|
|
277
|
+
case "start": image.start()
|
|
278
|
+
case "pause": image.pause()
|
|
279
|
+
case "resume": image.resume()
|
|
280
|
+
case "stop": image.stop()
|
|
281
|
+
default: throw exception("Unknown animated-image action: \(action).")
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
func status() throws -> [String: Any] {
|
|
286
|
+
guard !isDisposed, let manager = map?.getGuiManager() else {
|
|
287
|
+
throw KakaoMapException(mapGuiNotFound, "The GUI manager was disposed.")
|
|
288
|
+
}
|
|
289
|
+
let infoWindowStatus = (manager.infoWindowLayer.getAllInfoWindows() ?? [])
|
|
290
|
+
.sorted { $0.name < $1.name }.compactMap { infoWindow -> [String: Any]? in
|
|
291
|
+
guard let coordinate = infoWindow.rawPosition()?.wgsCoord else { return nil }
|
|
292
|
+
return [
|
|
293
|
+
"id": infoWindow.name,
|
|
294
|
+
"visible": infoWindow.isShow,
|
|
295
|
+
"position": [
|
|
296
|
+
"latitude": coordinate.latitude,
|
|
297
|
+
"longitude": coordinate.longitude
|
|
298
|
+
],
|
|
299
|
+
"zIndex": infoWindow.zOrder
|
|
300
|
+
] as [String: Any]
|
|
301
|
+
}
|
|
302
|
+
let widgetStatus = (manager.spriteGuiLayer.getAllSpriteGuis() ?? [])
|
|
303
|
+
.sorted { $0.name < $1.name }.map { widget in
|
|
304
|
+
[
|
|
305
|
+
"id": widget.name,
|
|
306
|
+
"visible": widget.isShow,
|
|
307
|
+
"position": [
|
|
308
|
+
"gravity": alignmentName(widget.origin),
|
|
309
|
+
"x": widget.position.x / MapUnits.pixelsPerPoint,
|
|
310
|
+
"y": widget.position.y / MapUnits.pixelsPerPoint
|
|
311
|
+
],
|
|
312
|
+
"rotation": widget.orientation * 180 / .pi,
|
|
313
|
+
"zIndex": widget.zOrder
|
|
314
|
+
] as [String: Any]
|
|
315
|
+
}
|
|
316
|
+
let animationStatus = animations.keys.sorted().compactMap { id -> [String: Any]? in
|
|
317
|
+
guard let animation = manager.getInfoWindowAnimator(animatorID: id) else { return nil }
|
|
318
|
+
return ["id": animation.animatorID, "running": animation.isStart]
|
|
319
|
+
}
|
|
320
|
+
return [
|
|
321
|
+
"infoWindows": infoWindowStatus,
|
|
322
|
+
"widgets": widgetStatus,
|
|
323
|
+
"animations": animationStatus,
|
|
324
|
+
"counts": [
|
|
325
|
+
"infoWindows": infoWindowStatus.count,
|
|
326
|
+
"widgets": widgetStatus.count,
|
|
327
|
+
"animations": animationStatus.count
|
|
328
|
+
],
|
|
329
|
+
"capabilities": [
|
|
330
|
+
"button": true,
|
|
331
|
+
"animatedImage": true,
|
|
332
|
+
"infoWindowAnimation": true,
|
|
333
|
+
"arbitraryComponentClicks": false,
|
|
334
|
+
"componentAnimationEvents": true,
|
|
335
|
+
"guiMoveEvents": true,
|
|
336
|
+
"autoMoveDuration": false
|
|
337
|
+
]
|
|
338
|
+
]
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
func dispose() {
|
|
342
|
+
guard !isDisposed else { return }
|
|
343
|
+
isDisposed = true
|
|
344
|
+
applyGeneration += 1
|
|
345
|
+
pendingApply?(KakaoMapException(mapGuiApplyCancelled, "The GUI manager was disposed."))
|
|
346
|
+
pendingApply = nil
|
|
347
|
+
imageTasks.forEach { $0.cancel() }
|
|
348
|
+
imageTasks.removeAll()
|
|
349
|
+
clearScene()
|
|
350
|
+
map = nil
|
|
351
|
+
imageLoader = nil
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private func replaceScene(_ scene: MapGuiSceneRecord, images: [String: UIImage]) throws {
|
|
355
|
+
guard let map
|
|
356
|
+
else { throw KakaoMapException(mapGuiNotFound, "The map is no longer available.") }
|
|
357
|
+
clearScene()
|
|
358
|
+
let manager = map.getGuiManager()
|
|
359
|
+
for record in scene.infoWindows {
|
|
360
|
+
let infoWindow = InfoWindow(record.id)
|
|
361
|
+
infoWindow.body = try bodyImage(record.body, images: images, ownerId: record.id)
|
|
362
|
+
if let tail = record.tail {
|
|
363
|
+
infoWindow.tail = try image(tail, images: images, ownerId: record.id)
|
|
364
|
+
}
|
|
365
|
+
infoWindow.position = try mapPoint(record.position)
|
|
366
|
+
infoWindow.bodyOffset = try pixelPoint(record.bodyOffset)
|
|
367
|
+
infoWindow.positionOffset = try pixelPoint(record.tailOffset)
|
|
368
|
+
infoWindow.zOrder = record.zIndex
|
|
369
|
+
installHandlers(infoWindow, ownerType: "infoWindow", ownerId: record.id)
|
|
370
|
+
manager.infoWindowLayer.addInfoWindow(infoWindow)
|
|
371
|
+
if record.visible {
|
|
372
|
+
record.autoMove?.enabled == true ? infoWindow.showWithAutoMove() : infoWindow.show()
|
|
373
|
+
} else {
|
|
374
|
+
infoWindow.hide()
|
|
375
|
+
}
|
|
376
|
+
infoWindows[record.id] = infoWindow
|
|
377
|
+
startAutoImages(record.body, owner: infoWindow)
|
|
378
|
+
}
|
|
379
|
+
for record in scene.widgets {
|
|
380
|
+
let widget = SpriteGui(record.id)
|
|
381
|
+
try widget.addChild(component(record.root, images: images, ownerId: record.id))
|
|
382
|
+
widget.origin = try alignment(record.position.gravity)
|
|
383
|
+
widget.position = try CGPoint(
|
|
384
|
+
x: pixelDouble(record.position.x, name: "widget position x"),
|
|
385
|
+
y: pixelDouble(record.position.y, name: "widget position y")
|
|
386
|
+
)
|
|
387
|
+
widget.orientation = record.rotation * .pi / 180
|
|
388
|
+
widget.zOrder = record.zIndex
|
|
389
|
+
if let backgroundColor = record.ios?.backgroundColor {
|
|
390
|
+
widget.bgColor = try color(backgroundColor)
|
|
391
|
+
}
|
|
392
|
+
if let splitLine = record.ios?.splitLine {
|
|
393
|
+
widget.showSplitLine = true
|
|
394
|
+
widget.splitLineColor = try color(splitLine.color)
|
|
395
|
+
widget.splitLineWidth = try nonnegativePixelInt(splitLine.width, name: "split-line width")
|
|
396
|
+
}
|
|
397
|
+
installHandlers(widget, ownerType: "widget", ownerId: record.id)
|
|
398
|
+
manager.spriteGuiLayer.addSpriteGui(widget)
|
|
399
|
+
record.visible ? widget.show() : widget.hide()
|
|
400
|
+
widgets[record.id] = widget
|
|
401
|
+
startAutoImages(record.root, owner: widget)
|
|
402
|
+
}
|
|
403
|
+
for record in scene.animations {
|
|
404
|
+
let effect = try DropAnimationEffect(pixelHeight: pixelFloat(
|
|
405
|
+
record.pixelHeight,
|
|
406
|
+
name: "drop height"
|
|
407
|
+
))
|
|
408
|
+
effect.interpolation = try interpolation(record.interpolation, duration: record.duration)
|
|
409
|
+
effect.playCount = UInt(record.repeat)
|
|
410
|
+
effect.hideAtStop = record.hideAtStop
|
|
411
|
+
effect.removeAtStop = record.removeAtStop
|
|
412
|
+
guard let animator = manager.addInfoWindowAnimator(animatorID: record.id, effect: effect)
|
|
413
|
+
else {
|
|
414
|
+
throw exception("Could not create info-window animator \(record.id).")
|
|
415
|
+
}
|
|
416
|
+
try animator.addInfoWindows(record.infoWindowIds.map(requireInfoWindow))
|
|
417
|
+
let animatorIdentity = ObjectIdentifier(animator)
|
|
418
|
+
animator.setStopCallback { [weak self] _ in
|
|
419
|
+
guard let self, !self.isDisposed,
|
|
420
|
+
let active = animations[record.id],
|
|
421
|
+
ObjectIdentifier(active) == animatorIdentity
|
|
422
|
+
else { return }
|
|
423
|
+
emit(["type": "animatorStopped", "id": record.id])
|
|
424
|
+
}
|
|
425
|
+
animations[record.id] = animator
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private func clearScene() {
|
|
430
|
+
eventHandlers.forEach { $0.dispose() }
|
|
431
|
+
eventHandlers.removeAll()
|
|
432
|
+
let removedAnimations = Array(animations.values)
|
|
433
|
+
animations.removeAll()
|
|
434
|
+
removedAnimations.forEach { $0.stop() }
|
|
435
|
+
map?.getGuiManager().clearAllInfoWindowAnimators()
|
|
436
|
+
map?.getGuiManager().infoWindowLayer.clear()
|
|
437
|
+
map?.getGuiManager().spriteGuiLayer.clear()
|
|
438
|
+
infoWindows.removeAll()
|
|
439
|
+
widgets.removeAll()
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private func installHandlers(_ gui: GuiBase, ownerType: String, ownerId: String) {
|
|
443
|
+
eventHandlers.append(gui.addTapEventHandler(target: self) { owner in { event in
|
|
444
|
+
owner.emit([
|
|
445
|
+
"type": "tap",
|
|
446
|
+
"ownerType": ownerType,
|
|
447
|
+
"ownerId": ownerId,
|
|
448
|
+
"componentId": event.guiComponentName ?? ""
|
|
449
|
+
])
|
|
450
|
+
} })
|
|
451
|
+
eventHandlers.append(gui.addAnimationEventHandler(target: self) { owner in { event in
|
|
452
|
+
owner.emit([
|
|
453
|
+
"type": "componentAnimationState",
|
|
454
|
+
"ownerType": ownerType,
|
|
455
|
+
"ownerId": ownerId,
|
|
456
|
+
"componentId": event.guiComponentName ?? "",
|
|
457
|
+
"state": owner.animationStateName(event.state)
|
|
458
|
+
])
|
|
459
|
+
} })
|
|
460
|
+
eventHandlers.append(gui.addMoveEndEventHandler(target: self) { owner in { event in
|
|
461
|
+
let coordinate = event.position.wgsCoord
|
|
462
|
+
owner.emit([
|
|
463
|
+
"type": "guiMoveEnd",
|
|
464
|
+
"ownerType": ownerType,
|
|
465
|
+
"ownerId": ownerId,
|
|
466
|
+
"position": ["latitude": coordinate.latitude, "longitude": coordinate.longitude]
|
|
467
|
+
])
|
|
468
|
+
} })
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private func bodyImage(
|
|
472
|
+
_ record: MapGuiNodeRecord,
|
|
473
|
+
images: [String: UIImage],
|
|
474
|
+
ownerId: String
|
|
475
|
+
) throws -> GuiImage {
|
|
476
|
+
let child = try component(record, images: images, ownerId: ownerId)
|
|
477
|
+
if let image = child as? GuiImage {
|
|
478
|
+
return image
|
|
479
|
+
}
|
|
480
|
+
let wrapper = GuiImage("\(record.id)-body")
|
|
481
|
+
wrapper.image = Self.transparentInfoWindowImage
|
|
482
|
+
wrapper.child = child
|
|
483
|
+
return wrapper
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private func component(
|
|
487
|
+
_ record: MapGuiNodeRecord,
|
|
488
|
+
images: [String: UIImage],
|
|
489
|
+
ownerId: String
|
|
490
|
+
) throws -> GuiComponentBase {
|
|
491
|
+
let result: GuiComponentBase
|
|
492
|
+
switch record.type {
|
|
493
|
+
case "layout":
|
|
494
|
+
let layout = GuiLayout(
|
|
495
|
+
record.id,
|
|
496
|
+
arrangement: record.orientation == "horizontal" ? .horizontal : .vertical
|
|
497
|
+
)
|
|
498
|
+
for child in record.children {
|
|
499
|
+
try layout.addChild(component(child, images: images, ownerId: ownerId))
|
|
500
|
+
}
|
|
501
|
+
if let backgroundColor = record.ios?.backgroundColor {
|
|
502
|
+
layout.bgColor = try color(backgroundColor)
|
|
503
|
+
}
|
|
504
|
+
if let splitLine = record.ios?.splitLine {
|
|
505
|
+
layout.showSplitLine = true
|
|
506
|
+
layout.splitLineColor = try color(splitLine.color)
|
|
507
|
+
layout.splitLineWidth = try nonnegativePixelInt(splitLine.width, name: "split-line width")
|
|
508
|
+
}
|
|
509
|
+
result = layout
|
|
510
|
+
case "image":
|
|
511
|
+
result = try image(record, images: images, ownerId: ownerId)
|
|
512
|
+
case "text":
|
|
513
|
+
guard let textValue = record.text else { throw exception("GUI text is required.") }
|
|
514
|
+
let text = GuiText(record.id)
|
|
515
|
+
try text.addText(
|
|
516
|
+
text: textValue,
|
|
517
|
+
style: TextStyle(
|
|
518
|
+
fontSize: UInt(positivePixelInt(record.textSize ?? 0, name: "text size")),
|
|
519
|
+
fontColor: color(record.color ?? ""),
|
|
520
|
+
strokeThickness: UInt(nonnegativePixelInt(
|
|
521
|
+
record.stroke?.width ?? 0,
|
|
522
|
+
name: "stroke width"
|
|
523
|
+
)),
|
|
524
|
+
strokeColor: color(record.stroke?.color ?? "#00000000")
|
|
525
|
+
)
|
|
526
|
+
)
|
|
527
|
+
result = text
|
|
528
|
+
case "button":
|
|
529
|
+
guard record.platform == "ios"
|
|
530
|
+
else { throw exception("GUI buttons must specify platform: ios.") }
|
|
531
|
+
let button = GuiButton(record.id)
|
|
532
|
+
let normalImage = try preparedImage(
|
|
533
|
+
requiredImage(record.source, images: images),
|
|
534
|
+
explicitSize: record.size,
|
|
535
|
+
source: record.source
|
|
536
|
+
)
|
|
537
|
+
let pressedImage = try preparedImage(
|
|
538
|
+
requiredImage(record.pressedSource, images: images),
|
|
539
|
+
explicitSize: record.pressedSize,
|
|
540
|
+
source: record.pressedSource
|
|
541
|
+
)
|
|
542
|
+
button.image = normalImage.image
|
|
543
|
+
button.pressedImage = pressedImage.image
|
|
544
|
+
if let size = normalImage.size {
|
|
545
|
+
button.imageSize = size
|
|
546
|
+
}
|
|
547
|
+
if let size = pressedImage.size {
|
|
548
|
+
button.pressedImageSize = size
|
|
549
|
+
}
|
|
550
|
+
if let child = record.child {
|
|
551
|
+
button.child = try component(child, images: images, ownerId: ownerId)
|
|
552
|
+
}
|
|
553
|
+
result = button
|
|
554
|
+
case "animatedImage":
|
|
555
|
+
guard record.platform == "ios" else {
|
|
556
|
+
throw exception("Animated GUI images must specify platform: ios.")
|
|
557
|
+
}
|
|
558
|
+
let animated = GuiAnimatedImage(record.id)
|
|
559
|
+
let nativeImages = try record.sources.map { source in
|
|
560
|
+
try preparedImage(
|
|
561
|
+
requiredImage(source, images: images),
|
|
562
|
+
explicitSize: record.size,
|
|
563
|
+
source: source
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
animated.addImages(nativeImages.map(\.image))
|
|
567
|
+
animated.duration = try UInt(positiveInt(
|
|
568
|
+
record.duration ?? 0,
|
|
569
|
+
name: "animated-image duration"
|
|
570
|
+
))
|
|
571
|
+
animated.playCount = try UInt(nonnegativeInt(
|
|
572
|
+
record.repeat ?? 1,
|
|
573
|
+
name: "animated-image repeat"
|
|
574
|
+
))
|
|
575
|
+
if let size = nativeImages.first?.size {
|
|
576
|
+
animated.imageSize = size
|
|
577
|
+
}
|
|
578
|
+
if let child = record.child {
|
|
579
|
+
animated.child = try component(child, images: images, ownerId: ownerId)
|
|
580
|
+
}
|
|
581
|
+
result = animated
|
|
582
|
+
default:
|
|
583
|
+
throw exception("Unknown GUI component type: \(record.type).")
|
|
584
|
+
}
|
|
585
|
+
result.padding = try padding(record.padding)
|
|
586
|
+
result.origin = try alignment(record.origin ?? "topLeft")
|
|
587
|
+
result.align = try alignment(record.align ?? "topLeft")
|
|
588
|
+
return result
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private func image(
|
|
592
|
+
_ record: MapGuiNodeRecord,
|
|
593
|
+
images: [String: UIImage],
|
|
594
|
+
ownerId: String
|
|
595
|
+
) throws -> GuiImage {
|
|
596
|
+
let image = GuiImage(record.id)
|
|
597
|
+
let nativeImage = try preparedImage(
|
|
598
|
+
requiredImage(record.source, images: images),
|
|
599
|
+
explicitSize: record.size,
|
|
600
|
+
source: record.source
|
|
601
|
+
)
|
|
602
|
+
image.image = nativeImage.image
|
|
603
|
+
if let size = nativeImage.size {
|
|
604
|
+
image.imageSize = size
|
|
605
|
+
}
|
|
606
|
+
if let stretch = record.stretch {
|
|
607
|
+
image.imageStretch = try GuiEdgeInsets(
|
|
608
|
+
top: UInt(nonnegativePixelInt(stretch.top, name: "image stretch")),
|
|
609
|
+
left: UInt(nonnegativePixelInt(stretch.left, name: "image stretch")),
|
|
610
|
+
bottom: UInt(nonnegativePixelInt(stretch.bottom, name: "image stretch")),
|
|
611
|
+
right: UInt(nonnegativePixelInt(stretch.right, name: "image stretch"))
|
|
612
|
+
)
|
|
613
|
+
}
|
|
614
|
+
if let child = record.child {
|
|
615
|
+
image.child = try component(child, images: images, ownerId: ownerId)
|
|
616
|
+
}
|
|
617
|
+
image.padding = try padding(record.padding)
|
|
618
|
+
image.origin = try alignment(record.origin ?? "topLeft")
|
|
619
|
+
image.align = try alignment(record.align ?? "topLeft")
|
|
620
|
+
return image
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private func loadImages(
|
|
624
|
+
_ scene: MapGuiSceneRecord,
|
|
625
|
+
generation: Int,
|
|
626
|
+
completion: @escaping (Result<[String: UIImage], Error>) -> Void
|
|
627
|
+
) {
|
|
628
|
+
var sources: [String: MapGuiImageSourceRecord] = [:]
|
|
629
|
+
for infoWindow in scene.infoWindows {
|
|
630
|
+
collectImages(infoWindow.body, into: &sources)
|
|
631
|
+
if let tail = infoWindow.tail {
|
|
632
|
+
collectImages(tail, into: &sources)
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
scene.widgets.forEach { collectImages($0.root, into: &sources) }
|
|
636
|
+
guard !sources.isEmpty else {
|
|
637
|
+
completion(.success([:]))
|
|
638
|
+
return
|
|
639
|
+
}
|
|
640
|
+
guard let imageLoader else {
|
|
641
|
+
completion(.failure(exception("The map image loader is unavailable.")))
|
|
642
|
+
return
|
|
643
|
+
}
|
|
644
|
+
var loaded: [String: UIImage] = [:]
|
|
645
|
+
var remaining = sources.count
|
|
646
|
+
var firstError: Error?
|
|
647
|
+
for source in sources.values {
|
|
648
|
+
let task = imageLoader.load(source.uri) { [weak self] result in
|
|
649
|
+
guard let self, !self.isDisposed, generation == applyGeneration else { return }
|
|
650
|
+
if case let .success(image) = result {
|
|
651
|
+
loaded[source.uri] = image
|
|
652
|
+
}
|
|
653
|
+
if case let .failure(error) = result, firstError == nil {
|
|
654
|
+
firstError = error
|
|
655
|
+
}
|
|
656
|
+
remaining -= 1
|
|
657
|
+
if remaining == 0 {
|
|
658
|
+
imageTasks.removeAll()
|
|
659
|
+
if let firstError {
|
|
660
|
+
completion(.failure(firstError))
|
|
661
|
+
} else {
|
|
662
|
+
completion(.success(loaded))
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
imageTasks.append(task)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
private func collectImages(
|
|
671
|
+
_ node: MapGuiNodeRecord,
|
|
672
|
+
into sources: inout [String: MapGuiImageSourceRecord]
|
|
673
|
+
) {
|
|
674
|
+
[node.source, node.pressedSource].compactMap { $0 }.forEach { sources[$0.uri] = $0 }
|
|
675
|
+
node.sources.forEach { sources[$0.uri] = $0 }
|
|
676
|
+
node.children.forEach { collectImages($0, into: &sources) }
|
|
677
|
+
if let child = node.child {
|
|
678
|
+
collectImages(child, into: &sources)
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private func validate(_ scene: MapGuiSceneRecord) throws {
|
|
683
|
+
try unique(scene.infoWindows.map(\.id), name: "info-window")
|
|
684
|
+
try unique(scene.widgets.map(\.id), name: "widget")
|
|
685
|
+
try unique(scene.animations.map(\.id), name: "GUI animation")
|
|
686
|
+
let infoWindowIds = Set(scene.infoWindows.map(\.id))
|
|
687
|
+
for infoWindow in scene.infoWindows {
|
|
688
|
+
try nonempty(infoWindow.id, name: "info-window ID")
|
|
689
|
+
_ = try mapPoint(infoWindow.position)
|
|
690
|
+
_ = try pixelPoint(infoWindow.bodyOffset)
|
|
691
|
+
_ = try pixelPoint(infoWindow.tailOffset)
|
|
692
|
+
var componentIds = Set<String>()
|
|
693
|
+
try validate(infoWindow.body, ids: &componentIds)
|
|
694
|
+
if let tail = infoWindow.tail {
|
|
695
|
+
guard tail.type == "image" else { throw exception("Info-window tails must be images.") }
|
|
696
|
+
try validate(tail, ids: &componentIds)
|
|
697
|
+
}
|
|
698
|
+
if let duration = infoWindow.autoMove?.duration, duration != 0 {
|
|
699
|
+
throw KakaoMapException(
|
|
700
|
+
mapGuiUnsupported,
|
|
701
|
+
"iOS does not expose an auto-move duration for info windows."
|
|
702
|
+
)
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
for widget in scene.widgets {
|
|
706
|
+
try nonempty(widget.id, name: "widget ID")
|
|
707
|
+
guard widget.root.type == "layout" else { throw exception("Widget roots must be layouts.") }
|
|
708
|
+
try validatePoint(widget.position.x, widget.position.y, name: "widget position")
|
|
709
|
+
_ = try alignment(widget.position.gravity)
|
|
710
|
+
guard widget.rotation.isFinite else { throw exception("Widget rotation must be finite.") }
|
|
711
|
+
try validateIosLayout(widget.ios)
|
|
712
|
+
var componentIds = Set<String>()
|
|
713
|
+
try validate(widget.root, ids: &componentIds)
|
|
714
|
+
}
|
|
715
|
+
for animation in scene.animations {
|
|
716
|
+
guard animation.platform == "ios"
|
|
717
|
+
else { throw exception("Info-window animations require platform: ios.") }
|
|
718
|
+
try nonempty(animation.id, name: "GUI animation ID")
|
|
719
|
+
_ = try pixelFloat(animation.pixelHeight, name: "drop height")
|
|
720
|
+
_ = try nonnegativeInt(animation.repeat, name: "animation repeat")
|
|
721
|
+
_ = try positiveInt(animation.duration, name: "animation duration")
|
|
722
|
+
guard !animation.infoWindowIds.isEmpty else {
|
|
723
|
+
throw exception("Animation \(animation.id) requires an info window.")
|
|
724
|
+
}
|
|
725
|
+
try unique(animation.infoWindowIds, name: "animation target")
|
|
726
|
+
_ = try interpolation(animation.interpolation, duration: animation.duration)
|
|
727
|
+
for id in animation.infoWindowIds where !infoWindowIds.contains(id) {
|
|
728
|
+
throw exception("Animation \(animation.id) references unknown info window \(id).")
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private func validate(_ node: MapGuiNodeRecord, ids: inout Set<String>) throws {
|
|
734
|
+
try nonempty(node.id, name: "GUI component ID")
|
|
735
|
+
guard ids.insert(node.id).inserted
|
|
736
|
+
else { throw exception("Duplicate GUI component ID: \(node.id).") }
|
|
737
|
+
if let padding = node.padding {
|
|
738
|
+
try [padding.top, padding.right, padding.bottom, padding.left].forEach {
|
|
739
|
+
_ = try nonnegativePixelInt($0, name: "GUI padding")
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if let size = node.size {
|
|
743
|
+
_ = try guiSize(size)
|
|
744
|
+
}
|
|
745
|
+
if let size = node.pressedSize {
|
|
746
|
+
_ = try guiSize(size)
|
|
747
|
+
}
|
|
748
|
+
if let stretch = node.stretch {
|
|
749
|
+
try [stretch.top, stretch.right, stretch.bottom, stretch.left].forEach {
|
|
750
|
+
_ = try nonnegativePixelInt($0, name: "image stretch")
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
_ = try alignment(node.origin ?? "topLeft")
|
|
754
|
+
_ = try alignment(node.align ?? "topLeft")
|
|
755
|
+
if node.clickable, node.type != "button" {
|
|
756
|
+
throw KakaoMapException(
|
|
757
|
+
mapGuiUnsupported,
|
|
758
|
+
"iOS SDK 2.12.19 only emits component taps from button nodes."
|
|
759
|
+
)
|
|
760
|
+
}
|
|
761
|
+
switch node.type {
|
|
762
|
+
case "layout":
|
|
763
|
+
guard ["horizontal", "vertical"].contains(node.orientation ?? "") else {
|
|
764
|
+
throw exception("GUI layout orientation must be horizontal or vertical.")
|
|
765
|
+
}
|
|
766
|
+
try validateIosLayout(node.ios)
|
|
767
|
+
try node.children.forEach { try validate($0, ids: &ids) }
|
|
768
|
+
case "image":
|
|
769
|
+
try validateSource(node.source)
|
|
770
|
+
if let child = node.child {
|
|
771
|
+
try validate(child, ids: &ids)
|
|
772
|
+
}
|
|
773
|
+
case "text":
|
|
774
|
+
guard node.text != nil else { throw exception("GUI text is required.") }
|
|
775
|
+
_ = try positivePixelInt(node.textSize ?? 0, name: "text size")
|
|
776
|
+
_ = try color(node.color ?? "")
|
|
777
|
+
if let stroke = node.stroke {
|
|
778
|
+
_ = try color(stroke.color)
|
|
779
|
+
_ = try nonnegativePixelInt(stroke.width, name: "stroke width")
|
|
780
|
+
}
|
|
781
|
+
case "button":
|
|
782
|
+
guard node.platform == "ios" else { throw exception("GUI buttons require platform: ios.") }
|
|
783
|
+
try validateSource(node.source)
|
|
784
|
+
try validateSource(node.pressedSource)
|
|
785
|
+
if let child = node.child {
|
|
786
|
+
try validate(child, ids: &ids)
|
|
787
|
+
}
|
|
788
|
+
case "animatedImage":
|
|
789
|
+
guard node.platform == "ios", !node.sources.isEmpty else {
|
|
790
|
+
throw exception("Animated GUI images require platform: ios and at least one image.")
|
|
791
|
+
}
|
|
792
|
+
try node.sources.forEach(validateSource)
|
|
793
|
+
_ = try positiveInt(node.duration ?? 0, name: "animated-image duration")
|
|
794
|
+
_ = try nonnegativeInt(node.repeat ?? 1, name: "animated-image repeat")
|
|
795
|
+
if let child = node.child {
|
|
796
|
+
try validate(child, ids: &ids)
|
|
797
|
+
}
|
|
798
|
+
default: throw exception("Unknown GUI component type: \(node.type).")
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
private func validateIosLayout(_ value: MapGuiIosLayoutRecord?) throws {
|
|
803
|
+
guard let value else { return }
|
|
804
|
+
if let backgroundColor = value.backgroundColor {
|
|
805
|
+
_ = try color(backgroundColor)
|
|
806
|
+
}
|
|
807
|
+
if let splitLine = value.splitLine {
|
|
808
|
+
_ = try color(splitLine.color)
|
|
809
|
+
_ = try nonnegativePixelInt(splitLine.width, name: "split-line width")
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
private func validateSource(_ source: MapGuiImageSourceRecord?) throws {
|
|
814
|
+
guard let source, !source.uri.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
815
|
+
throw exception("GUI image URI must not be empty.")
|
|
816
|
+
}
|
|
817
|
+
if let width = source.width {
|
|
818
|
+
_ = try positivePixelInt(width, name: "image source width")
|
|
819
|
+
}
|
|
820
|
+
if let height = source.height {
|
|
821
|
+
_ = try positivePixelInt(height, name: "image source height")
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private func requiredImage(
|
|
826
|
+
_ source: MapGuiImageSourceRecord?,
|
|
827
|
+
images: [String: UIImage]
|
|
828
|
+
) throws -> UIImage {
|
|
829
|
+
try validateSource(source)
|
|
830
|
+
guard let source, let image = images[source.uri] else {
|
|
831
|
+
throw exception("GUI image was not loaded.")
|
|
832
|
+
}
|
|
833
|
+
return image
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
private func requireInfoWindow(_ id: String) throws -> InfoWindow {
|
|
837
|
+
guard !isDisposed, map != nil else {
|
|
838
|
+
throw KakaoMapException(mapGuiNotFound, "The GUI manager was disposed.")
|
|
839
|
+
}
|
|
840
|
+
guard let infoWindow = infoWindows[id] else {
|
|
841
|
+
throw KakaoMapException(mapGuiNotFound, "Info window \(id) was not found.")
|
|
842
|
+
}
|
|
843
|
+
return infoWindow
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
private func startAutoImages(_ node: MapGuiNodeRecord, owner: GuiBase) {
|
|
847
|
+
if node.type == "animatedImage", node.autoStart {
|
|
848
|
+
(owner.getChild(node.id) as? GuiAnimatedImage)?.start()
|
|
849
|
+
}
|
|
850
|
+
node.children.forEach { startAutoImages($0, owner: owner) }
|
|
851
|
+
if let child = node.child {
|
|
852
|
+
startAutoImages(child, owner: owner)
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
private func requireWidget(_ id: String) throws -> SpriteGui {
|
|
857
|
+
guard !isDisposed, map != nil else {
|
|
858
|
+
throw KakaoMapException(mapGuiNotFound, "The GUI manager was disposed.")
|
|
859
|
+
}
|
|
860
|
+
guard let widget = widgets[id] else {
|
|
861
|
+
throw KakaoMapException(mapGuiNotFound, "Widget \(id) was not found.")
|
|
862
|
+
}
|
|
863
|
+
return widget
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
private func requireAnimation(_ id: String) throws -> InfoWindowAnimator {
|
|
867
|
+
guard !isDisposed, map != nil else {
|
|
868
|
+
throw KakaoMapException(mapGuiNotFound, "The GUI manager was disposed.")
|
|
869
|
+
}
|
|
870
|
+
guard let animation = animations[id] else {
|
|
871
|
+
throw KakaoMapException(mapGuiNotFound, "GUI animation \(id) was not found.")
|
|
872
|
+
}
|
|
873
|
+
return animation
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
private func mapPoint(_ value: MapGuiCoordinateRecord) throws -> MapPoint {
|
|
877
|
+
guard value.latitude.isFinite, value.longitude.isFinite,
|
|
878
|
+
(-90 ... 90).contains(value.latitude), (-180 ... 180).contains(value.longitude)
|
|
879
|
+
else { throw exception("Invalid map coordinate.") }
|
|
880
|
+
return MapPoint(longitude: value.longitude, latitude: value.latitude)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private func pixelPoint(_ value: MapGuiPointRecord?) throws -> CGPoint {
|
|
884
|
+
guard let value else { return .zero }
|
|
885
|
+
return try CGPoint(
|
|
886
|
+
x: pixelDouble(value.x, name: "offset x"),
|
|
887
|
+
y: pixelDouble(value.y, name: "offset y")
|
|
888
|
+
)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private func guiSize(_ value: MapGuiSizeRecord) throws -> GuiSize {
|
|
892
|
+
try GuiSize(
|
|
893
|
+
width: UInt(positivePixelInt(value.width, name: "image width")),
|
|
894
|
+
height: UInt(positivePixelInt(value.height, name: "image height"))
|
|
895
|
+
)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
private func preparedImage(
|
|
899
|
+
_ image: UIImage,
|
|
900
|
+
explicitSize explicit: MapGuiSizeRecord?,
|
|
901
|
+
source: MapGuiImageSourceRecord?
|
|
902
|
+
) throws -> (image: UIImage, size: GuiSize?) {
|
|
903
|
+
guard let logicalSize = try logicalImageSize(explicit, source: source, image: image) else {
|
|
904
|
+
return (image, nil)
|
|
905
|
+
}
|
|
906
|
+
let rendered = UIGraphicsImageRenderer(
|
|
907
|
+
size: logicalSize,
|
|
908
|
+
format: MapUnits.imageRendererFormat
|
|
909
|
+
).image { _ in
|
|
910
|
+
image.draw(in: CGRect(origin: .zero, size: logicalSize))
|
|
911
|
+
}
|
|
912
|
+
return try (
|
|
913
|
+
rendered,
|
|
914
|
+
GuiSize(
|
|
915
|
+
width: UInt(positivePixelInt(Double(logicalSize.width), name: "image width")),
|
|
916
|
+
height: UInt(positivePixelInt(Double(logicalSize.height), name: "image height"))
|
|
917
|
+
)
|
|
918
|
+
)
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
private func logicalImageSize(
|
|
922
|
+
_ explicit: MapGuiSizeRecord?,
|
|
923
|
+
source: MapGuiImageSourceRecord?,
|
|
924
|
+
image: UIImage
|
|
925
|
+
) throws -> CGSize? {
|
|
926
|
+
if let explicit {
|
|
927
|
+
return try CGSize(
|
|
928
|
+
width: CGFloat(positiveInt(explicit.width, name: "image width")),
|
|
929
|
+
height: CGFloat(positiveInt(explicit.height, name: "image height"))
|
|
930
|
+
)
|
|
931
|
+
}
|
|
932
|
+
guard source?.width != nil || source?.height != nil else { return nil }
|
|
933
|
+
let aspectRatio = image.size.width / image.size.height
|
|
934
|
+
let width = source?.width ?? ((source?.height ?? 0) * aspectRatio)
|
|
935
|
+
let height = source?.height ?? ((source?.width ?? 0) / aspectRatio)
|
|
936
|
+
return try CGSize(
|
|
937
|
+
width: CGFloat(positiveInt(width, name: "image source width")),
|
|
938
|
+
height: CGFloat(positiveInt(height, name: "image source height"))
|
|
939
|
+
)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
private func padding(_ value: MapGuiPaddingRecord?) throws -> GuiPadding {
|
|
943
|
+
guard let value else { return GuiPadding(left: 0, right: 0, top: 0, bottom: 0) }
|
|
944
|
+
return try GuiPadding(
|
|
945
|
+
left: Int32(nonnegativePixelInt(value.left, name: "GUI padding left")),
|
|
946
|
+
right: Int32(nonnegativePixelInt(value.right, name: "GUI padding right")),
|
|
947
|
+
top: Int32(nonnegativePixelInt(value.top, name: "GUI padding top")),
|
|
948
|
+
bottom: Int32(nonnegativePixelInt(value.bottom, name: "GUI padding bottom"))
|
|
949
|
+
)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
private func alignment(_ value: String) throws -> GuiAlignment {
|
|
953
|
+
let vertical: VerticalAlign
|
|
954
|
+
let horizontal: HorizontalAlign
|
|
955
|
+
switch value {
|
|
956
|
+
case "topLeft": (vertical, horizontal) = (.top, .left)
|
|
957
|
+
case "topCenter": (vertical, horizontal) = (.top, .center)
|
|
958
|
+
case "topRight": (vertical, horizontal) = (.top, .right)
|
|
959
|
+
case "centerLeft": (vertical, horizontal) = (.middle, .left)
|
|
960
|
+
case "center": (vertical, horizontal) = (.middle, .center)
|
|
961
|
+
case "centerRight": (vertical, horizontal) = (.middle, .right)
|
|
962
|
+
case "bottomLeft": (vertical, horizontal) = (.bottom, .left)
|
|
963
|
+
case "bottomCenter": (vertical, horizontal) = (.bottom, .center)
|
|
964
|
+
case "bottomRight": (vertical, horizontal) = (.bottom, .right)
|
|
965
|
+
default: throw exception("Unknown GUI alignment: \(value).")
|
|
966
|
+
}
|
|
967
|
+
return GuiAlignment(vAlign: vertical, hAlign: horizontal)
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
private func alignmentName(_ value: GuiAlignment) -> String {
|
|
971
|
+
switch (value.vAlign, value.hAlign) {
|
|
972
|
+
case (.top, .left): "topLeft"
|
|
973
|
+
case (.top, .center): "topCenter"
|
|
974
|
+
case (.top, .right): "topRight"
|
|
975
|
+
case (.middle, .left): "centerLeft"
|
|
976
|
+
case (.middle, .center): "center"
|
|
977
|
+
case (.middle, .right): "centerRight"
|
|
978
|
+
case (.bottom, .left): "bottomLeft"
|
|
979
|
+
case (.bottom, .center): "bottomCenter"
|
|
980
|
+
case (.bottom, .right): "bottomRight"
|
|
981
|
+
default: "topLeft"
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
private func interpolation(_ value: String, duration: Double) throws -> AnimationInterpolation {
|
|
986
|
+
let method: InterpolationMethodType
|
|
987
|
+
switch value {
|
|
988
|
+
case "linear": method = .linear
|
|
989
|
+
case "cubicIn": method = .cubicIn
|
|
990
|
+
case "cubicOut": method = .cubicOut
|
|
991
|
+
case "cubicInOut": method = .cubicInOut
|
|
992
|
+
default: throw exception("Unknown animation interpolation: \(value).")
|
|
993
|
+
}
|
|
994
|
+
return try AnimationInterpolation(
|
|
995
|
+
duration: UInt32(positiveInt(duration, name: "animation duration")),
|
|
996
|
+
method: method
|
|
997
|
+
)
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
private func animationStateName(_ value: KakaoMapsSDK.AnimationState) -> String {
|
|
1001
|
+
switch value {
|
|
1002
|
+
case .start: "started"
|
|
1003
|
+
case .pause: "paused"
|
|
1004
|
+
case .resume: "resumed"
|
|
1005
|
+
case .stop: "stopped"
|
|
1006
|
+
default: "unknown"
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
private func color(_ value: String) throws -> UIColor {
|
|
1011
|
+
guard value.hasPrefix("#") else {
|
|
1012
|
+
throw exception("Colors must use #RRGGBB or #RRGGBBAA.")
|
|
1013
|
+
}
|
|
1014
|
+
let hex = String(value.dropFirst())
|
|
1015
|
+
guard [6, 8].contains(hex.count), let number = UInt64(hex, radix: 16) else {
|
|
1016
|
+
throw exception("Colors must use #RRGGBB or #RRGGBBAA.")
|
|
1017
|
+
}
|
|
1018
|
+
let alpha = hex.count == 8 ? CGFloat(number & 0xFF) / 255 : 1
|
|
1019
|
+
let rgb = hex.count == 8 ? number >> 8 : number
|
|
1020
|
+
return UIColor(
|
|
1021
|
+
red: CGFloat((rgb >> 16) & 0xFF) / 255,
|
|
1022
|
+
green: CGFloat((rgb >> 8) & 0xFF) / 255,
|
|
1023
|
+
blue: CGFloat(rgb & 0xFF) / 255,
|
|
1024
|
+
alpha: alpha
|
|
1025
|
+
)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
private func validateDuration(_ value: Double) throws {
|
|
1029
|
+
guard value.isFinite, value >= 0, value <= Double(UInt32.max), value.rounded() == value else {
|
|
1030
|
+
throw exception("Duration must be a nonnegative integer in milliseconds.")
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
private func validatePoint(_ x: Double, _ y: Double, name: String) throws {
|
|
1035
|
+
guard x.isFinite, y.isFinite else { throw exception("\(name) must be finite.") }
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
private func positiveInt(_ value: Double, name: String) throws -> Int {
|
|
1039
|
+
guard value.isFinite, value > 0, value <= Double(Int32.max), value.rounded() == value else {
|
|
1040
|
+
throw exception("\(name) must be a positive integer.")
|
|
1041
|
+
}
|
|
1042
|
+
return Int(value)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private func nonnegativeInt(_ value: Double, name: String) throws -> Int {
|
|
1046
|
+
guard value.isFinite, value >= 0, value <= Double(Int32.max), value.rounded() == value else {
|
|
1047
|
+
throw exception("\(name) must be a nonnegative integer.")
|
|
1048
|
+
}
|
|
1049
|
+
return Int(value)
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
private func pixelFloat(_ value: Double, name: String) throws -> Float {
|
|
1053
|
+
let pixels = try pixelDouble(value, name: name)
|
|
1054
|
+
guard abs(pixels) <= Double(Float.greatestFiniteMagnitude) else {
|
|
1055
|
+
throw exception("\(name) must fit a 32-bit floating-point pixel value.")
|
|
1056
|
+
}
|
|
1057
|
+
return Float(pixels)
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
private func pixelDouble(_ value: Double, name: String) throws -> Double {
|
|
1061
|
+
let pixels = MapUnits.pixels(value)
|
|
1062
|
+
guard pixels.isFinite else { throw exception("\(name) must be finite after pixel conversion.") }
|
|
1063
|
+
return pixels
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
private func positivePixelInt(_ value: Double, name: String) throws -> Int {
|
|
1067
|
+
_ = try positiveInt(value, name: name)
|
|
1068
|
+
let result = try pixelInteger(value, name: name)
|
|
1069
|
+
guard result > 0 else { throw exception("\(name) is too small after pixel conversion.") }
|
|
1070
|
+
return result
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
private func nonnegativePixelInt(_ value: Double, name: String) throws -> Int {
|
|
1074
|
+
_ = try nonnegativeInt(value, name: name)
|
|
1075
|
+
return try pixelInteger(value, name: name)
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
private func pixelInteger(_ value: Double, name: String = "pixel value") throws -> Int {
|
|
1079
|
+
let pixels = MapUnits.pixels(value).rounded()
|
|
1080
|
+
guard pixels.isFinite, pixels >= 0, pixels <= Double(Int32.max) else {
|
|
1081
|
+
throw exception("\(name) is out of range after pixel conversion.")
|
|
1082
|
+
}
|
|
1083
|
+
return Int(pixels)
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
private func unique(_ values: [String], name: String) throws {
|
|
1087
|
+
guard Set(values).count == values.count else { throw exception("Duplicate \(name) ID.") }
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
private func nonempty(_ value: String, name: String) throws {
|
|
1091
|
+
guard !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
1092
|
+
throw exception("\(name) must not be empty.")
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
private func exception(_ message: String) -> KakaoMapException {
|
|
1097
|
+
KakaoMapException(mapGuiInvalidArgument, message)
|
|
1098
|
+
}
|
|
1099
|
+
}
|