@react-native-kakao/map 2.2.7 → 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,903 @@
|
|
|
1
|
+
import CoreFoundation
|
|
2
|
+
import KakaoMapsSDK
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
final class MapRoutes {
|
|
6
|
+
private let imageLoader: MapImageLoader
|
|
7
|
+
private let emit: (String, [String: Any]) -> Void
|
|
8
|
+
private let manager: RouteManager
|
|
9
|
+
private var layers: [String: RouteLayer] = [:]
|
|
10
|
+
private var routes: [String: Route] = [:]
|
|
11
|
+
private var styles: [String: RouteStyleSet] = [:]
|
|
12
|
+
private var styleCounts: [String: Int] = [:]
|
|
13
|
+
private var nativeStyleIDs: [String: String] = [:]
|
|
14
|
+
private var nativeLayerIDs: [String: String] = [:]
|
|
15
|
+
private var routeLayerIDs: [String: String] = [:]
|
|
16
|
+
private var animators: [String: RouteAnimator] = [:]
|
|
17
|
+
private var animationTargets: [String: Set<String>] = [:]
|
|
18
|
+
private var animationNativeIDs: [String: String] = [:]
|
|
19
|
+
private var animationRunEpochs: [String: UInt64] = [:]
|
|
20
|
+
private var progress: [String: Float] = [:]
|
|
21
|
+
private var progressEpochs: [String: UInt64] = [:]
|
|
22
|
+
private var imageTokens: [MapImageLoadToken] = []
|
|
23
|
+
private var pendingApply: ((Result<[String: Any], Error>) -> Void)?
|
|
24
|
+
private var generation = 0
|
|
25
|
+
private var sceneSequence = 0
|
|
26
|
+
private var animationSequence: UInt64 = 0
|
|
27
|
+
|
|
28
|
+
init(
|
|
29
|
+
map: KakaoMap,
|
|
30
|
+
imageLoader: MapImageLoader,
|
|
31
|
+
emit: @escaping (String, [String: Any]) -> Void
|
|
32
|
+
) {
|
|
33
|
+
self.imageLoader = imageLoader
|
|
34
|
+
self.emit = emit
|
|
35
|
+
manager = map.getRouteManager()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func apply(
|
|
39
|
+
_ scene: [String: Any],
|
|
40
|
+
completion: @escaping (Result<[String: Any], Error>) -> Void
|
|
41
|
+
) throws {
|
|
42
|
+
rejectPendingApply(
|
|
43
|
+
code: "ERR_KAKAO_MAP_ROUTE_SUPERSEDED",
|
|
44
|
+
message: "The route scene was superseded by a newer update."
|
|
45
|
+
)
|
|
46
|
+
cancelImages()
|
|
47
|
+
generation += 1
|
|
48
|
+
let token = generation
|
|
49
|
+
let styleSpecs = try scene.objectList("styleSets")
|
|
50
|
+
let layerSpecs = try scene.objectList("layers")
|
|
51
|
+
let routeSpecs = try scene.objectList("routes")
|
|
52
|
+
try validateScene(styles: styleSpecs, layers: layerSpecs, routes: routeSpecs)
|
|
53
|
+
pendingApply = completion
|
|
54
|
+
let uris = try Set(styleSpecs.flatMap { style in
|
|
55
|
+
try style.objectList("patterns").flatMap { pattern in
|
|
56
|
+
try [pattern.object("image").string("uri"), pattern.optionalObject("symbol")?.string("uri")]
|
|
57
|
+
.compactMap { $0 }
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
guard !uris.isEmpty else {
|
|
61
|
+
let result = Result { try buildScene(
|
|
62
|
+
styles: styleSpecs,
|
|
63
|
+
layers: layerSpecs,
|
|
64
|
+
routes: routeSpecs,
|
|
65
|
+
images: [:]
|
|
66
|
+
) }
|
|
67
|
+
pendingApply = nil
|
|
68
|
+
completion(result)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
var images: [String: UIImage] = [:]
|
|
72
|
+
var didFinish = false
|
|
73
|
+
let finish: (Result<[String: Any], Error>) -> Void = { [weak self] result in
|
|
74
|
+
guard let self, !didFinish, token == generation else { return }
|
|
75
|
+
didFinish = true
|
|
76
|
+
if case .failure = result {
|
|
77
|
+
imageTokens.forEach { $0.cancel() }
|
|
78
|
+
}
|
|
79
|
+
imageTokens.removeAll()
|
|
80
|
+
pendingApply = nil
|
|
81
|
+
completion(result)
|
|
82
|
+
}
|
|
83
|
+
for uri in uris {
|
|
84
|
+
imageTokens.append(imageLoader.load(uri) { [weak self] result in
|
|
85
|
+
guard let self, token == generation else { return }
|
|
86
|
+
switch result {
|
|
87
|
+
case let .success(image):
|
|
88
|
+
images[uri] = image
|
|
89
|
+
if images.count == uris.count {
|
|
90
|
+
finish(Result { try self.buildScene(
|
|
91
|
+
styles: styleSpecs,
|
|
92
|
+
layers: layerSpecs,
|
|
93
|
+
routes: routeSpecs,
|
|
94
|
+
images: images
|
|
95
|
+
) })
|
|
96
|
+
}
|
|
97
|
+
case let .failure(error):
|
|
98
|
+
finish(.failure(error))
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func command(_ command: [String: Any]) throws -> [String: Any] {
|
|
105
|
+
switch try command.string("type") {
|
|
106
|
+
case "setVisible":
|
|
107
|
+
let visible = try command.bool("visible")
|
|
108
|
+
let targets = try routeEntries(command.stringList("ids"))
|
|
109
|
+
targets.forEach { visible ? $0.show() : $0.hide() }
|
|
110
|
+
return none()
|
|
111
|
+
case "setZIndex":
|
|
112
|
+
try route(command.string("id")).zOrder = command.int("zIndex")
|
|
113
|
+
return none()
|
|
114
|
+
case "changeData":
|
|
115
|
+
let id = try command.string("id")
|
|
116
|
+
let styleID = try command.string("styleSetId")
|
|
117
|
+
guard styles[styleID] != nil else { throw routeNotFound("route style set", id: styleID) }
|
|
118
|
+
guard let nativeStyleID = nativeStyleIDs[styleID]
|
|
119
|
+
else { throw routeNotFound("route style set", id: styleID) }
|
|
120
|
+
let segmentSpecs = try command.objectList("segments")
|
|
121
|
+
try validateSegments(segmentSpecs, styleCount: styleCounts[styleID] ?? 0)
|
|
122
|
+
let target = try route(id)
|
|
123
|
+
invalidateProgressRequest(id)
|
|
124
|
+
try target.changeStyleAndData(
|
|
125
|
+
styleID: nativeStyleID,
|
|
126
|
+
segments: segmentSpecs.map(routeSegment)
|
|
127
|
+
)
|
|
128
|
+
return none()
|
|
129
|
+
case "setProgress":
|
|
130
|
+
let id = try command.string("id")
|
|
131
|
+
let value = try routeProgress(command.double("progress"))
|
|
132
|
+
let mode = try progressType(command.string("mode"))
|
|
133
|
+
let duration = try command.nonnegativeInt("duration", default: 0)
|
|
134
|
+
let target = try route(id)
|
|
135
|
+
progress[id] = value
|
|
136
|
+
let sceneGeneration = generation
|
|
137
|
+
let requestEpoch = nextProgressEpoch(id)
|
|
138
|
+
if duration == 0 {
|
|
139
|
+
target.setProgress(progress: value, type: mode, duration: 0, callback: nil)
|
|
140
|
+
emitRequestedProgress(
|
|
141
|
+
id: id,
|
|
142
|
+
progress: value,
|
|
143
|
+
sceneGeneration: sceneGeneration,
|
|
144
|
+
requestEpoch: requestEpoch,
|
|
145
|
+
target: target
|
|
146
|
+
)
|
|
147
|
+
} else {
|
|
148
|
+
target
|
|
149
|
+
.setProgress(
|
|
150
|
+
progress: value,
|
|
151
|
+
type: mode,
|
|
152
|
+
duration: UInt(duration)
|
|
153
|
+
) { [weak self] completedRoute in
|
|
154
|
+
guard completedRoute == nil || completedRoute === target else { return }
|
|
155
|
+
self?.emitRequestedProgress(
|
|
156
|
+
id: id,
|
|
157
|
+
progress: value,
|
|
158
|
+
sceneGeneration: sceneGeneration,
|
|
159
|
+
requestEpoch: requestEpoch,
|
|
160
|
+
target: target
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return none()
|
|
165
|
+
case "getProgress":
|
|
166
|
+
throw routeUnsupported("iOS Kakao Maps does not expose fields from RouteProgressInfo.")
|
|
167
|
+
case "getRequestedProgress":
|
|
168
|
+
let id = try command.string("id")
|
|
169
|
+
_ = try route(id)
|
|
170
|
+
return ["type": "progress", "progress": progress[id] ?? 0, "source": "requested"]
|
|
171
|
+
case "getProgressAtCoordinate":
|
|
172
|
+
return try [
|
|
173
|
+
"type": "progress",
|
|
174
|
+
"source": "native",
|
|
175
|
+
"progress": route(command.string("id"))
|
|
176
|
+
.getProgressAlongRouteLine(position: command.coordinate("coordinate"))
|
|
177
|
+
]
|
|
178
|
+
case "getCoordinateAtProgress":
|
|
179
|
+
throw routeUnsupported(
|
|
180
|
+
"iOS Kakao Maps does not expose fields from RoutePointInfo."
|
|
181
|
+
)
|
|
182
|
+
case "getState":
|
|
183
|
+
return try state(command.string("id"))
|
|
184
|
+
case "getCounts":
|
|
185
|
+
return counts()
|
|
186
|
+
case "getCapabilities":
|
|
187
|
+
return capabilities().merging(["type": "capabilities"]) { _, new in new }
|
|
188
|
+
case "startProgressAnimation":
|
|
189
|
+
try startAnimation(command.object("animation"))
|
|
190
|
+
return none()
|
|
191
|
+
case "stopAnimation":
|
|
192
|
+
try stopAnimation(command.string("animationId"), emitEvent: true)
|
|
193
|
+
return none()
|
|
194
|
+
case "remove":
|
|
195
|
+
let ids = try command.stringList("ids")
|
|
196
|
+
_ = try routeEntries(ids)
|
|
197
|
+
try ids.forEach(remove)
|
|
198
|
+
return none()
|
|
199
|
+
default:
|
|
200
|
+
throw routeInvalid("Unknown route command type.")
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func dispose() {
|
|
205
|
+
generation += 1
|
|
206
|
+
rejectPendingApply(
|
|
207
|
+
code: "ERR_KAKAO_MAP_ROUTE_DISPOSED",
|
|
208
|
+
message: "The map view was disposed while applying the route scene."
|
|
209
|
+
)
|
|
210
|
+
cancelImages()
|
|
211
|
+
disposeObjects()
|
|
212
|
+
styles.removeAll()
|
|
213
|
+
styleCounts.removeAll()
|
|
214
|
+
nativeStyleIDs.removeAll()
|
|
215
|
+
nativeLayerIDs.removeAll()
|
|
216
|
+
routeLayerIDs.removeAll()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private func rejectPendingApply(code: String, message: String) {
|
|
220
|
+
let completion = pendingApply
|
|
221
|
+
pendingApply = nil
|
|
222
|
+
completion?(.failure(KakaoMapException(code, message)))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
func capabilities() -> [String: Any] {
|
|
226
|
+
[
|
|
227
|
+
"curves": true,
|
|
228
|
+
"patterns": true,
|
|
229
|
+
"patternSymbols": true,
|
|
230
|
+
"patternImageSizing": false,
|
|
231
|
+
"progressAtCoordinate": true,
|
|
232
|
+
"coordinateAtProgress": false,
|
|
233
|
+
"nativeProgressRead": false,
|
|
234
|
+
"fractionalStyleWidth": false,
|
|
235
|
+
"progressAnimationRepeat": false
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private func validateScene(
|
|
240
|
+
styles styleSpecs: [[String: Any]],
|
|
241
|
+
layers layerSpecs: [[String: Any]],
|
|
242
|
+
routes routeSpecs: [[String: Any]]
|
|
243
|
+
) throws {
|
|
244
|
+
try requireUniqueRoute(styleSpecs, noun: "style set")
|
|
245
|
+
try requireUniqueRoute(layerSpecs, noun: "route layer")
|
|
246
|
+
try requireUniqueRoute(routeSpecs, noun: "route")
|
|
247
|
+
for layer in layerSpecs {
|
|
248
|
+
_ = try layer.int("zIndex", default: 0)
|
|
249
|
+
_ = try layer.bool("visible", default: true)
|
|
250
|
+
}
|
|
251
|
+
let styles = try Dictionary(uniqueKeysWithValues: styleSpecs.map { try ($0.string("id"), $0) })
|
|
252
|
+
let layerIDs = try Set(layerSpecs.map { try $0.string("id") })
|
|
253
|
+
for route in routeSpecs {
|
|
254
|
+
_ = try route.int("zIndex", default: 0)
|
|
255
|
+
_ = try route.bool("visible", default: true)
|
|
256
|
+
let id = try route.string("id")
|
|
257
|
+
guard try layerIDs.contains(route.string("layerId"))
|
|
258
|
+
else { throw routeInvalid("Route \(id) references an unknown layer.") }
|
|
259
|
+
let styleID = try route.string("styleSetId")
|
|
260
|
+
guard let style = styles[styleID]
|
|
261
|
+
else { throw routeInvalid("Route \(id) references an unknown style set.") }
|
|
262
|
+
let styleCount = try style.objectList("styles").count
|
|
263
|
+
let segments = try route.objectList("segments")
|
|
264
|
+
guard !segments.isEmpty else { throw routeInvalid("Routes require at least one segment.") }
|
|
265
|
+
for segment in segments {
|
|
266
|
+
guard try segment.coordinateList("coordinates").count >= 2
|
|
267
|
+
else { throw routeInvalid("Route segments require at least two coordinates.") }
|
|
268
|
+
guard try segment.nonnegativeInt("styleIndex", default: 0) < styleCount
|
|
269
|
+
else { throw routeInvalid("Route segment styleIndex is out of range.") }
|
|
270
|
+
_ = try routeSegment(segment)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for style in styleSpecs {
|
|
274
|
+
let patterns = try style.objectList("patterns")
|
|
275
|
+
let patternCount = patterns.count
|
|
276
|
+
for pattern in patterns {
|
|
277
|
+
let image = try pattern.object("image")
|
|
278
|
+
guard try !image.string("uri").isEmpty
|
|
279
|
+
else { throw routeInvalid("Route pattern image URI must not be empty.") }
|
|
280
|
+
try rejectRouteImageSizing(image)
|
|
281
|
+
if let symbol = try pattern.optionalObject("symbol") {
|
|
282
|
+
guard try !symbol.string("uri").isEmpty
|
|
283
|
+
else { throw routeInvalid("Route symbol image URI must not be empty.") }
|
|
284
|
+
try rejectRouteImageSizing(symbol)
|
|
285
|
+
}
|
|
286
|
+
guard try pattern.nonnegativeDouble("distance") > 0
|
|
287
|
+
else { throw routeInvalid("Route pattern distance must be positive.") }
|
|
288
|
+
}
|
|
289
|
+
let routeStyles = try style.objectList("styles")
|
|
290
|
+
guard !routeStyles.isEmpty else { throw routeInvalid("Route style sets require styles.") }
|
|
291
|
+
for routeStyle in routeStyles {
|
|
292
|
+
guard try !routeStyle.objectList("levels").isEmpty
|
|
293
|
+
else { throw routeInvalid("Route styles require levels.") }
|
|
294
|
+
}
|
|
295
|
+
for level in try routeStyles.flatMap({ try $0.objectList("levels") }) {
|
|
296
|
+
let index = try level.int("patternIndex", default: -1)
|
|
297
|
+
guard (-1 ..< patternCount).contains(index)
|
|
298
|
+
else { throw routeInvalid("Route patternIndex is out of range.") }
|
|
299
|
+
_ = try level.nonnegativeInt("width")
|
|
300
|
+
_ = try level.nonnegativeInt("strokeWidth", default: 0)
|
|
301
|
+
_ = try level.int("level")
|
|
302
|
+
_ = try level.color("color")
|
|
303
|
+
_ = try level.color("strokeColor", default: .clear)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private func buildScene(
|
|
309
|
+
styles styleSpecs: [[String: Any]],
|
|
310
|
+
layers layerSpecs: [[String: Any]],
|
|
311
|
+
routes routeSpecs: [[String: Any]],
|
|
312
|
+
images: [String: UIImage]
|
|
313
|
+
) throws -> [String: Any] {
|
|
314
|
+
var nextStyles: [String: RouteStyleSet] = [:]
|
|
315
|
+
var nextStyleCounts: [String: Int] = [:]
|
|
316
|
+
var nextNativeStyleIDs: [String: String] = [:]
|
|
317
|
+
var nextLayers: [String: RouteLayer] = [:]
|
|
318
|
+
var nextNativeLayerIDs: [String: String] = [:]
|
|
319
|
+
var nextRoutes: [String: Route] = [:]
|
|
320
|
+
var nextRouteLayerIDs: [String: String] = [:]
|
|
321
|
+
try registerStyles(
|
|
322
|
+
styleSpecs,
|
|
323
|
+
images: images,
|
|
324
|
+
styles: &nextStyles,
|
|
325
|
+
styleCounts: &nextStyleCounts,
|
|
326
|
+
nativeStyleIDs: &nextNativeStyleIDs
|
|
327
|
+
)
|
|
328
|
+
let scenePrefix = "rnk-routes-\(ObjectIdentifier(self).hashValue)-scene-\(sceneSequence)"
|
|
329
|
+
sceneSequence += 1
|
|
330
|
+
do {
|
|
331
|
+
for spec in layerSpecs {
|
|
332
|
+
let id = try spec.string("id")
|
|
333
|
+
let nativeID = "\(scenePrefix)-layer-\(id)"
|
|
334
|
+
guard let layer = try manager.addRouteLayer(
|
|
335
|
+
layerID: nativeID,
|
|
336
|
+
zOrder: spec.int("zIndex", default: 0)
|
|
337
|
+
) else {
|
|
338
|
+
throw routeInvalid("Could not create route layer \(id).")
|
|
339
|
+
}
|
|
340
|
+
layer.visible = false
|
|
341
|
+
nextLayers[id] = layer
|
|
342
|
+
nextNativeLayerIDs[id] = nativeID
|
|
343
|
+
}
|
|
344
|
+
for spec in routeSpecs {
|
|
345
|
+
let id = try spec.string("id")
|
|
346
|
+
let styleID = try spec.string("styleSetId")
|
|
347
|
+
guard let nativeStyleID = nextNativeStyleIDs[styleID]
|
|
348
|
+
else { throw routeNotFound("route style set", id: styleID) }
|
|
349
|
+
let options = try RouteOptions(
|
|
350
|
+
routeID: id,
|
|
351
|
+
styleID: nativeStyleID,
|
|
352
|
+
zOrder: spec.int("zIndex", default: 0)
|
|
353
|
+
)
|
|
354
|
+
options.segments = try spec.objectList("segments").map(routeSegment)
|
|
355
|
+
let layerID = try spec.string("layerId")
|
|
356
|
+
guard let value = nextLayers[layerID]?.addRoute(option: options) else {
|
|
357
|
+
throw routeInvalid("Could not create route \(id).")
|
|
358
|
+
}
|
|
359
|
+
if try spec.bool("visible", default: true) {
|
|
360
|
+
value.show()
|
|
361
|
+
} else {
|
|
362
|
+
value.hide()
|
|
363
|
+
}
|
|
364
|
+
nextRoutes[id] = value
|
|
365
|
+
nextRouteLayerIDs[id] = layerID
|
|
366
|
+
}
|
|
367
|
+
} catch {
|
|
368
|
+
nextNativeLayerIDs.values.forEach { manager.removeRouteLayer(layerID: $0) }
|
|
369
|
+
throw error
|
|
370
|
+
}
|
|
371
|
+
disposeObjects()
|
|
372
|
+
styles = nextStyles
|
|
373
|
+
styleCounts = nextStyleCounts
|
|
374
|
+
nativeStyleIDs = nextNativeStyleIDs
|
|
375
|
+
layers = nextLayers
|
|
376
|
+
nativeLayerIDs = nextNativeLayerIDs
|
|
377
|
+
routes = nextRoutes
|
|
378
|
+
routeLayerIDs = nextRouteLayerIDs
|
|
379
|
+
progress = Dictionary(uniqueKeysWithValues: nextRoutes.keys.map { ($0, 0) })
|
|
380
|
+
for spec in layerSpecs {
|
|
381
|
+
try nextLayers[spec.string("id")]?.visible = try spec.bool("visible", default: true)
|
|
382
|
+
}
|
|
383
|
+
return counts()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private func registerStyles(
|
|
387
|
+
_ specs: [[String: Any]],
|
|
388
|
+
images: [String: UIImage],
|
|
389
|
+
styles: inout [String: RouteStyleSet],
|
|
390
|
+
styleCounts: inout [String: Int],
|
|
391
|
+
nativeStyleIDs: inout [String: String]
|
|
392
|
+
) throws {
|
|
393
|
+
for spec in specs {
|
|
394
|
+
let id = try spec.string("id")
|
|
395
|
+
let nativeID = mapStyleNativeID("rnk-route-style", spec)
|
|
396
|
+
let patterns = try spec.objectList("patterns").map { pattern -> RoutePattern in
|
|
397
|
+
let imageURI = try pattern.object("image").string("uri")
|
|
398
|
+
guard let image = images[imageURI]
|
|
399
|
+
else { throw routeInvalid("Missing route pattern image.") }
|
|
400
|
+
let symbol = try pattern.optionalObject("symbol").map { source -> UIImage in
|
|
401
|
+
let uri = try source.string("uri")
|
|
402
|
+
guard let result = images[uri] else { throw routeInvalid("Missing route symbol image.") }
|
|
403
|
+
return result
|
|
404
|
+
}
|
|
405
|
+
return try RoutePattern(
|
|
406
|
+
pattern: image,
|
|
407
|
+
distance: routePixelFloat(pattern.nonnegativeDouble("distance")),
|
|
408
|
+
symbol: symbol,
|
|
409
|
+
pinStart: pattern.bool("pinStart", default: false),
|
|
410
|
+
pinEnd: pattern.bool("pinEnd", default: false)
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
let routeStyles = try spec.objectList("styles").map { style in
|
|
414
|
+
try RouteStyle(styles: style.objectList("levels").map { level in
|
|
415
|
+
try PerLevelRouteStyle(
|
|
416
|
+
width: routePixelUInt(level.nonnegativeInt("width")),
|
|
417
|
+
color: level.color("color"),
|
|
418
|
+
strokeWidth: routePixelUInt(level.nonnegativeInt("strokeWidth", default: 0)),
|
|
419
|
+
strokeColor: level.color("strokeColor", default: .clear),
|
|
420
|
+
level: level.int("level"),
|
|
421
|
+
patternIndex: level.int("patternIndex", default: -1)
|
|
422
|
+
)
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
let styleSet = RouteStyleSet(styleID: nativeID, styles: routeStyles)
|
|
426
|
+
patterns.forEach(styleSet.addPattern)
|
|
427
|
+
manager.addRouteStyleSet(styleSet)
|
|
428
|
+
styles[id] = styleSet
|
|
429
|
+
styleCounts[id] = routeStyles.count
|
|
430
|
+
nativeStyleIDs[id] = nativeID
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private func routeSegment(_ spec: [String: Any]) throws -> RouteSegment {
|
|
435
|
+
try RouteSegment(
|
|
436
|
+
points: routePoints(spec),
|
|
437
|
+
styleIndex: UInt(spec.nonnegativeInt("styleIndex", default: 0))
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private func validateSegments(_ segments: [[String: Any]], styleCount: Int) throws {
|
|
442
|
+
guard !segments.isEmpty else { throw routeInvalid("Routes require at least one segment.") }
|
|
443
|
+
for segment in segments {
|
|
444
|
+
guard try segment.coordinateList("coordinates").count >= 2
|
|
445
|
+
else { throw routeInvalid("Route segments require at least two coordinates.") }
|
|
446
|
+
guard try segment.nonnegativeInt("styleIndex", default: 0) < styleCount
|
|
447
|
+
else { throw routeInvalid("Route segment styleIndex is out of range.") }
|
|
448
|
+
_ = try routeSegment(segment)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private func routePoints(_ spec: [String: Any]) throws -> [MapPoint] {
|
|
453
|
+
let coordinates = try spec.coordinateList("coordinates")
|
|
454
|
+
let curve = try spec.optionalString("curve") ?? "none"
|
|
455
|
+
let points: [MapPoint]
|
|
456
|
+
switch curve {
|
|
457
|
+
case "none": points = coordinates
|
|
458
|
+
case "left",
|
|
459
|
+
"right":
|
|
460
|
+
points = coordinates.indices.dropLast().flatMap { index -> [MapPoint] in
|
|
461
|
+
let part = Primitives.getCurvePoints(
|
|
462
|
+
startPoint: coordinates[index],
|
|
463
|
+
endPoint: coordinates[index + 1],
|
|
464
|
+
isLeft: curve == "left"
|
|
465
|
+
)
|
|
466
|
+
return index == 0 ? part : Array(part.dropFirst())
|
|
467
|
+
}
|
|
468
|
+
default: throw routeInvalid("Unknown route curve type.")
|
|
469
|
+
}
|
|
470
|
+
return points
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private func startAnimation(_ animation: [String: Any]) throws {
|
|
474
|
+
let id = try animation.string("animationId")
|
|
475
|
+
guard !id.isEmpty else { throw routeInvalid("Route animationId must not be empty.") }
|
|
476
|
+
guard animators[id] == nil else { throw routeInvalid("Route animation \(id) already exists.") }
|
|
477
|
+
guard try animation.minimumInt("repeat", default: 1, minimum: 1) == 1 else {
|
|
478
|
+
throw routeUnsupported("The Kakao route progress animator does not support repeat counts.")
|
|
479
|
+
}
|
|
480
|
+
let duration = try animation.minimumInt("duration", minimum: 1)
|
|
481
|
+
let direction: ProgressDirection
|
|
482
|
+
switch try animation.string("direction") {
|
|
483
|
+
case "forward": direction = .forward
|
|
484
|
+
case "backward": direction = .backward
|
|
485
|
+
default: throw routeInvalid("Unknown route animation direction.")
|
|
486
|
+
}
|
|
487
|
+
let routeIDs = try animation.stringList("routeIds")
|
|
488
|
+
guard !routeIDs.isEmpty else { throw routeInvalid("Route animation requires routeIds.") }
|
|
489
|
+
let mode = try animation.string("mode")
|
|
490
|
+
let resetToInitial = try animation.bool("resetToInitial", default: false)
|
|
491
|
+
let targets = try routeIDs.map(route)
|
|
492
|
+
let effect = try ProgressAnimationEffect(
|
|
493
|
+
direction: direction,
|
|
494
|
+
type: progressType(mode)
|
|
495
|
+
)
|
|
496
|
+
effect.interpolation = try routeInterpolation(
|
|
497
|
+
duration: duration,
|
|
498
|
+
method: animation.optionalString("interpolation") ?? "linear"
|
|
499
|
+
)
|
|
500
|
+
effect.hideAtStop = try animation.bool("hideAtStop", default: false)
|
|
501
|
+
effect.resetToInitialState = resetToInitial
|
|
502
|
+
animationSequence &+= 1
|
|
503
|
+
let runEpoch = animationSequence
|
|
504
|
+
let nativeID = "rnk-route-\(ObjectIdentifier(self).hashValue)-\(runEpoch)"
|
|
505
|
+
guard let animator = manager.addRouteAnimator(animatorID: nativeID, effect: effect) else {
|
|
506
|
+
throw routeInvalid("Could not create route animation \(id).")
|
|
507
|
+
}
|
|
508
|
+
routeIDs.forEach(invalidateProgressRequest)
|
|
509
|
+
for (index, routeID) in routeIDs.enumerated() {
|
|
510
|
+
animator.addRoute(targets[index])
|
|
511
|
+
if !resetToInitial {
|
|
512
|
+
progress[routeID] = mode == "fill" ? 1 : 0
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
animator.setStopCallback { [weak self, weak animator] _ in
|
|
516
|
+
guard let self,
|
|
517
|
+
let animator,
|
|
518
|
+
animationNativeIDs[id] == nativeID,
|
|
519
|
+
animationRunEpochs[id] == runEpoch,
|
|
520
|
+
animators[id] === animator
|
|
521
|
+
else { return }
|
|
522
|
+
animators.removeValue(forKey: id)
|
|
523
|
+
animationTargets.removeValue(forKey: id)
|
|
524
|
+
animationNativeIDs.removeValue(forKey: id)
|
|
525
|
+
animationRunEpochs.removeValue(forKey: id)
|
|
526
|
+
manager.removeRouteAnimator(animatorID: nativeID)
|
|
527
|
+
emit("onRouteAnimationStop", ["animationId": id])
|
|
528
|
+
}
|
|
529
|
+
animators[id] = animator
|
|
530
|
+
animationTargets[id] = Set(routeIDs)
|
|
531
|
+
animationNativeIDs[id] = nativeID
|
|
532
|
+
animationRunEpochs[id] = runEpoch
|
|
533
|
+
animator.start()
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private func stopAnimation(_ id: String, emitEvent: Bool) throws {
|
|
537
|
+
guard let animator = animators.removeValue(forKey: id) else { throw routeNotFound(
|
|
538
|
+
"route animation",
|
|
539
|
+
id: id
|
|
540
|
+
) }
|
|
541
|
+
animationTargets.removeValue(forKey: id)
|
|
542
|
+
guard let nativeID = animationNativeIDs.removeValue(forKey: id)
|
|
543
|
+
else { throw routeNotFound("route animation", id: id) }
|
|
544
|
+
animationRunEpochs.removeValue(forKey: id)
|
|
545
|
+
animator.setStopCallback(nil)
|
|
546
|
+
animator.stop()
|
|
547
|
+
manager.removeRouteAnimator(animatorID: nativeID)
|
|
548
|
+
if emitEvent {
|
|
549
|
+
emit("onRouteAnimationStop", ["animationId": id])
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private func state(_ id: String) throws -> [String: Any] {
|
|
554
|
+
let value = try route(id)
|
|
555
|
+
guard let layerID = routeLayerIDs[id]
|
|
556
|
+
else { throw routeNotFound("route layer", id: "") }
|
|
557
|
+
return [
|
|
558
|
+
"type": "state",
|
|
559
|
+
"id": id,
|
|
560
|
+
"layerId": layerID,
|
|
561
|
+
"visible": value.isShow,
|
|
562
|
+
"zIndex": value.zOrder,
|
|
563
|
+
"nativeProgress": NSNull(),
|
|
564
|
+
"requestedProgress": progress[id] ?? 0
|
|
565
|
+
]
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private func counts() -> [String: Any] {
|
|
569
|
+
[
|
|
570
|
+
"type": "counts",
|
|
571
|
+
"layers": layers.count,
|
|
572
|
+
"routes": routes.count,
|
|
573
|
+
"animations": animators.count
|
|
574
|
+
]
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private func none() -> [String: Any] {
|
|
578
|
+
["type": "none"]
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private func route(_ id: String) throws -> Route {
|
|
582
|
+
guard let value = routes[id]
|
|
583
|
+
else { throw routeNotFound(
|
|
584
|
+
"route",
|
|
585
|
+
id: id
|
|
586
|
+
) }; return value
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private func routeEntries(_ ids: [String]) throws -> [Route] {
|
|
590
|
+
guard Set(ids).count == ids.count
|
|
591
|
+
else { throw routeInvalid("Route command IDs must be unique.") }
|
|
592
|
+
return try ids.map(route)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private func remove(_ id: String) throws {
|
|
596
|
+
invalidateProgressRequest(id)
|
|
597
|
+
for animationID in animationTargets.filter({ $0.value.contains(id) }).map(\.key) {
|
|
598
|
+
try stopAnimation(animationID, emitEvent: true)
|
|
599
|
+
}
|
|
600
|
+
let value = try route(id)
|
|
601
|
+
guard let layerID = routeLayerIDs[id], let layer = layers[layerID] else { throw routeNotFound(
|
|
602
|
+
"route layer",
|
|
603
|
+
id: routeLayerIDs[id] ?? ""
|
|
604
|
+
) }
|
|
605
|
+
layer.removeRoute(routeID: value.routeID)
|
|
606
|
+
routes.removeValue(forKey: id)
|
|
607
|
+
progress.removeValue(forKey: id)
|
|
608
|
+
routeLayerIDs.removeValue(forKey: id)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private func cancelImages() {
|
|
612
|
+
imageTokens.forEach { $0.cancel() }
|
|
613
|
+
imageTokens.removeAll()
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private func disposeObjects() {
|
|
617
|
+
let activeAnimators = animators
|
|
618
|
+
let activeNativeIDs = animationNativeIDs
|
|
619
|
+
animators.removeAll()
|
|
620
|
+
animationTargets.removeAll()
|
|
621
|
+
animationNativeIDs.removeAll()
|
|
622
|
+
animationRunEpochs.removeAll()
|
|
623
|
+
for (id, animator) in activeAnimators {
|
|
624
|
+
animator.setStopCallback(nil)
|
|
625
|
+
animator.stop()
|
|
626
|
+
if let nativeID = activeNativeIDs[id] {
|
|
627
|
+
manager.removeRouteAnimator(animatorID: nativeID)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
nativeLayerIDs.values.forEach { manager.removeRouteLayer(layerID: $0) }
|
|
631
|
+
layers.removeAll()
|
|
632
|
+
nativeLayerIDs.removeAll()
|
|
633
|
+
routes.removeAll()
|
|
634
|
+
routeLayerIDs.removeAll()
|
|
635
|
+
progress.removeAll()
|
|
636
|
+
progressEpochs.removeAll()
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
@discardableResult
|
|
640
|
+
private func nextProgressEpoch(_ id: String) -> UInt64 {
|
|
641
|
+
let epoch = (progressEpochs[id] ?? 0) &+ 1
|
|
642
|
+
progressEpochs[id] = epoch
|
|
643
|
+
return epoch
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private func invalidateProgressRequest(_ id: String) {
|
|
647
|
+
nextProgressEpoch(id)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
private func emitRequestedProgress(
|
|
651
|
+
id: String,
|
|
652
|
+
progress: Float,
|
|
653
|
+
sceneGeneration: Int,
|
|
654
|
+
requestEpoch: UInt64,
|
|
655
|
+
target: Route
|
|
656
|
+
) {
|
|
657
|
+
guard generation == sceneGeneration,
|
|
658
|
+
progressEpochs[id] == requestEpoch,
|
|
659
|
+
routes[id] === target
|
|
660
|
+
else { return }
|
|
661
|
+
emit("onRouteProgress", ["id": id, "progress": progress, "source": "requested"])
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
private func routeProgress(_ value: Double) throws -> Float {
|
|
666
|
+
guard value.isFinite,
|
|
667
|
+
(0 ... 1).contains(value)
|
|
668
|
+
else { throw routeInvalid("Route progress must be between 0 and 1.") }
|
|
669
|
+
return Float(value)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private func progressType(_ value: String) throws -> RouteProgressType {
|
|
673
|
+
switch value {
|
|
674
|
+
case "fill": .fillFromStart; case "clear": .clearFromStart; default: throw routeInvalid(
|
|
675
|
+
"Unknown route progress mode."
|
|
676
|
+
)
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private func routeInterpolation(
|
|
681
|
+
duration: Int,
|
|
682
|
+
method value: String
|
|
683
|
+
) throws -> AnimationInterpolation {
|
|
684
|
+
let method: InterpolationMethodType
|
|
685
|
+
switch value {
|
|
686
|
+
case "linear": method = .linear; case "cubicIn": method = .cubicIn; case "cubicOut": method =
|
|
687
|
+
.cubicOut; case "cubicInOut": method =
|
|
688
|
+
.cubicInOut; default: throw routeInvalid("Unknown interpolation \(value).")
|
|
689
|
+
}
|
|
690
|
+
return AnimationInterpolation(duration: UInt32(duration), method: method)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private func rejectRouteImageSizing(_ source: [String: Any]) throws {
|
|
694
|
+
guard source["width"] == nil, source["height"] == nil
|
|
695
|
+
else { throw routeUnsupported("Kakao route patterns use the source bitmap size.") }
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private func requireUniqueRoute(_ values: [[String: Any]], noun: String) throws {
|
|
699
|
+
let ids = try values.map { try $0.string("id") }; guard Set(ids).count == ids.count
|
|
700
|
+
else { throw routeInvalid("Duplicate \(noun) ID.") }
|
|
701
|
+
guard !ids.contains(where: \.isEmpty)
|
|
702
|
+
else { throw routeInvalid("\(noun) ID must not be empty.") }
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private func routeInvalid(_ message: String) -> KakaoMapException {
|
|
706
|
+
KakaoMapException(
|
|
707
|
+
"ERR_KAKAO_MAP_ROUTE_INVALID_ARGUMENT",
|
|
708
|
+
message
|
|
709
|
+
)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private func routeUnsupported(_ message: String) -> KakaoMapException {
|
|
713
|
+
KakaoMapException(
|
|
714
|
+
"ERR_KAKAO_MAP_ROUTE_UNSUPPORTED",
|
|
715
|
+
message
|
|
716
|
+
)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
private func routeNotFound(_ noun: String, id: String) -> KakaoMapException {
|
|
720
|
+
KakaoMapException(
|
|
721
|
+
"ERR_KAKAO_MAP_ROUTE_NOT_FOUND",
|
|
722
|
+
"Unknown \(noun) \(id)."
|
|
723
|
+
)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private extension [String: Any] {
|
|
727
|
+
func object(_ key: String) throws
|
|
728
|
+
-> [String: Any]
|
|
729
|
+
{
|
|
730
|
+
guard let value = self[key] as? [String: Any]
|
|
731
|
+
else { throw routeInvalid("\(key) must be an object.") }; return value
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
func optionalObject(_ key: String) throws -> [String: Any]? {
|
|
735
|
+
guard let raw = self[key]
|
|
736
|
+
else { return nil }; guard let value = raw as? [String: Any]
|
|
737
|
+
else { throw routeInvalid("\(key) must be an object.") }; return value
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
func objectList(_ key: String) throws -> [[String: Any]] {
|
|
741
|
+
guard let raw = self[key]
|
|
742
|
+
else { return [] }; guard let value = raw as? [[String: Any]]
|
|
743
|
+
else { throw routeInvalid("\(key) must be an array of objects.") }; return value
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
func string(_ key: String) throws -> String {
|
|
747
|
+
guard let value = self[key] as? String
|
|
748
|
+
else { throw routeInvalid("\(key) must be a string.") }; return value
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
func optionalString(_ key: String) throws -> String? {
|
|
752
|
+
guard let raw = self[key] else { return nil }
|
|
753
|
+
guard let value = raw as? String
|
|
754
|
+
else { throw routeInvalid("\(key) must be a string.") }
|
|
755
|
+
return value
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
func stringList(_ key: String) throws -> [String] {
|
|
759
|
+
guard let value = self[key] as? [String]
|
|
760
|
+
else { throw routeInvalid("\(key) must be an array of strings.") }; return value
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
func bool(
|
|
764
|
+
_ key: String,
|
|
765
|
+
default fallback: Bool? = nil
|
|
766
|
+
) throws
|
|
767
|
+
-> Bool
|
|
768
|
+
{
|
|
769
|
+
guard let raw = self[key] else {
|
|
770
|
+
guard let fallback else { throw routeInvalid("\(key) must be a boolean.") }
|
|
771
|
+
return fallback
|
|
772
|
+
}
|
|
773
|
+
guard CFGetTypeID(raw as CFTypeRef) == CFBooleanGetTypeID(),
|
|
774
|
+
let value = raw as? Bool
|
|
775
|
+
else { throw routeInvalid("\(key) must be a boolean.") }
|
|
776
|
+
return value
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
func double(
|
|
780
|
+
_ key: String,
|
|
781
|
+
default fallback: Double? = nil
|
|
782
|
+
) throws -> Double {
|
|
783
|
+
guard let raw = self[key] else {
|
|
784
|
+
guard let fallback else { throw routeInvalid("\(key) must be finite.") }
|
|
785
|
+
return fallback
|
|
786
|
+
}
|
|
787
|
+
guard let value = raw as? NSNumber,
|
|
788
|
+
CFGetTypeID(value) != CFBooleanGetTypeID(),
|
|
789
|
+
value.doubleValue.isFinite
|
|
790
|
+
else { throw routeInvalid("\(key) must be finite.") }
|
|
791
|
+
return value.doubleValue
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
func int(_ key: String, default fallback: Int? = nil) throws -> Int {
|
|
795
|
+
let value = try double(
|
|
796
|
+
key,
|
|
797
|
+
default: fallback.map(Double.init)
|
|
798
|
+
); guard let integer = Int(exactly: value)
|
|
799
|
+
else { throw routeInvalid("\(key) must be an integer.") }; return integer
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
func nonnegativeInt(
|
|
803
|
+
_ key: String,
|
|
804
|
+
default fallback: Int? = nil
|
|
805
|
+
) throws -> Int {
|
|
806
|
+
let value = try int(
|
|
807
|
+
key,
|
|
808
|
+
default: fallback
|
|
809
|
+
); guard value >= 0 else { throw routeInvalid("\(key) must be non-negative.") }; return value
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
func minimumInt(
|
|
813
|
+
_ key: String,
|
|
814
|
+
default fallback: Int? = nil,
|
|
815
|
+
minimum: Int
|
|
816
|
+
) throws -> Int {
|
|
817
|
+
let value = try int(
|
|
818
|
+
key,
|
|
819
|
+
default: fallback
|
|
820
|
+
); guard value >= minimum
|
|
821
|
+
else { throw routeInvalid("\(key) must be at least \(minimum).") }; return value
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
func nonnegativeDouble(_ key: String) throws -> Double {
|
|
825
|
+
let value = try double(
|
|
826
|
+
key
|
|
827
|
+
); guard value >=
|
|
828
|
+
0 else { throw routeInvalid("\(key) must be non-negative.") }; return value
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
func color(
|
|
832
|
+
_ key: String,
|
|
833
|
+
default fallback: UIColor? = nil
|
|
834
|
+
) throws
|
|
835
|
+
-> UIColor
|
|
836
|
+
{
|
|
837
|
+
guard let raw = self[key] else {
|
|
838
|
+
guard let fallback else { throw routeInvalid("\(key) must be a color.") }
|
|
839
|
+
return fallback
|
|
840
|
+
}
|
|
841
|
+
guard let value = raw as? String
|
|
842
|
+
else { throw routeInvalid("\(key) must be a color.") }
|
|
843
|
+
return try parseRouteColor(value)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
func coordinate(_ key: String) throws -> MapPoint {
|
|
847
|
+
try object(key).coordinateValue()
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
func coordinateValue() throws
|
|
851
|
+
-> MapPoint
|
|
852
|
+
{
|
|
853
|
+
let latitude = try double("latitude"); let longitude =
|
|
854
|
+
try double("longitude"); guard (-90 ... 90).contains(latitude),
|
|
855
|
+
(-180 ... 180).contains(longitude)
|
|
856
|
+
else { throw routeInvalid("Invalid coordinate.") }; return MapPoint(
|
|
857
|
+
longitude: longitude,
|
|
858
|
+
latitude: latitude
|
|
859
|
+
)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
func coordinateList(_ key: String) throws
|
|
863
|
+
-> [MapPoint]
|
|
864
|
+
{
|
|
865
|
+
guard let values = self[key] as? [[String: Any]]
|
|
866
|
+
else { throw routeInvalid("\(key) must be an array of coordinates.") }; return try values
|
|
867
|
+
.map { try $0.coordinateValue() }
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
private func routePixels(_ points: Double) throws -> Double {
|
|
872
|
+
let pixels = MapUnits.pixels(points)
|
|
873
|
+
guard pixels.isFinite else { throw routeInvalid("Pixel dimensions must be finite.") }
|
|
874
|
+
return pixels
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
private func routePixelUInt(_ points: Int) throws -> UInt {
|
|
878
|
+
let pixels = try routePixels(Double(points)).rounded()
|
|
879
|
+
guard let integer = UInt(exactly: pixels)
|
|
880
|
+
else { throw routeInvalid("Pixel dimensions are out of range.") }
|
|
881
|
+
return integer
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
private func routePixelFloat(_ points: Double) throws -> Float {
|
|
885
|
+
let pixels = try routePixels(points)
|
|
886
|
+
guard abs(pixels) <= Double(Float.greatestFiniteMagnitude)
|
|
887
|
+
else { throw routeInvalid("Pixel dimensions are out of range.") }
|
|
888
|
+
return Float(pixels)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private func parseRouteColor(_ value: String) throws -> UIColor {
|
|
892
|
+
guard value.range(of: "^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$", options: .regularExpression) != nil
|
|
893
|
+
else { throw routeInvalid("Colors must be #RRGGBB or #RRGGBBAA.") }
|
|
894
|
+
let raw = String(value.dropFirst())
|
|
895
|
+
let rgb = UInt64(raw.prefix(6), radix: 16) ?? 0
|
|
896
|
+
let alpha = raw.count == 8 ? UInt64(raw.suffix(2), radix: 16) ?? 255 : 255
|
|
897
|
+
return UIColor(
|
|
898
|
+
red: CGFloat((rgb >> 16) & 255) / 255,
|
|
899
|
+
green: CGFloat((rgb >> 8) & 255) / 255,
|
|
900
|
+
blue: CGFloat(rgb & 255) / 255,
|
|
901
|
+
alpha: CGFloat(alpha) / 255
|
|
902
|
+
)
|
|
903
|
+
}
|