@react-native-kakao/map 2.2.7 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -0,0 +1,1294 @@
|
|
|
1
|
+
import CoreFoundation
|
|
2
|
+
import KakaoMapsSDK
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
final class MapShapes {
|
|
6
|
+
private let emit: (String, [String: Any]) -> Void
|
|
7
|
+
private let manager: ShapeManager
|
|
8
|
+
private var layers: [String: ShapeLayer] = [:]
|
|
9
|
+
private var shapes: [String: ShapeEntry] = [:]
|
|
10
|
+
private var animators: [String: ShapeAnimator] = [:]
|
|
11
|
+
private var animationTargets: [String: Set<String>] = [:]
|
|
12
|
+
private var animationNativeIDs: [String: String] = [:]
|
|
13
|
+
private var animationRunEpochs: [String: UInt64] = [:]
|
|
14
|
+
private var polygonStyles: [String: PolygonStyleSet] = [:]
|
|
15
|
+
private var polylineStyles: [String: PolylineStyleSet] = [:]
|
|
16
|
+
private var styleCounts: [String: Int] = [:]
|
|
17
|
+
private var nativeStyleIDs: [String: String] = [:]
|
|
18
|
+
private var nativeLayerIDs: [String: String] = [:]
|
|
19
|
+
private var sceneSequence = 0
|
|
20
|
+
private var replacementSequence = 0
|
|
21
|
+
private var animationSequence: UInt64 = 0
|
|
22
|
+
|
|
23
|
+
init(map: KakaoMap, emit: @escaping (String, [String: Any]) -> Void) {
|
|
24
|
+
self.emit = emit
|
|
25
|
+
manager = map.getShapeManager()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func apply(_ scene: [String: Any]) throws -> [String: Any] {
|
|
29
|
+
let styleSpecs = try scene.objectList("styleSets")
|
|
30
|
+
let layerSpecs = try scene.objectList("layers")
|
|
31
|
+
let shapeSpecs = try scene.objectList("shapes")
|
|
32
|
+
try validateScene(styles: styleSpecs, layers: layerSpecs, shapes: shapeSpecs)
|
|
33
|
+
var nextPolygonStyles: [String: PolygonStyleSet] = [:]
|
|
34
|
+
var nextPolylineStyles: [String: PolylineStyleSet] = [:]
|
|
35
|
+
var nextStyleCounts: [String: Int] = [:]
|
|
36
|
+
var nextNativeStyleIDs: [String: String] = [:]
|
|
37
|
+
var nextLayers: [String: ShapeLayer] = [:]
|
|
38
|
+
var nextNativeLayerIDs: [String: String] = [:]
|
|
39
|
+
var nextShapes: [String: ShapeEntry] = [:]
|
|
40
|
+
try registerStyles(
|
|
41
|
+
styleSpecs,
|
|
42
|
+
polygonStyles: &nextPolygonStyles,
|
|
43
|
+
polylineStyles: &nextPolylineStyles,
|
|
44
|
+
styleCounts: &nextStyleCounts,
|
|
45
|
+
nativeStyleIDs: &nextNativeStyleIDs
|
|
46
|
+
)
|
|
47
|
+
let scenePrefix = "rnk-shapes-\(ObjectIdentifier(self).hashValue)-scene-\(sceneSequence)"
|
|
48
|
+
sceneSequence += 1
|
|
49
|
+
do {
|
|
50
|
+
for spec in layerSpecs {
|
|
51
|
+
try addLayer(
|
|
52
|
+
spec,
|
|
53
|
+
scenePrefix: scenePrefix,
|
|
54
|
+
layers: &nextLayers,
|
|
55
|
+
nativeLayerIDs: &nextNativeLayerIDs
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
for spec in shapeSpecs {
|
|
59
|
+
try addShape(
|
|
60
|
+
spec,
|
|
61
|
+
layers: nextLayers,
|
|
62
|
+
shapes: &nextShapes,
|
|
63
|
+
polygonStyles: nextPolygonStyles,
|
|
64
|
+
polylineStyles: nextPolylineStyles,
|
|
65
|
+
nativeStyleIDs: nextNativeStyleIDs
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
nextNativeLayerIDs.values.forEach { manager.removeShapeLayer(layerID: $0) }
|
|
70
|
+
throw error
|
|
71
|
+
}
|
|
72
|
+
disposeObjects()
|
|
73
|
+
polygonStyles = nextPolygonStyles
|
|
74
|
+
polylineStyles = nextPolylineStyles
|
|
75
|
+
styleCounts = nextStyleCounts
|
|
76
|
+
nativeStyleIDs = nextNativeStyleIDs
|
|
77
|
+
layers = nextLayers
|
|
78
|
+
nativeLayerIDs = nextNativeLayerIDs
|
|
79
|
+
shapes = nextShapes
|
|
80
|
+
for spec in layerSpecs {
|
|
81
|
+
try nextLayers[spec.string("id")]?.visible = try spec.bool("visible", default: true)
|
|
82
|
+
}
|
|
83
|
+
return counts()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func command(_ command: [String: Any]) throws -> [String: Any] {
|
|
87
|
+
switch try command.string("type") {
|
|
88
|
+
case "setVisible":
|
|
89
|
+
let isVisible = try command.bool("visible")
|
|
90
|
+
let targets = try entries(command.stringList("ids"))
|
|
91
|
+
try targets.forEach { try $0.setVisible(isVisible) }
|
|
92
|
+
return none()
|
|
93
|
+
case "setPosition":
|
|
94
|
+
try entry(command.string("id")).setPosition(command.coordinate("position"))
|
|
95
|
+
return none()
|
|
96
|
+
case "setZIndex":
|
|
97
|
+
throw shapeUnsupported("iOS Kakao Maps does not expose mutable shape z-order.")
|
|
98
|
+
case "setOrientation":
|
|
99
|
+
try entry(command.string("id")).setOrientation(command.double("orientation"))
|
|
100
|
+
return none()
|
|
101
|
+
case "changeData":
|
|
102
|
+
try changeData(command)
|
|
103
|
+
return none()
|
|
104
|
+
case "startWave":
|
|
105
|
+
try startWave(command.object("animation"))
|
|
106
|
+
return none()
|
|
107
|
+
case "stopAnimation":
|
|
108
|
+
try stopAnimation(command.string("animationId"), emitEvent: true)
|
|
109
|
+
return none()
|
|
110
|
+
case "getState":
|
|
111
|
+
return try state(command.string("id"))
|
|
112
|
+
case "getCounts":
|
|
113
|
+
return counts()
|
|
114
|
+
case "getCapabilities":
|
|
115
|
+
return capabilities().merging(["type": "capabilities"]) { _, new in new }
|
|
116
|
+
case "remove":
|
|
117
|
+
let ids = try command.stringList("ids")
|
|
118
|
+
_ = try entries(ids)
|
|
119
|
+
try ids.forEach(remove)
|
|
120
|
+
return none()
|
|
121
|
+
default:
|
|
122
|
+
throw shapeInvalid("Unknown shape command type.")
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func capabilities() -> [String: Any] {
|
|
127
|
+
[
|
|
128
|
+
"layerPasses": ["default", "overlay", "route"],
|
|
129
|
+
"geographicPrimitives": true,
|
|
130
|
+
"modelPrimitives": true,
|
|
131
|
+
"geographicPosition": false,
|
|
132
|
+
"geographicOrientation": true,
|
|
133
|
+
"modelPosition": true,
|
|
134
|
+
"modelOrientation": true,
|
|
135
|
+
"zIndexMutation": false,
|
|
136
|
+
"fractionalStyleWidth": false,
|
|
137
|
+
"waveAnimation": true,
|
|
138
|
+
"waveTargets": ["polygon", "polyline"],
|
|
139
|
+
"waveNaturalStopEvent": true
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func resolveShape(_ id: String) -> (any Shape)? {
|
|
144
|
+
shapes[id]?.nativeShape
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func dispose() {
|
|
148
|
+
disposeObjects()
|
|
149
|
+
polygonStyles.removeAll()
|
|
150
|
+
polylineStyles.removeAll()
|
|
151
|
+
styleCounts.removeAll()
|
|
152
|
+
nativeStyleIDs.removeAll()
|
|
153
|
+
nativeLayerIDs.removeAll()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private func validateScene(
|
|
157
|
+
styles styleSpecs: [[String: Any]],
|
|
158
|
+
layers layerSpecs: [[String: Any]],
|
|
159
|
+
shapes shapeSpecs: [[String: Any]]
|
|
160
|
+
) throws {
|
|
161
|
+
try requireUniqueShape(styleSpecs, noun: "style set")
|
|
162
|
+
try requireUniqueShape(layerSpecs, noun: "shape layer")
|
|
163
|
+
try requireUniqueShape(shapeSpecs, noun: "shape")
|
|
164
|
+
try styleSpecs.forEach(validateStyleSet)
|
|
165
|
+
for layer in layerSpecs {
|
|
166
|
+
_ = try layer.int("zIndex", default: defaultShapeLayerZIndex)
|
|
167
|
+
_ = try shapeLayerPass(layer.optionalString("pass") ?? "default")
|
|
168
|
+
_ = try layer.bool("visible", default: true)
|
|
169
|
+
}
|
|
170
|
+
let styleTypes = try Dictionary(uniqueKeysWithValues: styleSpecs.map { try (
|
|
171
|
+
$0.string("id"),
|
|
172
|
+
$0.string("type")
|
|
173
|
+
) })
|
|
174
|
+
let styleCounts = try Dictionary(uniqueKeysWithValues: styleSpecs.map { try (
|
|
175
|
+
$0.string("id"),
|
|
176
|
+
$0.objectList("styles").count
|
|
177
|
+
) })
|
|
178
|
+
let layerIDs = try Set(layerSpecs.map { try $0.string("id") })
|
|
179
|
+
for spec in shapeSpecs {
|
|
180
|
+
_ = try spec.int("zIndex", default: 0)
|
|
181
|
+
_ = try spec.bool("visible", default: true)
|
|
182
|
+
let geometry = try spec.object("geometry")
|
|
183
|
+
let type = try geometry.string("type")
|
|
184
|
+
let polylinePrimitives = ["geographicCurve", "modelArc", "geographicArc"]
|
|
185
|
+
let requiredStyle = type.contains("Polyline") || polylinePrimitives
|
|
186
|
+
.contains(type) ? "polyline" : "polygon"
|
|
187
|
+
let id = try spec.string("id")
|
|
188
|
+
guard try layerIDs.contains(spec.string("layerId"))
|
|
189
|
+
else { throw shapeInvalid("Shape \(id) references an unknown layer.") }
|
|
190
|
+
let styleID = try spec.string("styleSetId")
|
|
191
|
+
guard styleTypes[styleID] == requiredStyle, let count = styleCounts[styleID] else {
|
|
192
|
+
throw shapeInvalid("Shape \(id) references an incompatible style set.")
|
|
193
|
+
}
|
|
194
|
+
try validateShapeGeometry(normalizeShapeGeometry(geometry), styleCount: count)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private func validateStyleSet(_ spec: [String: Any]) throws {
|
|
199
|
+
let styles = try spec.objectList("styles")
|
|
200
|
+
guard !styles.isEmpty else { throw shapeInvalid("Shape style sets require styles.") }
|
|
201
|
+
switch try spec.string("type") {
|
|
202
|
+
case "polygon":
|
|
203
|
+
_ = try mapShapePolygonStyleSet(spec)
|
|
204
|
+
case "polyline":
|
|
205
|
+
_ = try polylineCap(spec.optionalString("cap") ?? "square")
|
|
206
|
+
for style in styles {
|
|
207
|
+
let levels = try style.objectList("levels")
|
|
208
|
+
guard !levels.isEmpty else { throw shapeInvalid("Polyline styles require levels.") }
|
|
209
|
+
for level in levels {
|
|
210
|
+
_ = try level.int("level")
|
|
211
|
+
_ = try level.nonnegativeInt("width")
|
|
212
|
+
_ = try level.nonnegativeInt("strokeWidth", default: 0)
|
|
213
|
+
_ = try level.color("color")
|
|
214
|
+
_ = try level.color("strokeColor", default: .clear)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
default:
|
|
218
|
+
throw shapeInvalid("Unknown shape style type.")
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private func registerStyles(
|
|
223
|
+
_ specs: [[String: Any]],
|
|
224
|
+
polygonStyles: inout [String: PolygonStyleSet],
|
|
225
|
+
polylineStyles: inout [String: PolylineStyleSet],
|
|
226
|
+
styleCounts: inout [String: Int],
|
|
227
|
+
nativeStyleIDs: inout [String: String]
|
|
228
|
+
) throws {
|
|
229
|
+
for spec in specs {
|
|
230
|
+
let id = try spec.string("id")
|
|
231
|
+
let nativeID = mapStyleNativeID("rnk-shape-style", spec)
|
|
232
|
+
let nativeSpec = spec.merging(["id": nativeID]) { _, new in new }
|
|
233
|
+
if try spec.string("type") == "polygon" {
|
|
234
|
+
let styleSet = try mapShapePolygonStyleSet(nativeSpec)
|
|
235
|
+
manager.addPolygonStyleSet(styleSet)
|
|
236
|
+
polygonStyles[id] = styleSet
|
|
237
|
+
} else {
|
|
238
|
+
let styles = try spec.objectList("styles").map { style in
|
|
239
|
+
try PolylineStyle(styles: style.objectList("levels").map { level in
|
|
240
|
+
try PerLevelPolylineStyle(
|
|
241
|
+
bodyColor: level.color("color"),
|
|
242
|
+
bodyWidth: shapePixelUInt(level.nonnegativeInt("width")),
|
|
243
|
+
strokeColor: level.color("strokeColor", default: .clear),
|
|
244
|
+
strokeWidth: shapePixelUInt(level.nonnegativeInt("strokeWidth", default: 0)),
|
|
245
|
+
level: level.int("level")
|
|
246
|
+
)
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
let styleSet = try PolylineStyleSet(
|
|
250
|
+
styleSetID: nativeID,
|
|
251
|
+
styles: styles,
|
|
252
|
+
capType: polylineCap(spec.optionalString("cap") ?? "square")
|
|
253
|
+
)
|
|
254
|
+
manager.addPolylineStyleSet(styleSet)
|
|
255
|
+
polylineStyles[id] = styleSet
|
|
256
|
+
}
|
|
257
|
+
styleCounts[id] = try spec.objectList("styles").count
|
|
258
|
+
nativeStyleIDs[id] = nativeID
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private func addLayer(
|
|
263
|
+
_ spec: [String: Any],
|
|
264
|
+
scenePrefix: String,
|
|
265
|
+
layers: inout [String: ShapeLayer],
|
|
266
|
+
nativeLayerIDs: inout [String: String]
|
|
267
|
+
) throws {
|
|
268
|
+
let id = try spec.string("id")
|
|
269
|
+
let nativeID = "\(scenePrefix)-layer-\(id)"
|
|
270
|
+
guard let layer = try manager.addShapeLayer(
|
|
271
|
+
layerID: nativeID,
|
|
272
|
+
zOrder: spec.int("zIndex", default: defaultShapeLayerZIndex),
|
|
273
|
+
passType: shapeLayerPass(spec.optionalString("pass") ?? "default")
|
|
274
|
+
) else { throw shapeInvalid("Could not create shape layer \(id).") }
|
|
275
|
+
layer.visible = false
|
|
276
|
+
layers[id] = layer
|
|
277
|
+
nativeLayerIDs[id] = nativeID
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private func addShape(
|
|
281
|
+
_ spec: [String: Any],
|
|
282
|
+
layers: [String: ShapeLayer],
|
|
283
|
+
shapes: inout [String: ShapeEntry],
|
|
284
|
+
polygonStyles: [String: PolygonStyleSet],
|
|
285
|
+
polylineStyles: [String: PolylineStyleSet],
|
|
286
|
+
nativeStyleIDs: [String: String],
|
|
287
|
+
nativeShapeID: String? = nil
|
|
288
|
+
) throws {
|
|
289
|
+
let id = try spec.string("id")
|
|
290
|
+
let shapeID = nativeShapeID ?? id
|
|
291
|
+
let layerID = try spec.string("layerId")
|
|
292
|
+
guard let layer = layers[layerID] else { throw shapeNotFound("shape layer", id: layerID) }
|
|
293
|
+
let styleID = try spec.string("styleSetId")
|
|
294
|
+
guard let nativeStyleID = nativeStyleIDs[styleID]
|
|
295
|
+
else { throw shapeNotFound("shape style set", id: styleID) }
|
|
296
|
+
let rawGeometry = try spec.object("geometry")
|
|
297
|
+
let geometryType = try rawGeometry.string("type")
|
|
298
|
+
let geometry = try normalizeShapeGeometry(rawGeometry)
|
|
299
|
+
let zOrder = try spec.int("zIndex", default: 0)
|
|
300
|
+
let isVisible = try spec.bool("visible", default: true)
|
|
301
|
+
let type = try geometry.string("type")
|
|
302
|
+
let entry: ShapeEntry
|
|
303
|
+
switch type {
|
|
304
|
+
case "geographicPolygon":
|
|
305
|
+
guard polygonStyles[styleID] != nil else { throw shapeNotFound(
|
|
306
|
+
"polygon style set",
|
|
307
|
+
id: styleID
|
|
308
|
+
) }
|
|
309
|
+
let options = MapPolygonShapeOptions(shapeID: shapeID, styleID: nativeStyleID, zOrder: zOrder)
|
|
310
|
+
options.polygons = try geometry.objectList("parts").map(mapShapeMapPolygon)
|
|
311
|
+
guard let shape = layer.addMapPolygonShape(options)
|
|
312
|
+
else { throw shapeInvalid("Could not create polygon \(id).") }
|
|
313
|
+
shape.userObject = NSNumber(value: zOrder)
|
|
314
|
+
entry = .mapPolygon(layerID, geometryType, shape)
|
|
315
|
+
case "modelPolygon":
|
|
316
|
+
guard polygonStyles[styleID] != nil else { throw shapeNotFound(
|
|
317
|
+
"polygon style set",
|
|
318
|
+
id: styleID
|
|
319
|
+
) }
|
|
320
|
+
let options = PolygonShapeOptions(shapeID: shapeID, styleID: nativeStyleID, zOrder: zOrder)
|
|
321
|
+
options.basePosition = try geometry.coordinate("base")
|
|
322
|
+
options.polygons = try geometry.objectList("parts").map(mapShapeModelPolygon)
|
|
323
|
+
guard let shape = layer.addPolygonShape(options)
|
|
324
|
+
else { throw shapeInvalid("Could not create model polygon \(id).") }
|
|
325
|
+
shape.userObject = NSNumber(value: zOrder)
|
|
326
|
+
shape.orientation = try geometry.double("orientation", default: 0) * .pi / 180
|
|
327
|
+
entry = .polygon(layerID, geometryType, shape)
|
|
328
|
+
case "geographicPolyline",
|
|
329
|
+
"geographicCurve":
|
|
330
|
+
guard polylineStyles[styleID] != nil else { throw shapeNotFound(
|
|
331
|
+
"polyline style set",
|
|
332
|
+
id: styleID
|
|
333
|
+
) }
|
|
334
|
+
let options = MapPolylineShapeOptions(
|
|
335
|
+
shapeID: shapeID,
|
|
336
|
+
styleID: nativeStyleID,
|
|
337
|
+
zOrder: zOrder
|
|
338
|
+
)
|
|
339
|
+
options.polylines = try geometry.objectList("parts").map(mapPolyline)
|
|
340
|
+
guard let shape = layer.addMapPolylineShape(options)
|
|
341
|
+
else { throw shapeInvalid("Could not create polyline \(id).") }
|
|
342
|
+
shape.userObject = NSNumber(value: zOrder)
|
|
343
|
+
entry = .mapPolyline(layerID, geometryType, shape)
|
|
344
|
+
case "modelPolyline":
|
|
345
|
+
guard polylineStyles[styleID] != nil else { throw shapeNotFound(
|
|
346
|
+
"polyline style set",
|
|
347
|
+
id: styleID
|
|
348
|
+
) }
|
|
349
|
+
let options = PolylineShapeOptions(shapeID: shapeID, styleID: nativeStyleID, zOrder: zOrder)
|
|
350
|
+
options.basePosition = try geometry.coordinate("base")
|
|
351
|
+
options.polylines = try geometry.objectList("parts").map(modelPolyline)
|
|
352
|
+
guard let shape = layer.addPolylineShape(options)
|
|
353
|
+
else { throw shapeInvalid("Could not create model polyline \(id).") }
|
|
354
|
+
shape.userObject = NSNumber(value: zOrder)
|
|
355
|
+
shape.orientation = try geometry.double("orientation", default: 0) * .pi / 180
|
|
356
|
+
entry = .polyline(layerID, geometryType, shape)
|
|
357
|
+
default:
|
|
358
|
+
throw shapeInvalid("Unsupported normalized shape geometry.")
|
|
359
|
+
}
|
|
360
|
+
try entry.setVisible(isVisible)
|
|
361
|
+
shapes[id] = entry
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private func normalizeShapeGeometry(_ geometry: [String: Any]) throws -> [String: Any] {
|
|
365
|
+
switch try geometry.string("type") {
|
|
366
|
+
case "modelCircle":
|
|
367
|
+
let points = try Primitives.getCirclePoints(
|
|
368
|
+
radius: geometry.positiveDouble("radius"),
|
|
369
|
+
numPoints: geometry.minimumInt("pointCount", default: 64, minimum: 3),
|
|
370
|
+
cw: geometry.bool("clockwise", default: true)
|
|
371
|
+
).map(shapeSDKModelPointToMeters)
|
|
372
|
+
return try [
|
|
373
|
+
"type": "modelPolygon",
|
|
374
|
+
"base": geometry.object("base"),
|
|
375
|
+
"orientation": geometry.double("orientation", default: 0),
|
|
376
|
+
"parts": [[
|
|
377
|
+
"outer": pointPayload(points),
|
|
378
|
+
"styleIndex": geometry.int("styleIndex", default: 0)
|
|
379
|
+
]]
|
|
380
|
+
]
|
|
381
|
+
case "modelRectangle":
|
|
382
|
+
let points = try Primitives.getRectanglePoints(
|
|
383
|
+
width: geometry.positiveDouble("width"),
|
|
384
|
+
height: geometry.positiveDouble("height"),
|
|
385
|
+
cw: geometry.bool("clockwise", default: true)
|
|
386
|
+
).map(shapeSDKModelPointToMeters)
|
|
387
|
+
return try [
|
|
388
|
+
"type": "modelPolygon",
|
|
389
|
+
"base": geometry.object("base"),
|
|
390
|
+
"orientation": geometry.double("orientation", default: 0),
|
|
391
|
+
"parts": [[
|
|
392
|
+
"outer": pointPayload(points),
|
|
393
|
+
"styleIndex": geometry.int("styleIndex", default: 0)
|
|
394
|
+
]]
|
|
395
|
+
]
|
|
396
|
+
case "geographicCircle":
|
|
397
|
+
let points = try Primitives.getCirclePoints(
|
|
398
|
+
radius: geometry.positiveDouble("radius"),
|
|
399
|
+
numPoints: geometry.minimumInt("pointCount", default: 64, minimum: 3),
|
|
400
|
+
cw: geometry.bool("clockwise", default: true),
|
|
401
|
+
center: geometry.coordinate("center")
|
|
402
|
+
)
|
|
403
|
+
return try [
|
|
404
|
+
"type": "geographicPolygon",
|
|
405
|
+
"parts": [[
|
|
406
|
+
"outer": coordinatePayload(points),
|
|
407
|
+
"styleIndex": geometry.int("styleIndex", default: 0)
|
|
408
|
+
]]
|
|
409
|
+
]
|
|
410
|
+
case "geographicRectangle":
|
|
411
|
+
let points = try Primitives.getRectanglePoints(
|
|
412
|
+
width: geometry.positiveDouble("width"),
|
|
413
|
+
height: geometry.positiveDouble("height"),
|
|
414
|
+
cw: geometry.bool("clockwise", default: true),
|
|
415
|
+
center: geometry.coordinate("center")
|
|
416
|
+
)
|
|
417
|
+
return try [
|
|
418
|
+
"type": "geographicPolygon",
|
|
419
|
+
"parts": [[
|
|
420
|
+
"outer": coordinatePayload(points),
|
|
421
|
+
"styleIndex": geometry.int("styleIndex", default: 0)
|
|
422
|
+
]]
|
|
423
|
+
]
|
|
424
|
+
case "geographicCurve":
|
|
425
|
+
let isLeft: Bool
|
|
426
|
+
switch try geometry.string("side") {
|
|
427
|
+
case "left": isLeft = true
|
|
428
|
+
case "right": isLeft = false
|
|
429
|
+
default: throw shapeInvalid("Curve side must be left or right.")
|
|
430
|
+
}
|
|
431
|
+
let points = try Primitives.getCurvePoints(
|
|
432
|
+
startPoint: geometry.coordinate("start"),
|
|
433
|
+
endPoint: geometry.coordinate("end"),
|
|
434
|
+
isLeft: isLeft
|
|
435
|
+
)
|
|
436
|
+
return try [
|
|
437
|
+
"type": "geographicCurve",
|
|
438
|
+
"parts": [[
|
|
439
|
+
"points": coordinatePayload(points),
|
|
440
|
+
"styleIndex": geometry.int("styleIndex", default: 0)
|
|
441
|
+
]]
|
|
442
|
+
]
|
|
443
|
+
case "modelArc":
|
|
444
|
+
return try [
|
|
445
|
+
"type": "modelPolyline",
|
|
446
|
+
"base": geometry.object("base"),
|
|
447
|
+
"orientation": geometry.double("orientation", default: 0),
|
|
448
|
+
"parts": [[
|
|
449
|
+
"points": pointPayload(shapeArcPoints(geometry)),
|
|
450
|
+
"styleIndex": geometry.int("styleIndex", default: 0)
|
|
451
|
+
]]
|
|
452
|
+
]
|
|
453
|
+
case "geographicArc":
|
|
454
|
+
let center = try geometry.coordinate("center")
|
|
455
|
+
let points = try shapeArcPoints(geometry).map {
|
|
456
|
+
shapeOffset(center, east: Double($0.x), north: Double($0.y))
|
|
457
|
+
}
|
|
458
|
+
return try [
|
|
459
|
+
"type": "geographicPolyline",
|
|
460
|
+
"parts": [[
|
|
461
|
+
"points": coordinatePayload(points),
|
|
462
|
+
"styleIndex": geometry.int("styleIndex", default: 0)
|
|
463
|
+
]]
|
|
464
|
+
]
|
|
465
|
+
default:
|
|
466
|
+
return geometry
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private func changeData(_ command: [String: Any]) throws {
|
|
471
|
+
let id = try command.string("id")
|
|
472
|
+
let old = try entry(id)
|
|
473
|
+
let styleID = try command.string("styleSetId")
|
|
474
|
+
let requestedGeometry = try command.object("geometry")
|
|
475
|
+
let geometry = try normalizeShapeGeometry(requestedGeometry)
|
|
476
|
+
let type = try geometry.string("type")
|
|
477
|
+
let isVisible = old.isVisible
|
|
478
|
+
let expectsPolyline = type.contains("Polyline") || type == "geographicCurve"
|
|
479
|
+
guard expectsPolyline ? polylineStyles[styleID] != nil : polygonStyles[styleID] != nil
|
|
480
|
+
else { throw shapeInvalid("Shape \(id) references an incompatible style set.") }
|
|
481
|
+
try validateShapeGeometry(geometry, styleCount: styleCounts[styleID] ?? 0)
|
|
482
|
+
let spec: [String: Any] = [
|
|
483
|
+
"id": id,
|
|
484
|
+
"layerId": old.layerID,
|
|
485
|
+
"styleSetId": styleID,
|
|
486
|
+
"zIndex": old.zOrder,
|
|
487
|
+
"visible": false,
|
|
488
|
+
"geometry": requestedGeometry
|
|
489
|
+
]
|
|
490
|
+
var replacement: [String: ShapeEntry] = [:]
|
|
491
|
+
try addShape(
|
|
492
|
+
spec,
|
|
493
|
+
layers: layers,
|
|
494
|
+
shapes: &replacement,
|
|
495
|
+
polygonStyles: polygonStyles,
|
|
496
|
+
polylineStyles: polylineStyles,
|
|
497
|
+
nativeStyleIDs: nativeStyleIDs,
|
|
498
|
+
nativeShapeID: "rnk-shape-replacement-\(ObjectIdentifier(self).hashValue)-\(replacementSequence)"
|
|
499
|
+
)
|
|
500
|
+
replacementSequence += 1
|
|
501
|
+
try remove(id)
|
|
502
|
+
guard let value = replacement[id]
|
|
503
|
+
else { throw shapeInvalid("Could not replace shape \(id).") }
|
|
504
|
+
try value.setVisible(isVisible)
|
|
505
|
+
shapes[id] = value
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private func startWave(_ animation: [String: Any]) throws {
|
|
509
|
+
let id = try animation.string("animationId")
|
|
510
|
+
guard !id.isEmpty else { throw shapeInvalid("Shape animationId must not be empty.") }
|
|
511
|
+
guard animators[id] == nil else { throw shapeInvalid("Shape animation \(id) already exists.") }
|
|
512
|
+
let effect = try mapShapeWaveEffect(animation)
|
|
513
|
+
let targets = try animation.stringList("shapeIds").map(entry)
|
|
514
|
+
guard !targets.isEmpty else { throw shapeInvalid("Shape wave animation requires shapeIds.") }
|
|
515
|
+
animationSequence &+= 1
|
|
516
|
+
let runEpoch = animationSequence
|
|
517
|
+
let nativeID = "rnk-shape-\(ObjectIdentifier(self).hashValue)-\(runEpoch)"
|
|
518
|
+
guard let animator = manager.addShapeAnimator(animatorID: nativeID, effect: effect) else {
|
|
519
|
+
throw shapeInvalid("Could not create shape animation \(id).")
|
|
520
|
+
}
|
|
521
|
+
for target in targets {
|
|
522
|
+
switch target {
|
|
523
|
+
case let .mapPolygon(_, _, shape): animator.addMapPolygonShape(shape)
|
|
524
|
+
case let .polygon(_, _, shape): animator.addPolygonShape(shape)
|
|
525
|
+
case let .mapPolyline(_, _, shape): animator.addMapPolylineShape(shape)
|
|
526
|
+
case let .polyline(_, _, shape): animator.addPolylineShape(shape)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
animator.setStopCallback { [weak self, weak animator] _ in
|
|
530
|
+
guard let self,
|
|
531
|
+
let animator,
|
|
532
|
+
animationNativeIDs[id] == nativeID,
|
|
533
|
+
animationRunEpochs[id] == runEpoch,
|
|
534
|
+
animators[id] === animator
|
|
535
|
+
else { return }
|
|
536
|
+
animators.removeValue(forKey: id)
|
|
537
|
+
animationTargets.removeValue(forKey: id)
|
|
538
|
+
animationNativeIDs.removeValue(forKey: id)
|
|
539
|
+
animationRunEpochs.removeValue(forKey: id)
|
|
540
|
+
manager.removeShapeAnimator(animatorID: nativeID)
|
|
541
|
+
emit("onShapeAnimationStop", ["animationId": id])
|
|
542
|
+
}
|
|
543
|
+
animators[id] = animator
|
|
544
|
+
animationTargets[id] = try Set(animation.stringList("shapeIds"))
|
|
545
|
+
animationNativeIDs[id] = nativeID
|
|
546
|
+
animationRunEpochs[id] = runEpoch
|
|
547
|
+
animator.start()
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private func stopAnimation(_ id: String, emitEvent: Bool) throws {
|
|
551
|
+
guard let animator = animators.removeValue(forKey: id) else { throw shapeNotFound(
|
|
552
|
+
"shape animation",
|
|
553
|
+
id: id
|
|
554
|
+
) }
|
|
555
|
+
animationTargets.removeValue(forKey: id)
|
|
556
|
+
guard let nativeID = animationNativeIDs.removeValue(forKey: id)
|
|
557
|
+
else { throw shapeNotFound("shape animation", id: id) }
|
|
558
|
+
animationRunEpochs.removeValue(forKey: id)
|
|
559
|
+
animator.setStopCallback(nil)
|
|
560
|
+
animator.stop()
|
|
561
|
+
manager.removeShapeAnimator(animatorID: nativeID)
|
|
562
|
+
if emitEvent {
|
|
563
|
+
emit("onShapeAnimationStop", ["animationId": id])
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private func remove(_ id: String) throws {
|
|
568
|
+
for animationID in animationTargets.filter({ $0.value.contains(id) }).map(\.key) {
|
|
569
|
+
try stopAnimation(animationID, emitEvent: true)
|
|
570
|
+
}
|
|
571
|
+
let value = try entry(id)
|
|
572
|
+
guard let layer = layers[value.layerID] else { throw shapeNotFound(
|
|
573
|
+
"shape layer",
|
|
574
|
+
id: value.layerID
|
|
575
|
+
) }
|
|
576
|
+
switch value {
|
|
577
|
+
case let .mapPolygon(_, _, shape): layer.removeMapPolygonShape(shapeID: shape.shapeID)
|
|
578
|
+
case let .polygon(_, _, shape): layer.removePolygonShape(shapeID: shape.shapeID)
|
|
579
|
+
case let .mapPolyline(_, _, shape): layer.removeMapPolylineShape(shapeID: shape.shapeID)
|
|
580
|
+
case let .polyline(_, _, shape): layer.removePolylineShape(shapeID: shape.shapeID)
|
|
581
|
+
}
|
|
582
|
+
shapes.removeValue(forKey: id)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private func disposeObjects() {
|
|
586
|
+
let activeAnimators = animators
|
|
587
|
+
let activeNativeIDs = animationNativeIDs
|
|
588
|
+
animators.removeAll()
|
|
589
|
+
animationTargets.removeAll()
|
|
590
|
+
animationNativeIDs.removeAll()
|
|
591
|
+
animationRunEpochs.removeAll()
|
|
592
|
+
for (id, animator) in activeAnimators {
|
|
593
|
+
animator.setStopCallback(nil)
|
|
594
|
+
animator.stop()
|
|
595
|
+
if let nativeID = activeNativeIDs[id] {
|
|
596
|
+
manager.removeShapeAnimator(animatorID: nativeID)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
for id in layers.keys {
|
|
600
|
+
if let nativeID = nativeLayerIDs[id] {
|
|
601
|
+
manager.removeShapeLayer(layerID: nativeID)
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
layers.removeAll()
|
|
605
|
+
nativeLayerIDs.removeAll()
|
|
606
|
+
shapes.removeAll()
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private func state(_ id: String) throws -> [String: Any] {
|
|
610
|
+
let value = try entry(id)
|
|
611
|
+
return [
|
|
612
|
+
"type": "state",
|
|
613
|
+
"id": id,
|
|
614
|
+
"layerId": value.layerID,
|
|
615
|
+
"visible": value.isVisible,
|
|
616
|
+
"zIndex": value.zOrder,
|
|
617
|
+
"geometryType": value.geometryType,
|
|
618
|
+
"position": value.positionPayload ?? NSNull(),
|
|
619
|
+
"orientation": value.orientationDegrees
|
|
620
|
+
]
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private func counts() -> [String: Any] {
|
|
624
|
+
[
|
|
625
|
+
"type": "counts",
|
|
626
|
+
"layers": layers.count,
|
|
627
|
+
"shapes": shapes.count,
|
|
628
|
+
"animations": animators.count
|
|
629
|
+
]
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private func none() -> [String: Any] {
|
|
633
|
+
["type": "none"]
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private func entry(_ id: String) throws -> ShapeEntry {
|
|
637
|
+
guard let value = shapes[id]
|
|
638
|
+
else { throw shapeNotFound(
|
|
639
|
+
"shape",
|
|
640
|
+
id: id
|
|
641
|
+
) }; return value
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private func entries(_ ids: [String]) throws -> [ShapeEntry] {
|
|
645
|
+
guard Set(ids).count == ids.count
|
|
646
|
+
else { throw shapeInvalid("Shape command IDs must be unique.") }
|
|
647
|
+
return try ids.map(entry)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private enum ShapeEntry {
|
|
652
|
+
case mapPolygon(String, String, MapPolygonShape)
|
|
653
|
+
case polygon(String, String, PolygonShape)
|
|
654
|
+
case mapPolyline(String, String, MapPolylineShape)
|
|
655
|
+
case polyline(String, String, PolylineShape)
|
|
656
|
+
|
|
657
|
+
var nativeShape: any Shape {
|
|
658
|
+
switch self {
|
|
659
|
+
case let .mapPolygon(_, _, value): value
|
|
660
|
+
case let .polygon(_, _, value): value
|
|
661
|
+
case let .mapPolyline(_, _, value): value
|
|
662
|
+
case let .polyline(_, _, value): value
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
var layerID: String {
|
|
667
|
+
switch self { case let .mapPolygon(id, _, _),
|
|
668
|
+
let .polygon(id, _, _),
|
|
669
|
+
let .mapPolyline(
|
|
670
|
+
id,
|
|
671
|
+
_,
|
|
672
|
+
_
|
|
673
|
+
),
|
|
674
|
+
let .polyline(id, _, _): id }
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
var geometryType: String {
|
|
678
|
+
switch self { case let .mapPolygon(_, type, _),
|
|
679
|
+
let .polygon(
|
|
680
|
+
_,
|
|
681
|
+
type,
|
|
682
|
+
_
|
|
683
|
+
),
|
|
684
|
+
let .mapPolyline(_, type, _),
|
|
685
|
+
let .polyline(_, type, _): type }
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
var isVisible: Bool {
|
|
689
|
+
switch self { case let .mapPolygon(_, _, value): value.isShow; case let .polygon(
|
|
690
|
+
_,
|
|
691
|
+
_,
|
|
692
|
+
value
|
|
693
|
+
): value.isShow; case let .mapPolyline(_, _, value): value
|
|
694
|
+
.isShow; case let .polyline(_, _, value): value.isShow }
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
var zOrder: Int {
|
|
698
|
+
switch self {
|
|
699
|
+
case let .mapPolygon(_, _, value): (value.userObject as? NSNumber)?
|
|
700
|
+
.intValue ?? 0; case let .polygon(
|
|
701
|
+
_,
|
|
702
|
+
_,
|
|
703
|
+
value
|
|
704
|
+
): (value.userObject as? NSNumber)?.intValue ?? 0; case let .mapPolyline(_, _, value): (value
|
|
705
|
+
.userObject as? NSNumber)?.intValue ?? 0; case let .polyline(
|
|
706
|
+
_,
|
|
707
|
+
_,
|
|
708
|
+
value
|
|
709
|
+
): (value.userObject as? NSNumber)?.intValue ?? 0
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
var positionPayload: [String: Double]? {
|
|
714
|
+
switch self {
|
|
715
|
+
case let .polygon(_, _, value):
|
|
716
|
+
[
|
|
717
|
+
"latitude": value.position.wgsCoord.latitude,
|
|
718
|
+
"longitude": value.position.wgsCoord.longitude
|
|
719
|
+
]
|
|
720
|
+
case let .polyline(_, _, value):
|
|
721
|
+
[
|
|
722
|
+
"latitude": value.position.wgsCoord.latitude,
|
|
723
|
+
"longitude": value.position.wgsCoord.longitude
|
|
724
|
+
]
|
|
725
|
+
case .mapPolygon,
|
|
726
|
+
.mapPolyline:
|
|
727
|
+
nil
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
var orientationDegrees: Double {
|
|
732
|
+
let radians: Double = switch self {
|
|
733
|
+
case let .mapPolygon(_, _, value): value.orientation
|
|
734
|
+
case let .polygon(_, _, value): value.orientation
|
|
735
|
+
case let .mapPolyline(_, _, value): value.orientation
|
|
736
|
+
case let .polyline(_, _, value): value.orientation
|
|
737
|
+
}
|
|
738
|
+
return radians * 180 / .pi
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
func setVisible(_ visible: Bool) throws {
|
|
742
|
+
switch self {
|
|
743
|
+
case let .mapPolygon(_, _, value): visible ? value.show() : value.hide(); case let .polygon(
|
|
744
|
+
_,
|
|
745
|
+
_,
|
|
746
|
+
value
|
|
747
|
+
): visible ? value.show() : value.hide(); case let .mapPolyline(_, _, value): visible ? value
|
|
748
|
+
.show() : value.hide(); case let .polyline(
|
|
749
|
+
_,
|
|
750
|
+
_,
|
|
751
|
+
value
|
|
752
|
+
): visible ? value.show() : value.hide()
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
func setPosition(_ position: MapPoint) throws {
|
|
757
|
+
switch self { case let .polygon(_, _, value): value.position = position; case let .polyline(
|
|
758
|
+
_,
|
|
759
|
+
_,
|
|
760
|
+
value
|
|
761
|
+
): value
|
|
762
|
+
.position =
|
|
763
|
+
position; default: throw shapeUnsupported(
|
|
764
|
+
"iOS Kakao Maps cannot reposition geographic shapes."
|
|
765
|
+
)
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
func setOrientation(_ degrees: Double) throws {
|
|
770
|
+
guard degrees.isFinite
|
|
771
|
+
else { throw shapeInvalid("orientation must be finite.") }; switch self { case let .mapPolygon(
|
|
772
|
+
_,
|
|
773
|
+
_,
|
|
774
|
+
value
|
|
775
|
+
): value.orientation = degrees * .pi / 180; case let .polygon(_, _, value): value
|
|
776
|
+
.orientation = degrees * .pi / 180; case let .mapPolyline(
|
|
777
|
+
_,
|
|
778
|
+
_,
|
|
779
|
+
value
|
|
780
|
+
): value.orientation = degrees * .pi / 180; case let .polyline(_, _, value): value
|
|
781
|
+
.orientation = degrees * .pi / 180 }
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
func mapStyleNativeID(_ prefix: String, _ spec: [String: Any]) -> String {
|
|
786
|
+
let data = (try? JSONSerialization.data(withJSONObject: spec, options: [.sortedKeys])) ?? Data()
|
|
787
|
+
return "\(prefix)-\(data.count)-\(UInt(bitPattern: data.hashValue))"
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
func mapShapeMapPolygon(_ part: [String: Any]) throws -> MapPolygon {
|
|
791
|
+
let value = try MapPolygon(
|
|
792
|
+
exteriorRing: part.coordinateList("outer"),
|
|
793
|
+
holes: part.optionalCoordinateLists("holes"),
|
|
794
|
+
styleIndex: UInt(part.nonnegativeInt("styleIndex", default: 0))
|
|
795
|
+
)
|
|
796
|
+
value.mergeOverlappedHoles = try part.bool("mergeOverlappedHoles", default: false)
|
|
797
|
+
return value
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
func mapShapeModelPolygon(_ part: [String: Any]) throws -> Polygon {
|
|
801
|
+
let value = try Polygon(
|
|
802
|
+
exteriorRing: part.pointList("outer"),
|
|
803
|
+
holes: part.optionalPointLists("holes"),
|
|
804
|
+
styleIndex: UInt(part.nonnegativeInt("styleIndex", default: 0))
|
|
805
|
+
)
|
|
806
|
+
value.mergeOverlappedHoles = try part.bool("mergeOverlappedHoles", default: false)
|
|
807
|
+
return value
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private func mapPolyline(_ part: [String: Any]) throws -> MapPolyline {
|
|
811
|
+
try MapPolyline(
|
|
812
|
+
line: part.coordinateList("points"),
|
|
813
|
+
styleIndex: UInt(part.nonnegativeInt("styleIndex", default: 0))
|
|
814
|
+
)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
private func modelPolyline(_ part: [String: Any]) throws -> Polyline {
|
|
818
|
+
try Polyline(
|
|
819
|
+
line: part.pointList("points"),
|
|
820
|
+
styleIndex: UInt(part.nonnegativeInt("styleIndex", default: 0))
|
|
821
|
+
)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private func coordinatePayload(_ points: [MapPoint]) -> [[String: Double]] {
|
|
825
|
+
points.map { [
|
|
826
|
+
"latitude": $0.wgsCoord.latitude,
|
|
827
|
+
"longitude": $0.wgsCoord.longitude
|
|
828
|
+
] }
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
private func pointPayload(_ points: [CGPoint]) -> [[String: Double]] {
|
|
832
|
+
points.map { [
|
|
833
|
+
"x": Double($0.x),
|
|
834
|
+
"y": Double($0.y)
|
|
835
|
+
] }
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
private func shapeArcPoints(_ geometry: [String: Any]) throws -> [CGPoint] {
|
|
839
|
+
let radius = try geometry.positiveDouble("radius")
|
|
840
|
+
let count = try geometry.minimumInt("pointCount", default: 64, minimum: 2)
|
|
841
|
+
let startDegrees = try geometry.double("startAngle")
|
|
842
|
+
let endDegrees = try geometry.double("endAngle")
|
|
843
|
+
let clockwise = try geometry.bool("clockwise", default: true)
|
|
844
|
+
let angles = shapeArcAngles(
|
|
845
|
+
start: startDegrees,
|
|
846
|
+
end: endDegrees,
|
|
847
|
+
clockwise: clockwise
|
|
848
|
+
)
|
|
849
|
+
return (0 ..< count).map { index in
|
|
850
|
+
let angle = angles.start + angles.sweep * Double(index) / Double(count - 1)
|
|
851
|
+
return CGPoint(x: radius * sin(angle), y: radius * cos(angle))
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
private func shapeArcAngles(
|
|
856
|
+
start: Double,
|
|
857
|
+
end: Double,
|
|
858
|
+
clockwise: Bool
|
|
859
|
+
) -> (start: Double, sweep: Double) {
|
|
860
|
+
let normalizedStart = shapeNormalizedDegrees(start)
|
|
861
|
+
let normalizedEnd = shapeNormalizedDegrees(end)
|
|
862
|
+
let delta = normalizedEnd - normalizedStart
|
|
863
|
+
let sweep: Double = if delta == 0 {
|
|
864
|
+
start == end ? 0 : (clockwise ? 360 : -360)
|
|
865
|
+
} else if clockwise {
|
|
866
|
+
delta > 0 ? delta : delta + 360
|
|
867
|
+
} else {
|
|
868
|
+
delta < 0 ? delta : delta - 360
|
|
869
|
+
}
|
|
870
|
+
return (normalizedStart * .pi / 180, sweep * .pi / 180)
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
private func shapeNormalizedDegrees(_ value: Double) -> Double {
|
|
874
|
+
let remainder = value.truncatingRemainder(dividingBy: 360)
|
|
875
|
+
return remainder < 0 ? remainder + 360 : remainder
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private func shapeOffset(_ center: MapPoint, east: Double, north: Double) -> MapPoint {
|
|
879
|
+
let distance = sqrt(east * east + north * north)
|
|
880
|
+
let bearing = atan2(east, north)
|
|
881
|
+
let coordinate = center.wgsCoord
|
|
882
|
+
let latitude = coordinate.latitude * .pi / 180
|
|
883
|
+
let longitude = coordinate.longitude * .pi / 180
|
|
884
|
+
let angular = distance / shapeEarthRadius
|
|
885
|
+
let targetLatitude =
|
|
886
|
+
asin(sin(latitude) * cos(angular) + cos(latitude) * sin(angular) * cos(bearing))
|
|
887
|
+
let targetLongitude = longitude + atan2(
|
|
888
|
+
sin(bearing) * sin(angular) * cos(latitude),
|
|
889
|
+
cos(angular) - sin(latitude) * sin(targetLatitude)
|
|
890
|
+
)
|
|
891
|
+
return MapPoint(longitude: targetLongitude * 180 / .pi, latitude: targetLatitude * 180 / .pi)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
private func validateShapeGeometry(_ geometry: [String: Any], styleCount: Int) throws {
|
|
895
|
+
let type = try geometry.string("type")
|
|
896
|
+
guard ["geographicPolygon", "modelPolygon", "geographicPolyline", "modelPolyline"].contains(type)
|
|
897
|
+
else { return }
|
|
898
|
+
let parts = try geometry.objectList("parts")
|
|
899
|
+
guard !parts.isEmpty else { throw shapeInvalid("Shape geometry requires parts.") }
|
|
900
|
+
for part in parts {
|
|
901
|
+
let index = try part.nonnegativeInt("styleIndex", default: 0)
|
|
902
|
+
guard index < styleCount else { throw shapeInvalid("Shape styleIndex is out of range.") }
|
|
903
|
+
let count = try (type.contains("Polygon") ? part.anyList("outer") : part.anyList("points"))
|
|
904
|
+
.count
|
|
905
|
+
let minimum = type.contains("Polygon") ? 3 : 2
|
|
906
|
+
guard count >= minimum
|
|
907
|
+
else { throw shapeInvalid("Shape part requires at least \(minimum) points.") }
|
|
908
|
+
if type == "geographicPolygon" {
|
|
909
|
+
for hole in try part.optionalCoordinateLists("holes") ?? [] {
|
|
910
|
+
guard hole.count >= 3
|
|
911
|
+
else { throw shapeInvalid("Shape polygon holes require at least 3 points.") }
|
|
912
|
+
}
|
|
913
|
+
} else if type == "modelPolygon" {
|
|
914
|
+
for hole in try part.optionalPointLists("holes") ?? [] {
|
|
915
|
+
guard hole.count >= 3
|
|
916
|
+
else { throw shapeInvalid("Shape polygon holes require at least 3 points.") }
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
switch type {
|
|
921
|
+
case "geographicPolygon":
|
|
922
|
+
_ = try parts.map(mapShapeMapPolygon)
|
|
923
|
+
case "modelPolygon":
|
|
924
|
+
_ = try geometry.coordinate("base")
|
|
925
|
+
_ = try geometry.double("orientation", default: 0)
|
|
926
|
+
_ = try parts.map(mapShapeModelPolygon)
|
|
927
|
+
case "geographicPolyline":
|
|
928
|
+
_ = try parts.map(mapPolyline)
|
|
929
|
+
case "modelPolyline":
|
|
930
|
+
_ = try geometry.coordinate("base")
|
|
931
|
+
_ = try geometry.double("orientation", default: 0)
|
|
932
|
+
_ = try parts.map(modelPolyline)
|
|
933
|
+
default:
|
|
934
|
+
break
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private func shapeLayerPass(_ value: String) throws
|
|
939
|
+
-> ShapeLayerPassType
|
|
940
|
+
{
|
|
941
|
+
switch value {
|
|
942
|
+
case "default": .default; case "overlay": .overlay; case "route": .route; default: throw shapeInvalid(
|
|
943
|
+
"Unknown shape layer pass \(value)."
|
|
944
|
+
)
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
private func polylineCap(_ value: String) throws
|
|
949
|
+
-> PolylineCapType
|
|
950
|
+
{
|
|
951
|
+
switch value {
|
|
952
|
+
case "butt": .butt; case "round": .round; case "square": .square; default: throw shapeInvalid(
|
|
953
|
+
"Unknown polyline cap \(value)."
|
|
954
|
+
)
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
private func shapeInterpolation(
|
|
959
|
+
duration: Int,
|
|
960
|
+
method value: String
|
|
961
|
+
) throws -> AnimationInterpolation {
|
|
962
|
+
let method: InterpolationMethodType
|
|
963
|
+
switch value {
|
|
964
|
+
case "linear": method = .linear
|
|
965
|
+
case "cubicIn": method = .cubicIn
|
|
966
|
+
case "cubicOut": method = .cubicOut
|
|
967
|
+
case "cubicInOut": method = .cubicInOut
|
|
968
|
+
default: throw shapeInvalid("Unknown interpolation \(value).")
|
|
969
|
+
}
|
|
970
|
+
return AnimationInterpolation(duration: UInt32(duration), method: method)
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
func mapShapePolygonStyleSet(_ spec: [String: Any]) throws -> PolygonStyleSet {
|
|
974
|
+
try PolygonStyleSet(
|
|
975
|
+
styleSetID: spec.string("id"),
|
|
976
|
+
styles: spec.objectList("styles").map { style in
|
|
977
|
+
let levels = try style.objectList("levels")
|
|
978
|
+
guard !levels.isEmpty else { throw shapeInvalid("Polygon styles require levels.") }
|
|
979
|
+
return try PolygonStyle(styles: levels.map { level in
|
|
980
|
+
try PerLevelPolygonStyle(
|
|
981
|
+
color: level.color("fillColor"),
|
|
982
|
+
strokeWidth: shapePixelUInt(level.nonnegativeInt("strokeWidth", default: 0)),
|
|
983
|
+
strokeColor: level.color("strokeColor", default: .clear),
|
|
984
|
+
level: level.int("level")
|
|
985
|
+
)
|
|
986
|
+
})
|
|
987
|
+
}
|
|
988
|
+
)
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
func mapShapeWaveEffect(_ animation: [String: Any]) throws -> WaveAnimationEffect {
|
|
992
|
+
let frames = try animation.objectList("frames")
|
|
993
|
+
guard !frames.isEmpty else { throw shapeInvalid("Shape wave animation requires frames.") }
|
|
994
|
+
let effect = try WaveAnimationEffect(datas: frames.map { frame in
|
|
995
|
+
try WaveAnimationData(
|
|
996
|
+
startAlpha: Float(frame.unitDouble("startAlpha")),
|
|
997
|
+
endAlpha: Float(frame.unitDouble("endAlpha")),
|
|
998
|
+
startRadius: shapePixelFloat(frame.nonnegativeDouble("startRadius")),
|
|
999
|
+
endRadius: shapePixelFloat(frame.nonnegativeDouble("endRadius")),
|
|
1000
|
+
level: frame.int("level")
|
|
1001
|
+
)
|
|
1002
|
+
})
|
|
1003
|
+
effect.interpolation = try shapeInterpolation(
|
|
1004
|
+
duration: animation.minimumInt("duration", minimum: 1),
|
|
1005
|
+
method: animation.optionalString("interpolation") ?? "linear"
|
|
1006
|
+
)
|
|
1007
|
+
effect.playCount = try UInt(animation.minimumInt("repeat", default: 1, minimum: 1))
|
|
1008
|
+
effect.hideAtStop = try animation.bool("hideAtStop", default: false)
|
|
1009
|
+
return effect
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
private func requireUniqueShape(_ values: [[String: Any]], noun: String) throws {
|
|
1013
|
+
let ids = try values.map { try $0.string("id") }; guard Set(ids).count == ids.count
|
|
1014
|
+
else { throw shapeInvalid("Duplicate \(noun) ID.") }
|
|
1015
|
+
guard !ids.contains(where: \.isEmpty)
|
|
1016
|
+
else { throw shapeInvalid("\(noun) ID must not be empty.") }
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
private let defaultShapeLayerZIndex = 10000
|
|
1020
|
+
|
|
1021
|
+
private func shapeInvalid(_ message: String) -> KakaoMapException {
|
|
1022
|
+
KakaoMapException(
|
|
1023
|
+
"ERR_KAKAO_MAP_SHAPE_INVALID_ARGUMENT",
|
|
1024
|
+
message
|
|
1025
|
+
)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
private func shapeUnsupported(_ message: String) -> KakaoMapException {
|
|
1029
|
+
KakaoMapException(
|
|
1030
|
+
"ERR_KAKAO_MAP_SHAPE_UNSUPPORTED",
|
|
1031
|
+
message
|
|
1032
|
+
)
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
private func shapeNotFound(_ noun: String, id: String) -> KakaoMapException {
|
|
1036
|
+
KakaoMapException(
|
|
1037
|
+
"ERR_KAKAO_MAP_SHAPE_NOT_FOUND",
|
|
1038
|
+
"Unknown \(noun) \(id)."
|
|
1039
|
+
)
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
private extension [String: Any] {
|
|
1043
|
+
func object(_ key: String) throws
|
|
1044
|
+
-> [String: Any]
|
|
1045
|
+
{
|
|
1046
|
+
guard let value = self[key] as? [String: Any]
|
|
1047
|
+
else { throw shapeInvalid("\(key) must be an object.") }; return value
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
func objectList(_ key: String) throws -> [[String: Any]] {
|
|
1051
|
+
guard let raw = self[key]
|
|
1052
|
+
else { return [] }; guard let value = raw as? [[String: Any]]
|
|
1053
|
+
else { throw shapeInvalid("\(key) must be an array of objects.") }; return value
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
func anyList(_ key: String) throws -> [Any] {
|
|
1057
|
+
guard let value = self[key] as? [Any]
|
|
1058
|
+
else { throw shapeInvalid("\(key) must be an array.") }; return value
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
func string(_ key: String) throws -> String {
|
|
1062
|
+
guard let value = self[key] as? String
|
|
1063
|
+
else { throw shapeInvalid("\(key) must be a string.") }; return value
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
func optionalString(_ key: String) throws -> String? {
|
|
1067
|
+
guard let raw = self[key] else { return nil }
|
|
1068
|
+
guard let value = raw as? String
|
|
1069
|
+
else { throw shapeInvalid("\(key) must be a string.") }
|
|
1070
|
+
return value
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
func stringList(_ key: String) throws -> [String] {
|
|
1074
|
+
guard let value = self[key] as? [String]
|
|
1075
|
+
else { throw shapeInvalid("\(key) must be an array of strings.") }; return value
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
func bool(
|
|
1079
|
+
_ key: String,
|
|
1080
|
+
default fallback: Bool? = nil
|
|
1081
|
+
) throws
|
|
1082
|
+
-> Bool
|
|
1083
|
+
{
|
|
1084
|
+
guard let raw = self[key] else {
|
|
1085
|
+
guard let fallback else { throw shapeInvalid("\(key) must be a boolean.") }
|
|
1086
|
+
return fallback
|
|
1087
|
+
}
|
|
1088
|
+
guard CFGetTypeID(raw as CFTypeRef) == CFBooleanGetTypeID(),
|
|
1089
|
+
let value = raw as? Bool
|
|
1090
|
+
else { throw shapeInvalid("\(key) must be a boolean.") }
|
|
1091
|
+
return value
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
func double(
|
|
1095
|
+
_ key: String,
|
|
1096
|
+
default fallback: Double? = nil
|
|
1097
|
+
) throws -> Double {
|
|
1098
|
+
guard let raw = self[key] else {
|
|
1099
|
+
guard let fallback else { throw shapeInvalid("\(key) must be finite.") }
|
|
1100
|
+
return fallback
|
|
1101
|
+
}
|
|
1102
|
+
guard let value = raw as? NSNumber,
|
|
1103
|
+
CFGetTypeID(value) != CFBooleanGetTypeID(),
|
|
1104
|
+
value.doubleValue.isFinite
|
|
1105
|
+
else { throw shapeInvalid("\(key) must be finite.") }
|
|
1106
|
+
return value.doubleValue
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
func int(_ key: String, default fallback: Int? = nil) throws -> Int {
|
|
1110
|
+
let value = try double(
|
|
1111
|
+
key,
|
|
1112
|
+
default: fallback.map(Double.init)
|
|
1113
|
+
); guard let integer = Int(exactly: value)
|
|
1114
|
+
else { throw shapeInvalid("\(key) must be an integer.") }; return integer
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
func nonnegativeInt(
|
|
1118
|
+
_ key: String,
|
|
1119
|
+
default fallback: Int? = nil
|
|
1120
|
+
) throws -> Int {
|
|
1121
|
+
let value = try int(
|
|
1122
|
+
key,
|
|
1123
|
+
default: fallback
|
|
1124
|
+
); guard value >= 0 else { throw shapeInvalid("\(key) must be non-negative.") }; return value
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
func minimumInt(
|
|
1128
|
+
_ key: String,
|
|
1129
|
+
default fallback: Int? = nil,
|
|
1130
|
+
minimum: Int
|
|
1131
|
+
) throws -> Int {
|
|
1132
|
+
let value = try int(
|
|
1133
|
+
key,
|
|
1134
|
+
default: fallback
|
|
1135
|
+
); guard value >= minimum
|
|
1136
|
+
else { throw shapeInvalid("\(key) must be at least \(minimum).") }; return value
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
func positiveDouble(_ key: String) throws -> Double {
|
|
1140
|
+
let value = try double(key); guard value >
|
|
1141
|
+
0 else { throw shapeInvalid("\(key) must be positive.") }; return value
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
func nonnegativeDouble(_ key: String) throws -> Double {
|
|
1145
|
+
let value = try double(
|
|
1146
|
+
key
|
|
1147
|
+
); guard value >=
|
|
1148
|
+
0 else { throw shapeInvalid("\(key) must be non-negative.") }; return value
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
func unitDouble(_ key: String) throws -> Double {
|
|
1152
|
+
let value = try double(key); guard (0 ... 1)
|
|
1153
|
+
.contains(value) else { throw shapeInvalid("\(key) must be between 0 and 1.") }; return value
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
func color(
|
|
1157
|
+
_ key: String,
|
|
1158
|
+
default fallback: UIColor? = nil
|
|
1159
|
+
) throws
|
|
1160
|
+
-> UIColor
|
|
1161
|
+
{
|
|
1162
|
+
guard let raw = self[key] else {
|
|
1163
|
+
guard let fallback else { throw shapeInvalid("\(key) must be a color.") }
|
|
1164
|
+
return fallback
|
|
1165
|
+
}
|
|
1166
|
+
guard let value = raw as? String
|
|
1167
|
+
else { throw shapeInvalid("\(key) must be a color.") }
|
|
1168
|
+
return try parseShapeColor(value)
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
func coordinate(_ key: String) throws -> MapPoint {
|
|
1172
|
+
try object(key).coordinateValue()
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
func coordinateValue() throws
|
|
1176
|
+
-> MapPoint
|
|
1177
|
+
{
|
|
1178
|
+
let latitude = try double("latitude"); let longitude =
|
|
1179
|
+
try double("longitude"); guard (-90 ... 90).contains(latitude),
|
|
1180
|
+
(-180 ... 180).contains(longitude)
|
|
1181
|
+
else { throw shapeInvalid("Invalid coordinate.") }; return MapPoint(
|
|
1182
|
+
longitude: longitude,
|
|
1183
|
+
latitude: latitude
|
|
1184
|
+
)
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
func coordinateList(_ key: String) throws -> [MapPoint] {
|
|
1188
|
+
try anyList(key)
|
|
1189
|
+
.map {
|
|
1190
|
+
guard let value = $0 as? [String: Any]
|
|
1191
|
+
else { throw shapeInvalid("Coordinate must be an object.") }; return try value
|
|
1192
|
+
.coordinateValue()
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
func optionalCoordinateLists(_ key: String) throws -> [[MapPoint]]? {
|
|
1197
|
+
guard let raw = self[key]
|
|
1198
|
+
else { return nil }; guard let values = raw as? [[Any]]
|
|
1199
|
+
else { throw shapeInvalid("\(key) must be an array of arrays.") }; return try values
|
|
1200
|
+
.map {
|
|
1201
|
+
try $0
|
|
1202
|
+
.map {
|
|
1203
|
+
guard let value = $0 as? [String: Any]
|
|
1204
|
+
else { throw shapeInvalid("Coordinate must be an object.") }; return try value
|
|
1205
|
+
.coordinateValue()
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
func pointList(_ key: String) throws -> [CGPoint] {
|
|
1211
|
+
try anyList(key)
|
|
1212
|
+
.map {
|
|
1213
|
+
guard let value = $0 as? [String: Any]
|
|
1214
|
+
else { throw shapeInvalid("Point must be an object.") }
|
|
1215
|
+
return try shapeModelMetersToSDKPoint(
|
|
1216
|
+
x: value.double("x"),
|
|
1217
|
+
y: value.double("y")
|
|
1218
|
+
)
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
func optionalPointLists(_ key: String) throws -> [[CGPoint]]? {
|
|
1223
|
+
guard let raw = self[key]
|
|
1224
|
+
else { return nil }; guard let values = raw as? [[Any]]
|
|
1225
|
+
else { throw shapeInvalid("\(key) must be an array of arrays.") }; return try values
|
|
1226
|
+
.map {
|
|
1227
|
+
try $0
|
|
1228
|
+
.map {
|
|
1229
|
+
guard let value = $0 as? [String: Any]
|
|
1230
|
+
else { throw shapeInvalid("Point must be an object.") }
|
|
1231
|
+
return try shapeModelMetersToSDKPoint(
|
|
1232
|
+
x: value.double("x"),
|
|
1233
|
+
y: value.double("y")
|
|
1234
|
+
)
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
private let shapeEarthRadius = 6378137.0
|
|
1241
|
+
private let shapeModelDegreesPerMeter = 180 / (.pi * shapeEarthRadius)
|
|
1242
|
+
private let shapeModelPointConversionMessage = "Model point conversion must be finite."
|
|
1243
|
+
|
|
1244
|
+
private func shapeModelMetersToSDKPoint(x: Double, y: Double) throws -> CGPoint {
|
|
1245
|
+
let sdkX = x * shapeModelDegreesPerMeter
|
|
1246
|
+
let sdkY = y * shapeModelDegreesPerMeter
|
|
1247
|
+
guard sdkX.isFinite, sdkY.isFinite else {
|
|
1248
|
+
throw shapeInvalid(shapeModelPointConversionMessage)
|
|
1249
|
+
}
|
|
1250
|
+
return CGPoint(x: sdkX, y: sdkY)
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
private func shapeSDKModelPointToMeters(_ point: CGPoint) throws -> CGPoint {
|
|
1254
|
+
let x = Double(point.x) / shapeModelDegreesPerMeter
|
|
1255
|
+
let y = Double(point.y) / shapeModelDegreesPerMeter
|
|
1256
|
+
guard x.isFinite, y.isFinite else {
|
|
1257
|
+
throw shapeInvalid(shapeModelPointConversionMessage)
|
|
1258
|
+
}
|
|
1259
|
+
return CGPoint(x: x, y: y)
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
private func shapePixels(_ points: Double) throws -> Double {
|
|
1263
|
+
let pixels = MapUnits.pixels(points)
|
|
1264
|
+
guard pixels.isFinite else { throw shapeInvalid("Pixel dimensions must be finite.") }
|
|
1265
|
+
return pixels
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
private func shapePixelUInt(_ points: Int) throws -> UInt {
|
|
1269
|
+
let pixels = try shapePixels(Double(points)).rounded()
|
|
1270
|
+
guard let integer = UInt(exactly: pixels)
|
|
1271
|
+
else { throw shapeInvalid("Pixel dimensions are out of range.") }
|
|
1272
|
+
return integer
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
private func shapePixelFloat(_ points: Double) throws -> Float {
|
|
1276
|
+
let pixels = try shapePixels(points)
|
|
1277
|
+
guard abs(pixels) <= Double(Float.greatestFiniteMagnitude)
|
|
1278
|
+
else { throw shapeInvalid("Pixel dimensions are out of range.") }
|
|
1279
|
+
return Float(pixels)
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
private func parseShapeColor(_ value: String) throws -> UIColor {
|
|
1283
|
+
guard value.range(of: "^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$", options: .regularExpression) != nil
|
|
1284
|
+
else { throw shapeInvalid("Colors must be #RRGGBB or #RRGGBBAA.") }
|
|
1285
|
+
let raw = String(value.dropFirst())
|
|
1286
|
+
let rgb = UInt64(raw.prefix(6), radix: 16) ?? 0
|
|
1287
|
+
let alpha = raw.count == 8 ? UInt64(raw.suffix(2), radix: 16) ?? 255 : 255
|
|
1288
|
+
return UIColor(
|
|
1289
|
+
red: CGFloat((rgb >> 16) & 255) / 255,
|
|
1290
|
+
green: CGFloat((rgb >> 8) & 255) / 255,
|
|
1291
|
+
blue: CGFloat(rgb & 255) / 255,
|
|
1292
|
+
alpha: CGFloat(alpha) / 255
|
|
1293
|
+
)
|
|
1294
|
+
}
|