@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,805 @@
|
|
|
1
|
+
package net.mjstudio.rnkakao.map
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import com.kakao.vectormap.CurveType
|
|
6
|
+
import com.kakao.vectormap.KakaoMap
|
|
7
|
+
import com.kakao.vectormap.LatLng
|
|
8
|
+
import com.kakao.vectormap.animation.Interpolation
|
|
9
|
+
import com.kakao.vectormap.route.RouteLine
|
|
10
|
+
import com.kakao.vectormap.route.RouteLineAnimator
|
|
11
|
+
import com.kakao.vectormap.route.RouteLineLayer
|
|
12
|
+
import com.kakao.vectormap.route.RouteLineOptions
|
|
13
|
+
import com.kakao.vectormap.route.RouteLinePattern
|
|
14
|
+
import com.kakao.vectormap.route.RouteLineSegment
|
|
15
|
+
import com.kakao.vectormap.route.RouteLineStyle
|
|
16
|
+
import com.kakao.vectormap.route.RouteLineStyles
|
|
17
|
+
import com.kakao.vectormap.route.RouteLineStylesSet
|
|
18
|
+
import com.kakao.vectormap.route.animation.ProgressAnimation
|
|
19
|
+
import com.kakao.vectormap.route.animation.ProgressDirection
|
|
20
|
+
import com.kakao.vectormap.route.animation.ProgressType
|
|
21
|
+
import expo.modules.kotlin.exception.CodedException
|
|
22
|
+
import java.lang.ref.WeakReference
|
|
23
|
+
|
|
24
|
+
internal class MapRoutes(
|
|
25
|
+
map: KakaoMap,
|
|
26
|
+
density: Float,
|
|
27
|
+
private val imageLoader: MapImageLoader,
|
|
28
|
+
private val emit: (String, Map<String, Any>) -> Unit,
|
|
29
|
+
) {
|
|
30
|
+
private val manager = map.routeLineManager ?: invalidRoute("Route manager is unavailable.")
|
|
31
|
+
private val density = density.takeIf { it.isFinite() && it > 0 } ?: invalidRoute("Display density must be positive and finite.")
|
|
32
|
+
private val layers = mutableMapOf<String, RouteLineLayer>()
|
|
33
|
+
private val routes = mutableMapOf<String, RouteLine>()
|
|
34
|
+
private val styleSets = mutableMapOf<String, RouteLineStylesSet>()
|
|
35
|
+
private val styleCounts = mutableMapOf<String, Int>()
|
|
36
|
+
private val animators = mutableMapOf<String, RouteLineAnimator>()
|
|
37
|
+
private val animationTargets = mutableMapOf<String, Set<String>>()
|
|
38
|
+
private val animationNativeIds = mutableMapOf<String, String>()
|
|
39
|
+
private val animationRunEpochs = mutableMapOf<String, Long>()
|
|
40
|
+
private val animatorIdsByDefinition = mutableMapOf<String, String>()
|
|
41
|
+
private val animatorCache = mutableMapOf<String, RouteLineAnimator>()
|
|
42
|
+
private val animatorTargetIds = mutableMapOf<String, Set<String>>()
|
|
43
|
+
private val animatorsNeedingTargets = mutableSetOf<String>()
|
|
44
|
+
private val requestedProgress = mutableMapOf<String, Float>()
|
|
45
|
+
private val progressEpochs = mutableMapOf<String, Long>()
|
|
46
|
+
private val routeLayerIds = mutableMapOf<String, String>()
|
|
47
|
+
private val pendingImages = mutableSetOf<MapImageLoadToken>()
|
|
48
|
+
private var pendingApply: ((Result<Map<String, Any>>) -> Unit)? = null
|
|
49
|
+
private var generation = 0
|
|
50
|
+
private var animationSequence = 0
|
|
51
|
+
private var animationRunSequence = 0L
|
|
52
|
+
private var sceneSequence = 0
|
|
53
|
+
private var replacementSequence = 0
|
|
54
|
+
private val nativePrefix = "rnk-routes-${System.identityHashCode(this)}"
|
|
55
|
+
|
|
56
|
+
fun apply(
|
|
57
|
+
scene: Map<String, Any?>,
|
|
58
|
+
completion: (Result<Map<String, Any>>) -> Unit,
|
|
59
|
+
) {
|
|
60
|
+
rejectPendingApply("${ROUTE_ERROR}_SUPERSEDED", "The route scene was superseded by a newer update.")
|
|
61
|
+
pendingImages.forEach(MapImageLoadToken::cancel)
|
|
62
|
+
pendingImages.clear()
|
|
63
|
+
val token = ++generation
|
|
64
|
+
val styleSpecs = scene.list("styleSets")
|
|
65
|
+
val layerSpecs = scene.list("layers")
|
|
66
|
+
val routeSpecs = scene.list("routes")
|
|
67
|
+
validateScene(styleSpecs, layerSpecs, routeSpecs)
|
|
68
|
+
pendingApply = completion
|
|
69
|
+
val uris =
|
|
70
|
+
styleSpecs
|
|
71
|
+
.flatMap { it.list("patterns") }
|
|
72
|
+
.flatMap { pattern ->
|
|
73
|
+
listOfNotNull(pattern.map("image").string("uri"), pattern.mapOrNull("symbol")?.string("uri"))
|
|
74
|
+
}.toSet()
|
|
75
|
+
if (uris.isEmpty()) {
|
|
76
|
+
val result = runCatching { buildScene(styleSpecs, layerSpecs, routeSpecs, emptyMap()) }
|
|
77
|
+
pendingApply = null
|
|
78
|
+
completion(result)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
val images = mutableMapOf<String, Bitmap>()
|
|
82
|
+
val requests = mutableListOf<MapImageLoadToken>()
|
|
83
|
+
var isCompleted = false
|
|
84
|
+
val managerReference = WeakReference(this)
|
|
85
|
+
|
|
86
|
+
fun finish(result: Result<Map<String, Any>>) {
|
|
87
|
+
val owner = managerReference.get() ?: return
|
|
88
|
+
if (isCompleted || token != owner.generation) return
|
|
89
|
+
isCompleted = true
|
|
90
|
+
if (result.isFailure) requests.forEach(MapImageLoadToken::cancel)
|
|
91
|
+
requests.forEach(owner.pendingImages::remove)
|
|
92
|
+
owner.pendingApply = null
|
|
93
|
+
completion(result)
|
|
94
|
+
}
|
|
95
|
+
uris.forEach { uri ->
|
|
96
|
+
val request =
|
|
97
|
+
imageLoader.load(uri) { result ->
|
|
98
|
+
result.fold(
|
|
99
|
+
onSuccess = { bitmap ->
|
|
100
|
+
images[uri] = bitmap
|
|
101
|
+
if (images.size == uris.size) {
|
|
102
|
+
managerReference.get()?.let { owner ->
|
|
103
|
+
finish(runCatching { owner.buildScene(styleSpecs, layerSpecs, routeSpecs, images) })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
onFailure = { error -> finish(Result.failure(error)) },
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
requests += request
|
|
111
|
+
pendingImages += request
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fun command(command: Map<String, Any?>): Map<String, Any?> =
|
|
116
|
+
when (command.string("type")) {
|
|
117
|
+
"setVisible" -> {
|
|
118
|
+
val targets = routeEntries(command.stringList("ids"))
|
|
119
|
+
val isVisible = command.boolean("visible")
|
|
120
|
+
targets.forEach { if (isVisible) it.show() else it.hide() }
|
|
121
|
+
none()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
"setZIndex" -> {
|
|
125
|
+
route(command.string("id")).setZOrder(command.int("zIndex"))
|
|
126
|
+
none()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
"changeData" -> {
|
|
130
|
+
val id = command.string("id")
|
|
131
|
+
val value = route(id)
|
|
132
|
+
val styleId = command.string("styleSetId")
|
|
133
|
+
val style = style(styleId)
|
|
134
|
+
val styleCount = styleCounts[styleId] ?: unknownRoute("route style set", styleId)
|
|
135
|
+
val segments = command.list("segments")
|
|
136
|
+
validateSegments(segments, styleCount)
|
|
137
|
+
invalidateProgressRequest(id)
|
|
138
|
+
val layer = value.layer ?: invalidRoute("Route $id has no layer.")
|
|
139
|
+
val isVisible = value.isShow
|
|
140
|
+
val options =
|
|
141
|
+
RouteLineOptions
|
|
142
|
+
.from(
|
|
143
|
+
"$nativePrefix-replacement-${replacementSequence++}",
|
|
144
|
+
segments.map { routeSegment(it, style) },
|
|
145
|
+
).setStylesSet(style)
|
|
146
|
+
.setZOrder(value.zOrder)
|
|
147
|
+
.setVisible(false)
|
|
148
|
+
val replacement = layer.addRouteLine(options) ?: invalidRoute("Could not replace route $id.")
|
|
149
|
+
replacement.setProgress(value.progress)
|
|
150
|
+
stopAnimationsForRoute(id)
|
|
151
|
+
invalidateCachedAnimationsForRoute(id)
|
|
152
|
+
value.remove()
|
|
153
|
+
if (isVisible) replacement.show()
|
|
154
|
+
routes[id] = replacement
|
|
155
|
+
none()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
"setProgress" -> {
|
|
159
|
+
val requested = command.double("progress")
|
|
160
|
+
val value = signedProgress(requested, command.string("mode"))
|
|
161
|
+
val id = command.string("id")
|
|
162
|
+
val target = route(id)
|
|
163
|
+
val duration = command.int("duration", 0)
|
|
164
|
+
if (duration < 0) invalidRoute("duration must be non-negative.")
|
|
165
|
+
requestedProgress[id] = requested.toFloat()
|
|
166
|
+
val sceneGeneration = generation
|
|
167
|
+
val requestEpoch = nextProgressEpoch(id)
|
|
168
|
+
if (duration == 0) {
|
|
169
|
+
target.setProgress(value)
|
|
170
|
+
emitRequestedProgress(id, requested, sceneGeneration, requestEpoch, target)
|
|
171
|
+
} else {
|
|
172
|
+
val managerReference = WeakReference(this)
|
|
173
|
+
target.progressTo(value, duration) { completedRoute, _ ->
|
|
174
|
+
managerReference.get()?.let { owner ->
|
|
175
|
+
if (completedRoute === target) {
|
|
176
|
+
owner.emitRequestedProgress(id, requested, sceneGeneration, requestEpoch, target)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
none()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
"getProgress" -> {
|
|
185
|
+
mapOf("type" to "progress", "progress" to route(command.string("id")).progress, "source" to "native")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
"getRequestedProgress" -> {
|
|
189
|
+
val id = command.string("id")
|
|
190
|
+
route(id)
|
|
191
|
+
mapOf("type" to "progress", "progress" to (requestedProgress[id] ?: 0f), "source" to "requested")
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
"getProgressAtCoordinate" -> {
|
|
195
|
+
mapOf(
|
|
196
|
+
"type" to "progress",
|
|
197
|
+
"progress" to route(command.string("id")).getProgressFromPoint(command.coordinate("coordinate")),
|
|
198
|
+
"source" to "native",
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
"getCoordinateAtProgress" -> {
|
|
203
|
+
val point =
|
|
204
|
+
route(command.string("id")).getPointFromProgress(command.progress())
|
|
205
|
+
?: invalidRoute("The SDK could not resolve this route progress.")
|
|
206
|
+
mapOf(
|
|
207
|
+
"type" to "coordinate",
|
|
208
|
+
"latitude" to point.point.latitude,
|
|
209
|
+
"longitude" to point.point.longitude,
|
|
210
|
+
"orientation" to point.orientation.toDouble(),
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
"getState" -> {
|
|
215
|
+
state(command.string("id"))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
"getCounts" -> {
|
|
219
|
+
counts()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
"getCapabilities" -> {
|
|
223
|
+
capabilities() + ("type" to "capabilities")
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
"startProgressAnimation" -> {
|
|
227
|
+
startAnimation(command.map("animation"))
|
|
228
|
+
none()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
"stopAnimation" -> {
|
|
232
|
+
stopAnimation(command.string("animationId"), true)
|
|
233
|
+
none()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
"remove" -> {
|
|
237
|
+
val ids = command.stringList("ids")
|
|
238
|
+
routeEntries(ids)
|
|
239
|
+
ids.forEach(::remove)
|
|
240
|
+
none()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
else -> {
|
|
244
|
+
invalidRoute("Unknown route command type.")
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fun dispose() {
|
|
249
|
+
generation++
|
|
250
|
+
rejectPendingApply("${ROUTE_ERROR}_DISPOSED", "The map view was disposed while applying the route scene.")
|
|
251
|
+
pendingImages.forEach(MapImageLoadToken::cancel)
|
|
252
|
+
pendingImages.clear()
|
|
253
|
+
disposeObjects()
|
|
254
|
+
styleSets.clear()
|
|
255
|
+
styleCounts.clear()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private fun rejectPendingApply(
|
|
259
|
+
code: String,
|
|
260
|
+
message: String,
|
|
261
|
+
) {
|
|
262
|
+
val completion = pendingApply
|
|
263
|
+
pendingApply = null
|
|
264
|
+
completion?.invoke(Result.failure(CodedException(code, message, null)))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
fun capabilities() =
|
|
268
|
+
mapOf(
|
|
269
|
+
"curves" to true,
|
|
270
|
+
"patterns" to true,
|
|
271
|
+
"patternSymbols" to true,
|
|
272
|
+
"patternImageSizing" to false,
|
|
273
|
+
"progressAtCoordinate" to true,
|
|
274
|
+
"coordinateAtProgress" to true,
|
|
275
|
+
"nativeProgressRead" to true,
|
|
276
|
+
"fractionalStyleWidth" to true,
|
|
277
|
+
"progressAnimationRepeat" to false,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
private fun validateScene(
|
|
281
|
+
styleSpecs: List<Map<String, Any?>>,
|
|
282
|
+
layerSpecs: List<Map<String, Any?>>,
|
|
283
|
+
routeSpecs: List<Map<String, Any?>>,
|
|
284
|
+
) {
|
|
285
|
+
requireUniqueRoute(styleSpecs, "style set")
|
|
286
|
+
requireUniqueRoute(layerSpecs, "route layer")
|
|
287
|
+
requireUniqueRoute(routeSpecs, "route")
|
|
288
|
+
layerSpecs.forEach { layer ->
|
|
289
|
+
layer.int("zIndex", 0)
|
|
290
|
+
layer.boolean("visible", true)
|
|
291
|
+
}
|
|
292
|
+
val styles = styleSpecs.associateBy { it.string("id") }
|
|
293
|
+
val layerIds = layerSpecs.mapTo(mutableSetOf()) { it.string("id") }
|
|
294
|
+
routeSpecs.forEach { route ->
|
|
295
|
+
route.int("zIndex", 0)
|
|
296
|
+
route.boolean("visible", true)
|
|
297
|
+
if (route.string("layerId") !in layerIds) invalidRoute("Route ${route.string("id")} references an unknown layer.")
|
|
298
|
+
val styleSpec = styles[route.string("styleSetId")] ?: invalidRoute("Route ${route.string("id")} references an unknown style set.")
|
|
299
|
+
val styleCount = styleSpec.list("styles").size
|
|
300
|
+
route.list("segments").forEach { segment ->
|
|
301
|
+
if (segment.coordinateList("coordinates").size < 2) invalidRoute("Route segments require at least two coordinates.")
|
|
302
|
+
if (segment.int("styleIndex", 0) !in 0 until styleCount) invalidRoute("Route segment styleIndex is out of range.")
|
|
303
|
+
routeCurve(segment)
|
|
304
|
+
}
|
|
305
|
+
if (route.list("segments").isEmpty()) invalidRoute("Routes require at least one segment.")
|
|
306
|
+
}
|
|
307
|
+
styleSpecs.forEach { spec ->
|
|
308
|
+
val patterns = spec.list("patterns")
|
|
309
|
+
val patternCount = patterns.size
|
|
310
|
+
patterns.forEach { pattern ->
|
|
311
|
+
val image = pattern.map("image")
|
|
312
|
+
if (image.string("uri").isBlank()) invalidRoute("Route pattern image URI must not be empty.")
|
|
313
|
+
rejectRouteImageSizing(image)
|
|
314
|
+
pattern.mapOrNull("symbol")?.let {
|
|
315
|
+
if (it.string("uri").isBlank()) invalidRoute("Route symbol image URI must not be empty.")
|
|
316
|
+
rejectRouteImageSizing(it)
|
|
317
|
+
}
|
|
318
|
+
if (pattern.float("distance") <= 0) invalidRoute("Route pattern distance must be positive.")
|
|
319
|
+
}
|
|
320
|
+
val styles = spec.list("styles")
|
|
321
|
+
if (styles.isEmpty()) invalidRoute("Route style sets require styles.")
|
|
322
|
+
styles.forEach { style -> if (style.list("levels").isEmpty()) invalidRoute("Route styles require levels.") }
|
|
323
|
+
styles.flatMap { it.list("levels") }.forEach { level ->
|
|
324
|
+
val index = level.int("patternIndex", -1)
|
|
325
|
+
if (index !in -1 until patternCount) invalidRoute("Route patternIndex is out of range.")
|
|
326
|
+
if (level.float("width") < 0 || level.float("strokeWidth", 0f) < 0) invalidRoute("Route widths must be non-negative.")
|
|
327
|
+
level.int("level")
|
|
328
|
+
level.color("color")
|
|
329
|
+
level.color("strokeColor", Color.TRANSPARENT)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private fun buildScene(
|
|
335
|
+
styleSpecs: List<Map<String, Any?>>,
|
|
336
|
+
layerSpecs: List<Map<String, Any?>>,
|
|
337
|
+
routeSpecs: List<Map<String, Any?>>,
|
|
338
|
+
images: Map<String, Bitmap>,
|
|
339
|
+
): Map<String, Any> {
|
|
340
|
+
val nextStyleSets = mutableMapOf<String, RouteLineStylesSet>()
|
|
341
|
+
val nextStyleCounts = mutableMapOf<String, Int>()
|
|
342
|
+
val nextLayers = mutableMapOf<String, RouteLineLayer>()
|
|
343
|
+
val nextRoutes = mutableMapOf<String, RouteLine>()
|
|
344
|
+
val nextRouteLayerIds = mutableMapOf<String, String>()
|
|
345
|
+
registerStyles(styleSpecs, images, nextStyleSets, nextStyleCounts)
|
|
346
|
+
val sceneId = sceneSequence++
|
|
347
|
+
val scenePrefix = "$nativePrefix-scene-$sceneId"
|
|
348
|
+
try {
|
|
349
|
+
layerSpecs.forEach { spec ->
|
|
350
|
+
val id = spec.string("id")
|
|
351
|
+
val nativeId = "$scenePrefix-layer-$id"
|
|
352
|
+
val layer = manager.addLayer(nativeId, spec.int("zIndex", 0)) ?: invalidRoute("Could not create route layer $id.")
|
|
353
|
+
layer.setVisible(false)
|
|
354
|
+
nextLayers[id] = layer
|
|
355
|
+
}
|
|
356
|
+
routeSpecs.forEach { spec ->
|
|
357
|
+
val id = spec.string("id")
|
|
358
|
+
val styleId = spec.string("styleSetId")
|
|
359
|
+
val styleSet = nextStyleSets[styleId] ?: unknownRoute("route style set", styleId)
|
|
360
|
+
val options =
|
|
361
|
+
RouteLineOptions
|
|
362
|
+
.from(id, spec.list("segments").map { routeSegment(it, styleSet) })
|
|
363
|
+
.setStylesSet(styleSet)
|
|
364
|
+
.setZOrder(spec.int("zIndex", 0))
|
|
365
|
+
.setVisible(spec.boolean("visible", true))
|
|
366
|
+
val layerId = spec.string("layerId")
|
|
367
|
+
nextRoutes[id] = nextLayers[layerId]?.addRouteLine(options) ?: invalidRoute("Could not create route $id.")
|
|
368
|
+
nextRouteLayerIds[id] = layerId
|
|
369
|
+
}
|
|
370
|
+
} catch (error: Throwable) {
|
|
371
|
+
nextLayers.values.forEach(manager::remove)
|
|
372
|
+
throw error
|
|
373
|
+
}
|
|
374
|
+
disposeObjects()
|
|
375
|
+
styleSets.clear()
|
|
376
|
+
styleSets.putAll(nextStyleSets)
|
|
377
|
+
styleCounts.clear()
|
|
378
|
+
styleCounts.putAll(nextStyleCounts)
|
|
379
|
+
layers.putAll(nextLayers)
|
|
380
|
+
routes.putAll(nextRoutes)
|
|
381
|
+
routeLayerIds.putAll(nextRouteLayerIds)
|
|
382
|
+
requestedProgress.putAll(nextRoutes.keys.associateWith { 0f })
|
|
383
|
+
layerSpecs.forEach { spec -> nextLayers.getValue(spec.string("id")).setVisible(spec.boolean("visible", true)) }
|
|
384
|
+
return counts()
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private fun registerStyles(
|
|
388
|
+
specs: List<Map<String, Any?>>,
|
|
389
|
+
images: Map<String, Bitmap>,
|
|
390
|
+
targetStyleSets: MutableMap<String, RouteLineStylesSet>,
|
|
391
|
+
targetStyleCounts: MutableMap<String, Int>,
|
|
392
|
+
) {
|
|
393
|
+
specs.forEach { spec ->
|
|
394
|
+
val id = spec.string("id")
|
|
395
|
+
val nativeId = mapStyleNativeID("rnk-route-style", spec)
|
|
396
|
+
val patterns =
|
|
397
|
+
spec.list("patterns").map { pattern ->
|
|
398
|
+
val bitmap = images[pattern.map("image").string("uri")] ?: invalidRoute("Missing route pattern image.")
|
|
399
|
+
val symbol = pattern.mapOrNull("symbol")?.let { images[it.string("uri")] ?: invalidRoute("Missing route symbol image.") }
|
|
400
|
+
(
|
|
401
|
+
if (symbol ==
|
|
402
|
+
null
|
|
403
|
+
) {
|
|
404
|
+
RouteLinePattern.from(bitmap, pattern.float("distance") * density)
|
|
405
|
+
} else {
|
|
406
|
+
RouteLinePattern.from(bitmap, symbol, pattern.float("distance") * density)
|
|
407
|
+
}
|
|
408
|
+
).setPinStart(pattern.boolean("pinStart", false))
|
|
409
|
+
.setPinEnd(pattern.boolean("pinEnd", false))
|
|
410
|
+
}
|
|
411
|
+
val styles =
|
|
412
|
+
spec.list("styles").map { style ->
|
|
413
|
+
RouteLineStyles.from(
|
|
414
|
+
style.list("levels").map { level ->
|
|
415
|
+
val patternIndex = level.int("patternIndex", -1)
|
|
416
|
+
val width = level.float("width") * density
|
|
417
|
+
val color = level.color("color")
|
|
418
|
+
val strokeWidth = level.float("strokeWidth", 0f) * density
|
|
419
|
+
val strokeColor = level.color("strokeColor", Color.TRANSPARENT)
|
|
420
|
+
val pattern = patterns.getOrNull(patternIndex)
|
|
421
|
+
val routeStyle =
|
|
422
|
+
if (pattern == null) {
|
|
423
|
+
RouteLineStyle.from(width, color, strokeWidth, strokeColor)
|
|
424
|
+
} else {
|
|
425
|
+
RouteLineStyle.from(width, color, strokeWidth, strokeColor, pattern)
|
|
426
|
+
}
|
|
427
|
+
routeStyle.setZoomLevel(level.int("level"))
|
|
428
|
+
},
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
targetStyleSets[id] = manager.addStylesSet(RouteLineStylesSet.from(nativeId, styles, patterns))
|
|
432
|
+
targetStyleCounts[id] = styles.size
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private fun routeSegment(
|
|
437
|
+
spec: Map<String, Any?>,
|
|
438
|
+
styleSet: RouteLineStylesSet,
|
|
439
|
+
): RouteLineSegment {
|
|
440
|
+
val styleIndex = spec.int("styleIndex", 0)
|
|
441
|
+
val styles = styleSet.getStyles(styleIndex) ?: invalidRoute("Route segment styleIndex is out of range.")
|
|
442
|
+
return RouteLineSegment
|
|
443
|
+
.from(spec.coordinateList("coordinates"), styles)
|
|
444
|
+
.setCurveType(routeCurve(spec))
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private fun routeCurve(spec: Map<String, Any?>) =
|
|
448
|
+
when (spec.stringOrNull("curve") ?: "none") {
|
|
449
|
+
"none" -> CurveType.None
|
|
450
|
+
"left" -> CurveType.LeftCurve
|
|
451
|
+
"right" -> CurveType.RightCurve
|
|
452
|
+
else -> invalidRoute("Unknown route curve type.")
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private fun validateSegments(
|
|
456
|
+
segments: List<Map<String, Any?>>,
|
|
457
|
+
styleCount: Int,
|
|
458
|
+
) {
|
|
459
|
+
if (segments.isEmpty()) invalidRoute("Routes require at least one segment.")
|
|
460
|
+
segments.forEach { segment ->
|
|
461
|
+
if (segment.coordinateList("coordinates").size < 2) invalidRoute("Route segments require at least two coordinates.")
|
|
462
|
+
if (segment.int("styleIndex", 0) !in 0 until styleCount) invalidRoute("Route segment styleIndex is out of range.")
|
|
463
|
+
routeCurve(segment)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private fun startAnimation(animation: Map<String, Any?>) {
|
|
468
|
+
val id = animation.string("animationId")
|
|
469
|
+
if (id.isBlank()) invalidRoute("Route animationId must not be empty.")
|
|
470
|
+
if (animators.containsKey(id)) invalidRoute("Route animation $id already exists.")
|
|
471
|
+
if (animation.int("repeat", 1) != 1) throw unsupportedRoute("The Kakao route progress animator does not support repeat counts.")
|
|
472
|
+
val duration = animation.int("duration")
|
|
473
|
+
if (duration <= 0) invalidRoute("Route animation duration must be positive.")
|
|
474
|
+
val routeIds = animation.stringList("routeIds")
|
|
475
|
+
if (routeIds.isEmpty()) invalidRoute("Route animation requires routeIds.")
|
|
476
|
+
val direction =
|
|
477
|
+
when (animation.string("direction")) {
|
|
478
|
+
"forward" -> ProgressDirection.StartFirst
|
|
479
|
+
"backward" -> ProgressDirection.EndFirst
|
|
480
|
+
else -> invalidRoute("Unknown route animation direction.")
|
|
481
|
+
}
|
|
482
|
+
val type =
|
|
483
|
+
when (animation.string("mode")) {
|
|
484
|
+
"fill" -> ProgressType.ToShow
|
|
485
|
+
"clear" -> ProgressType.ToHide
|
|
486
|
+
else -> invalidRoute("Unknown route animation mode.")
|
|
487
|
+
}
|
|
488
|
+
val requested = if (animation.string("mode") == "fill") 1f else 0f
|
|
489
|
+
val resetToInitial = animation.boolean("resetToInitial", false)
|
|
490
|
+
val targets = routeIds.map(::route)
|
|
491
|
+
val interpolation = routeInterpolation(animation.stringOrNull("interpolation") ?: "linear")
|
|
492
|
+
val hideAtStop = animation.boolean("hideAtStop", false)
|
|
493
|
+
val definition = mapCanonicalValue(animation - "animationId")
|
|
494
|
+
val nativeId =
|
|
495
|
+
animatorIdsByDefinition.getOrPut(definition) {
|
|
496
|
+
"rnk-route-${System.identityHashCode(this)}-${animationSequence++}"
|
|
497
|
+
}
|
|
498
|
+
if (nativeId in animationNativeIds.values) invalidRoute("An equivalent route animation is already running.")
|
|
499
|
+
val cachedAnimator = animatorCache[nativeId]
|
|
500
|
+
val animator =
|
|
501
|
+
cachedAnimator ?: run {
|
|
502
|
+
val effect =
|
|
503
|
+
ProgressAnimation
|
|
504
|
+
.from(nativeId, duration)
|
|
505
|
+
.setProgressDirection(direction)
|
|
506
|
+
.setProgressType(type)
|
|
507
|
+
.setInterpolation(interpolation)
|
|
508
|
+
.setHideAtStop(hideAtStop)
|
|
509
|
+
.setResetToInitialState(resetToInitial)
|
|
510
|
+
(manager.addAnimator(effect) ?: invalidRoute("Could not create route animation $id.")).also {
|
|
511
|
+
it.addRouteLines(*targets.toTypedArray())
|
|
512
|
+
animatorCache[nativeId] = it
|
|
513
|
+
animatorTargetIds[nativeId] = routeIds.toSet()
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (cachedAnimator != null && nativeId in animatorsNeedingTargets) {
|
|
517
|
+
animator.addRouteLines(*targets.toTypedArray())
|
|
518
|
+
animatorsNeedingTargets.remove(nativeId)
|
|
519
|
+
}
|
|
520
|
+
routeIds.forEach(::invalidateProgressRequest)
|
|
521
|
+
if (!resetToInitial) routeIds.forEach { requestedProgress[it] = requested }
|
|
522
|
+
animators[id] = animator
|
|
523
|
+
animationTargets[id] = routeIds.toSet()
|
|
524
|
+
animationNativeIds[id] = nativeId
|
|
525
|
+
val runEpoch = ++animationRunSequence
|
|
526
|
+
animationRunEpochs[id] = runEpoch
|
|
527
|
+
val managerReference = WeakReference(this)
|
|
528
|
+
animator.start { completedAnimator -> managerReference.get()?.completeAnimation(id, nativeId, runEpoch, completedAnimator) }
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private fun stopAnimation(
|
|
532
|
+
id: String,
|
|
533
|
+
shouldStop: Boolean,
|
|
534
|
+
) {
|
|
535
|
+
val animator = animators.remove(id) ?: unknownRoute("route animation", id)
|
|
536
|
+
animationTargets.remove(id)
|
|
537
|
+
val nativeId = animationNativeIds.remove(id)
|
|
538
|
+
animationRunEpochs.remove(id)
|
|
539
|
+
if (shouldStop) {
|
|
540
|
+
animator.stop()
|
|
541
|
+
nativeId?.let(animatorsNeedingTargets::add)
|
|
542
|
+
}
|
|
543
|
+
emit("onRouteAnimationStop", mapOf("animationId" to id))
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private fun completeAnimation(
|
|
547
|
+
id: String,
|
|
548
|
+
nativeId: String,
|
|
549
|
+
runEpoch: Long,
|
|
550
|
+
completedAnimator: RouteLineAnimator,
|
|
551
|
+
) {
|
|
552
|
+
if (animationNativeIds[id] != nativeId || animationRunEpochs[id] != runEpoch || animators[id] !== completedAnimator) return
|
|
553
|
+
animators.remove(id)
|
|
554
|
+
animationTargets.remove(id)
|
|
555
|
+
animationNativeIds.remove(id)
|
|
556
|
+
animationRunEpochs.remove(id)
|
|
557
|
+
emit("onRouteAnimationStop", mapOf("animationId" to id))
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private fun state(id: String): Map<String, Any?> {
|
|
561
|
+
val value = route(id)
|
|
562
|
+
return mapOf(
|
|
563
|
+
"type" to "state",
|
|
564
|
+
"id" to id,
|
|
565
|
+
"layerId" to routeLayerIds.getValue(id),
|
|
566
|
+
"visible" to value.isShow,
|
|
567
|
+
"zIndex" to value.zOrder,
|
|
568
|
+
"nativeProgress" to value.progress,
|
|
569
|
+
"requestedProgress" to (requestedProgress[id] ?: 0f),
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private fun counts() = mapOf("type" to "counts", "layers" to layers.size, "routes" to routes.size, "animations" to animators.size)
|
|
574
|
+
|
|
575
|
+
private fun none() = mapOf<String, Any?>("type" to "none")
|
|
576
|
+
|
|
577
|
+
private fun route(id: String) = routes[id] ?: unknownRoute("route", id)
|
|
578
|
+
|
|
579
|
+
private fun routeEntries(ids: List<String>): List<RouteLine> {
|
|
580
|
+
if (ids.toSet().size != ids.size) invalidRoute("Route command IDs must be unique.")
|
|
581
|
+
return ids.map(::route)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private fun style(id: String) = styleSets[id] ?: unknownRoute("route style set", id)
|
|
585
|
+
|
|
586
|
+
private fun remove(id: String) {
|
|
587
|
+
invalidateProgressRequest(id)
|
|
588
|
+
stopAnimationsForRoute(id)
|
|
589
|
+
invalidateCachedAnimationsForRoute(id)
|
|
590
|
+
routes.remove(id)?.remove() ?: unknownRoute("route", id)
|
|
591
|
+
requestedProgress.remove(id)
|
|
592
|
+
routeLayerIds.remove(id)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private fun stopAnimationsForRoute(id: String) {
|
|
596
|
+
animationTargets
|
|
597
|
+
.filterValues { id in it }
|
|
598
|
+
.keys
|
|
599
|
+
.toList()
|
|
600
|
+
.forEach { stopAnimation(it, true) }
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private fun invalidateCachedAnimationsForRoute(id: String) {
|
|
604
|
+
animatorTargetIds
|
|
605
|
+
.filterValues { id in it }
|
|
606
|
+
.keys
|
|
607
|
+
.forEach { nativeId ->
|
|
608
|
+
animatorCache[nativeId]?.stop()
|
|
609
|
+
animatorsNeedingTargets += nativeId
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private fun disposeObjects() {
|
|
614
|
+
animators.clear()
|
|
615
|
+
animationTargets.clear()
|
|
616
|
+
animationNativeIds.clear()
|
|
617
|
+
animationRunEpochs.clear()
|
|
618
|
+
animatorCache.values.forEach(RouteLineAnimator::stop)
|
|
619
|
+
animatorsNeedingTargets.addAll(animatorCache.keys)
|
|
620
|
+
layers.values.forEach(manager::remove)
|
|
621
|
+
layers.clear()
|
|
622
|
+
routes.clear()
|
|
623
|
+
requestedProgress.clear()
|
|
624
|
+
progressEpochs.clear()
|
|
625
|
+
routeLayerIds.clear()
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private fun nextProgressEpoch(id: String): Long {
|
|
629
|
+
val epoch = (progressEpochs[id] ?: 0L) + 1
|
|
630
|
+
progressEpochs[id] = epoch
|
|
631
|
+
return epoch
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private fun invalidateProgressRequest(id: String) {
|
|
635
|
+
nextProgressEpoch(id)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private fun emitRequestedProgress(
|
|
639
|
+
id: String,
|
|
640
|
+
requested: Double,
|
|
641
|
+
sceneGeneration: Int,
|
|
642
|
+
requestEpoch: Long,
|
|
643
|
+
target: RouteLine,
|
|
644
|
+
) {
|
|
645
|
+
if (generation != sceneGeneration || progressEpochs[id] != requestEpoch || routes[id] !== target) return
|
|
646
|
+
emit("onRouteProgress", mapOf("id" to id, "progress" to requested, "source" to "requested"))
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
private const val ROUTE_ERROR = "ERR_KAKAO_MAP_ROUTE"
|
|
651
|
+
|
|
652
|
+
private fun signedProgress(
|
|
653
|
+
value: Double,
|
|
654
|
+
mode: String,
|
|
655
|
+
): Float {
|
|
656
|
+
if (!value.isFinite() || value !in 0.0..1.0) invalidRoute("Route progress must be between 0 and 1.")
|
|
657
|
+
return when (mode) {
|
|
658
|
+
"fill" -> value.toFloat()
|
|
659
|
+
"clear" -> -value.toFloat()
|
|
660
|
+
else -> invalidRoute("Unknown route progress mode.")
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private fun Map<String, Any?>.progress() =
|
|
665
|
+
double("progress")
|
|
666
|
+
.also {
|
|
667
|
+
if (it !in
|
|
668
|
+
0.0..1.0
|
|
669
|
+
) {
|
|
670
|
+
invalidRoute("Route progress must be between 0 and 1.")
|
|
671
|
+
}
|
|
672
|
+
}.toFloat()
|
|
673
|
+
|
|
674
|
+
private fun routeInterpolation(value: String) =
|
|
675
|
+
when (value) {
|
|
676
|
+
"linear" -> Interpolation.Linear
|
|
677
|
+
"cubicIn" -> Interpolation.CubicIn
|
|
678
|
+
"cubicOut" -> Interpolation.CubicOut
|
|
679
|
+
"cubicInOut" -> Interpolation.CubicInOut
|
|
680
|
+
else -> invalidRoute("Unknown interpolation $value.")
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private fun rejectRouteImageSizing(source: Map<String, Any?>) {
|
|
684
|
+
if (source.containsKey("width") || source.containsKey("height")) {
|
|
685
|
+
throw unsupportedRoute("Kakao route patterns use the source bitmap size.")
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
private fun requireUniqueRoute(
|
|
690
|
+
values: List<Map<String, Any?>>,
|
|
691
|
+
noun: String,
|
|
692
|
+
) {
|
|
693
|
+
val ids = values.map { it.string("id") }
|
|
694
|
+
if (ids.any(String::isBlank)) invalidRoute("$noun ID must not be empty.")
|
|
695
|
+
if (ids.toSet().size !=
|
|
696
|
+
ids.size
|
|
697
|
+
) {
|
|
698
|
+
invalidRoute("Duplicate $noun ID.")
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private fun Map<String, Any?>.map(key: String) = this[key] as? Map<String, Any?> ?: invalidRoute("$key must be an object.")
|
|
703
|
+
|
|
704
|
+
private fun Map<String, Any?>.mapOrNull(key: String): Map<String, Any?>? {
|
|
705
|
+
if (!containsKey(key)) return null
|
|
706
|
+
return this[key] as? Map<String, Any?> ?: invalidRoute("$key must be an object.")
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
private fun Map<String, Any?>.list(key: String): List<Map<String, Any?>> {
|
|
710
|
+
if (!containsKey(key)) return emptyList()
|
|
711
|
+
val raw = this[key] as? List<*> ?: invalidRoute("$key must be an array of objects.")
|
|
712
|
+
return raw.map { it as? Map<String, Any?> ?: invalidRoute("$key must contain objects.") }
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private fun Map<String, Any?>.string(key: String) = this[key] as? String ?: invalidRoute("$key must be a string.")
|
|
716
|
+
|
|
717
|
+
private fun Map<String, Any?>.stringOrNull(key: String): String? {
|
|
718
|
+
if (!containsKey(key)) return null
|
|
719
|
+
return this[key] as? String ?: invalidRoute("$key must be a string.")
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private fun Map<String, Any?>.stringList(key: String) =
|
|
723
|
+
(this[key] as? List<*>)?.map { it as? String ?: invalidRoute("$key must contain strings.") } ?: invalidRoute("$key must be an array.")
|
|
724
|
+
|
|
725
|
+
private fun Map<String, Any?>.boolean(
|
|
726
|
+
key: String,
|
|
727
|
+
fallback: Boolean? = null,
|
|
728
|
+
): Boolean {
|
|
729
|
+
if (!containsKey(key)) return fallback ?: invalidRoute("$key must be a boolean.")
|
|
730
|
+
return this[key] as? Boolean ?: invalidRoute("$key must be a boolean.")
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private fun Map<String, Any?>.double(
|
|
734
|
+
key: String,
|
|
735
|
+
fallback: Double? = null,
|
|
736
|
+
): Double {
|
|
737
|
+
if (!containsKey(key)) return fallback ?: invalidRoute("$key must be finite.")
|
|
738
|
+
return (this[key] as? Number)?.toDouble()?.takeIf { it.isFinite() } ?: invalidRoute("$key must be finite.")
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
private fun Map<String, Any?>.float(
|
|
742
|
+
key: String,
|
|
743
|
+
fallback: Float? = null,
|
|
744
|
+
) = double(key, fallback?.toDouble()).toFloat()
|
|
745
|
+
|
|
746
|
+
private fun Map<String, Any?>.int(
|
|
747
|
+
key: String,
|
|
748
|
+
fallback: Int? = null,
|
|
749
|
+
): Int {
|
|
750
|
+
val value = double(key, fallback?.toDouble())
|
|
751
|
+
if (value % 1 != 0.0 ||
|
|
752
|
+
value !in Int.MIN_VALUE.toDouble()..Int.MAX_VALUE.toDouble()
|
|
753
|
+
) {
|
|
754
|
+
invalidRoute("$key must be an integer.")
|
|
755
|
+
}
|
|
756
|
+
return value.toInt()
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
private fun Map<String, Any?>.color(
|
|
760
|
+
key: String,
|
|
761
|
+
fallback: Int? = null,
|
|
762
|
+
): Int {
|
|
763
|
+
if (!containsKey(key)) return fallback ?: invalidRoute("$key must be a color.")
|
|
764
|
+
return (this[key] as? String)?.let(::parseRouteColor) ?: invalidRoute("$key must be a color.")
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private fun Map<String, Any?>.coordinate(key: String) = map(key).coordinateValue()
|
|
768
|
+
|
|
769
|
+
private fun Map<String, Any?>.coordinateList(key: String) =
|
|
770
|
+
(this[key] as? List<*>)?.map { it.coordinateValue() } ?: invalidRoute("$key must be an array.")
|
|
771
|
+
|
|
772
|
+
private fun Any?.coordinateValue(): LatLng {
|
|
773
|
+
val value =
|
|
774
|
+
this as? Map<String, Any?> ?: invalidRoute("Coordinate must be an object.")
|
|
775
|
+
val latitude = value.double("latitude")
|
|
776
|
+
val longitude = value.double("longitude")
|
|
777
|
+
if (latitude !in
|
|
778
|
+
-90.0..90.0 ||
|
|
779
|
+
longitude !in -180.0..180.0
|
|
780
|
+
) {
|
|
781
|
+
invalidRoute("Invalid coordinate.")
|
|
782
|
+
}
|
|
783
|
+
return LatLng.from(latitude, longitude)
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
private fun parseRouteColor(value: String): Int {
|
|
787
|
+
if (!Regex("^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$").matches(value)) invalidRoute("Colors must be #RRGGBB or #RRGGBBAA.")
|
|
788
|
+
val raw = value.drop(1)
|
|
789
|
+
return if (raw.length ==
|
|
790
|
+
6
|
|
791
|
+
) {
|
|
792
|
+
Color.parseColor(value)
|
|
793
|
+
} else {
|
|
794
|
+
((raw.takeLast(2).toLong(16) shl 24) or raw.dropLast(2).toLong(16)).toInt()
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
private fun invalidRoute(message: String): Nothing = throw CodedException("${ROUTE_ERROR}_INVALID_ARGUMENT", message, null)
|
|
799
|
+
|
|
800
|
+
private fun unsupportedRoute(message: String): CodedException = CodedException("${ROUTE_ERROR}_UNSUPPORTED", message, null)
|
|
801
|
+
|
|
802
|
+
private fun unknownRoute(
|
|
803
|
+
noun: String,
|
|
804
|
+
id: String,
|
|
805
|
+
): Nothing = throw CodedException("${ROUTE_ERROR}_NOT_FOUND", "Unknown $noun $id.", null)
|