@react-native-kakao/map 2.2.6 → 3.1.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +5 -38
- package/RNCKakaoMap.podspec +16 -52
- package/android/build.gradle +12 -107
- package/android/consumer-rules.pro +2 -0
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/net/mjstudio/rnkakao/map/CameraOptions.kt +61 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapModule.kt +131 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapView.kt +1257 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapCameraCommands.kt +306 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapDimScreen.kt +431 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapGui.kt +899 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapImageLoader.kt +232 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapLabels.kt +1572 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapOptions.kt +360 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapRoutes.kt +805 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapShapes.kt +1096 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapUtilities.kt +495 -0
- package/expo-module.config.json +5 -0
- package/ios/CameraOptions.swift +41 -0
- package/ios/KakaoMapModule.swift +204 -0
- package/ios/KakaoMapView.swift +1018 -0
- package/ios/MapCameraCommands.swift +451 -0
- package/ios/MapDimScreen.swift +463 -0
- package/ios/MapGui.swift +1099 -0
- package/ios/MapImageLoader.swift +177 -0
- package/ios/MapLabels.swift +1676 -0
- package/ios/MapOptions.swift +374 -0
- package/ios/MapRoutes.swift +903 -0
- package/ios/MapShapes.swift +1294 -0
- package/ios/MapUnits.swift +20 -0
- package/ios/MapUtilities.swift +365 -0
- package/lib/commonjs/index.js +180 -29
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +18 -0
- package/lib/commonjs/index.web.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types-assets.js +2 -0
- package/lib/commonjs/types-assets.js.map +1 -0
- package/lib/commonjs/types-camera.js +6 -0
- package/lib/commonjs/types-camera.js.map +1 -0
- package/lib/commonjs/types-dim.js +6 -0
- package/lib/commonjs/types-dim.js.map +1 -0
- package/lib/commonjs/types-gui.js +6 -0
- package/lib/commonjs/types-gui.js.map +1 -0
- package/lib/commonjs/types-labels.js +6 -0
- package/lib/commonjs/types-labels.js.map +1 -0
- package/lib/commonjs/types-routes.js +6 -0
- package/lib/commonjs/types-routes.js.map +1 -0
- package/lib/commonjs/types-shapes.js +6 -0
- package/lib/commonjs/types-shapes.js.map +1 -0
- package/lib/commonjs/types-utilities.js +6 -0
- package/lib/commonjs/types-utilities.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/index.js +180 -22
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +13 -0
- package/lib/module/index.web.js.map +1 -0
- package/lib/module/types-assets.js +2 -0
- package/lib/module/types-assets.js.map +1 -0
- package/lib/module/types-camera.js +4 -0
- package/lib/module/types-camera.js.map +1 -0
- package/lib/module/types-dim.js +4 -0
- package/lib/module/types-dim.js.map +1 -0
- package/lib/module/types-gui.js +4 -0
- package/lib/module/types-gui.js.map +1 -0
- package/lib/module/types-labels.js +4 -0
- package/lib/module/types-labels.js.map +1 -0
- package/lib/module/types-routes.js +4 -0
- package/lib/module/types-routes.js.map +1 -0
- package/lib/module/types-shapes.js +4 -0
- package/lib/module/types-shapes.js.map +1 -0
- package/lib/module/types-utilities.js +4 -0
- package/lib/module/types-utilities.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/src/index.d.ts +59 -9
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types-assets.d.ts +9 -0
- package/lib/typescript/src/types-assets.d.ts.map +1 -0
- package/lib/typescript/src/types-camera.d.ts +123 -0
- package/lib/typescript/src/types-camera.d.ts.map +1 -0
- package/lib/typescript/src/types-dim.d.ts +54 -0
- package/lib/typescript/src/types-dim.d.ts.map +1 -0
- package/lib/typescript/src/types-gui.d.ts +189 -0
- package/lib/typescript/src/types-gui.d.ts.map +1 -0
- package/lib/typescript/src/types-labels.d.ts +246 -0
- package/lib/typescript/src/types-labels.d.ts.map +1 -0
- package/lib/typescript/src/types-routes.d.ts +162 -0
- package/lib/typescript/src/types-routes.d.ts.map +1 -0
- package/lib/typescript/src/types-shapes.d.ts +253 -0
- package/lib/typescript/src/types-shapes.d.ts.map +1 -0
- package/lib/typescript/src/types-utilities.d.ts +130 -0
- package/lib/typescript/src/types-utilities.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +222 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +24 -77
- package/src/index.tsx +322 -0
- package/src/index.web.tsx +46 -0
- package/src/types-assets.ts +8 -0
- package/src/types-camera.ts +97 -0
- package/src/types-dim.ts +54 -0
- package/src/types-gui.ts +171 -0
- package/src/types-labels.ts +207 -0
- package/src/types-routes.ts +118 -0
- package/src/types-shapes.ts +244 -0
- package/src/types-utilities.ts +115 -0
- package/src/types.ts +223 -0
- package/android/gradle.properties +0 -1
- package/android/src/main/AndroidManifestNew.xml +0 -2
- package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapModule.kt +0 -25
- package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapPackage.kt +0 -35
- package/android/src/newarch/KakaoMapSpec.kt +0 -7
- package/android/src/oldarch/KakaoMapSpec.kt +0 -14
- package/ios/Component/Map/RNCKakaoMapView.h +0 -23
- package/ios/Component/Map/RNCKakaoMapView.mm +0 -209
- package/ios/Component/Map/RNCKakaoMapViewManager.mm +0 -19
- package/ios/NativeModule/RNCKakaoMap.h +0 -7
- package/ios/NativeModule/RNCKakaoMap.mm +0 -19
- package/ios/NativeModule/RNCKakaoMapManager.swift +0 -7
- package/ios/Util/Util.h +0 -97
- package/lib/commonjs/component/KakaoMapView.js +0 -54
- package/lib/commonjs/component/KakaoMapView.js.map +0 -1
- package/lib/commonjs/spec/NativeKakaoMap.js +0 -9
- package/lib/commonjs/spec/NativeKakaoMap.js.map +0 -1
- package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js +0 -15
- package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
- package/lib/commonjs/type/type.js +0 -2
- package/lib/commonjs/type/type.js.map +0 -1
- package/lib/commonjs/util/Const.js +0 -14
- package/lib/commonjs/util/Const.js.map +0 -1
- package/lib/commonjs/util/Util.js +0 -15
- package/lib/commonjs/util/Util.js.map +0 -1
- package/lib/module/component/KakaoMapView.js +0 -48
- package/lib/module/component/KakaoMapView.js.map +0 -1
- package/lib/module/spec/NativeKakaoMap.js +0 -3
- package/lib/module/spec/NativeKakaoMap.js.map +0 -1
- package/lib/module/spec/RNCKakaoMapViewNativeComponent.js +0 -10
- package/lib/module/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
- package/lib/module/type/type.js +0 -2
- package/lib/module/type/type.js.map +0 -1
- package/lib/module/util/Const.js +0 -8
- package/lib/module/util/Const.js.map +0 -1
- package/lib/module/util/Util.js +0 -9
- package/lib/module/util/Util.js.map +0 -1
- package/lib/typescript/src/component/KakaoMapView.d.ts +0 -25
- package/lib/typescript/src/component/KakaoMapView.d.ts.map +0 -1
- package/lib/typescript/src/spec/NativeKakaoMap.d.ts +0 -7
- package/lib/typescript/src/spec/NativeKakaoMap.d.ts.map +0 -1
- package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts +0 -32
- package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts.map +0 -1
- package/lib/typescript/src/type/type.d.ts +0 -8
- package/lib/typescript/src/type/type.d.ts.map +0 -1
- package/lib/typescript/src/util/Const.d.ts +0 -8
- package/lib/typescript/src/util/Const.d.ts.map +0 -1
- package/lib/typescript/src/util/Util.d.ts +0 -3
- package/lib/typescript/src/util/Util.d.ts.map +0 -1
- package/src/component/KakaoMapView.tsx +0 -82
- package/src/index.ts +0 -43
- package/src/spec/NativeKakaoMap.ts +0 -8
- package/src/spec/RNCKakaoMapViewNativeComponent.ts +0 -38
- package/src/type/type.ts +0 -7
- package/src/util/Const.ts +0 -7
- package/src/util/Util.ts +0 -11
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
package net.mjstudio.rnkakao.map
|
|
2
|
+
|
|
3
|
+
import android.graphics.Color
|
|
4
|
+
import android.graphics.PointF
|
|
5
|
+
import com.kakao.vectormap.KakaoMap
|
|
6
|
+
import com.kakao.vectormap.LatLng
|
|
7
|
+
import com.kakao.vectormap.animation.Interpolation
|
|
8
|
+
import com.kakao.vectormap.shape.DotPoints
|
|
9
|
+
import com.kakao.vectormap.shape.LatLngVertex
|
|
10
|
+
import com.kakao.vectormap.shape.MapPoints
|
|
11
|
+
import com.kakao.vectormap.shape.PointVertex
|
|
12
|
+
import com.kakao.vectormap.shape.Polygon
|
|
13
|
+
import com.kakao.vectormap.shape.PolygonOptions
|
|
14
|
+
import com.kakao.vectormap.shape.PolygonStyle
|
|
15
|
+
import com.kakao.vectormap.shape.PolygonStyles
|
|
16
|
+
import com.kakao.vectormap.shape.PolygonStylesSet
|
|
17
|
+
import com.kakao.vectormap.shape.Polyline
|
|
18
|
+
import com.kakao.vectormap.shape.PolylineCap
|
|
19
|
+
import com.kakao.vectormap.shape.PolylineOptions
|
|
20
|
+
import com.kakao.vectormap.shape.PolylineStyle
|
|
21
|
+
import com.kakao.vectormap.shape.PolylineStyles
|
|
22
|
+
import com.kakao.vectormap.shape.PolylineStylesSet
|
|
23
|
+
import com.kakao.vectormap.shape.ShapeAnimator
|
|
24
|
+
import com.kakao.vectormap.shape.ShapeLayer
|
|
25
|
+
import com.kakao.vectormap.shape.ShapeLayerOptions
|
|
26
|
+
import com.kakao.vectormap.shape.ShapeLayerPass
|
|
27
|
+
import com.kakao.vectormap.shape.animation.CircleWave
|
|
28
|
+
import com.kakao.vectormap.shape.animation.CircleWaves
|
|
29
|
+
import expo.modules.kotlin.exception.CodedException
|
|
30
|
+
import kotlin.math.asin
|
|
31
|
+
import kotlin.math.atan2
|
|
32
|
+
import kotlin.math.cos
|
|
33
|
+
import kotlin.math.sin
|
|
34
|
+
|
|
35
|
+
internal class MapShapes(
|
|
36
|
+
map: KakaoMap,
|
|
37
|
+
density: Float,
|
|
38
|
+
private val emit: (String, Map<String, Any?>) -> Unit,
|
|
39
|
+
) {
|
|
40
|
+
private val manager = map.shapeManager ?: invalid("Shape manager is unavailable.")
|
|
41
|
+
private val density = density.takeIf { it.isFinite() && it > 0 } ?: invalid("Display density must be positive and finite.")
|
|
42
|
+
private val layers = mutableMapOf<String, ShapeLayer>()
|
|
43
|
+
private val shapes = mutableMapOf<String, ShapeEntry>()
|
|
44
|
+
private val animators = mutableMapOf<String, ShapeAnimator>()
|
|
45
|
+
private val animationTargets = mutableMapOf<String, Set<String>>()
|
|
46
|
+
private val animationNativeIds = mutableMapOf<String, String>()
|
|
47
|
+
private val animatorIdsByDefinition = mutableMapOf<String, String>()
|
|
48
|
+
private val animatorCache = mutableMapOf<String, ShapeAnimator>()
|
|
49
|
+
private val polygonStyles = mutableMapOf<String, PolygonStylesSet>()
|
|
50
|
+
private val polylineStyles = mutableMapOf<String, PolylineStylesSet>()
|
|
51
|
+
private val styleCounts = mutableMapOf<String, Int>()
|
|
52
|
+
private var animationSequence = 0
|
|
53
|
+
private var sceneSequence = 0
|
|
54
|
+
private var replacementSequence = 0
|
|
55
|
+
private val nativePrefix = "rnk-shapes-${System.identityHashCode(this)}"
|
|
56
|
+
|
|
57
|
+
fun apply(scene: Map<String, Any?>): Map<String, Any> {
|
|
58
|
+
val styleSpecs = scene.list("styleSets")
|
|
59
|
+
val layerSpecs = scene.list("layers")
|
|
60
|
+
val shapeSpecs = scene.list("shapes")
|
|
61
|
+
validateScene(styleSpecs, layerSpecs, shapeSpecs)
|
|
62
|
+
val nextPolygonStyles = mutableMapOf<String, PolygonStylesSet>()
|
|
63
|
+
val nextPolylineStyles = mutableMapOf<String, PolylineStylesSet>()
|
|
64
|
+
val nextStyleCounts = mutableMapOf<String, Int>()
|
|
65
|
+
val nextLayers = mutableMapOf<String, ShapeLayer>()
|
|
66
|
+
val nextShapes = mutableMapOf<String, ShapeEntry>()
|
|
67
|
+
registerStyles(styleSpecs, nextPolygonStyles, nextPolylineStyles, nextStyleCounts)
|
|
68
|
+
val scenePrefix = "$nativePrefix-scene-${sceneSequence++}"
|
|
69
|
+
try {
|
|
70
|
+
layerSpecs.forEach { addLayer(it, nextLayers, scenePrefix) }
|
|
71
|
+
shapeSpecs.forEach { addShape(it, nextLayers, nextShapes, nextPolygonStyles, nextPolylineStyles) }
|
|
72
|
+
} catch (error: Throwable) {
|
|
73
|
+
nextLayers.values.forEach(manager::remove)
|
|
74
|
+
throw error
|
|
75
|
+
}
|
|
76
|
+
disposeObjects()
|
|
77
|
+
polygonStyles.clear()
|
|
78
|
+
polygonStyles.putAll(nextPolygonStyles)
|
|
79
|
+
polylineStyles.clear()
|
|
80
|
+
polylineStyles.putAll(nextPolylineStyles)
|
|
81
|
+
styleCounts.clear()
|
|
82
|
+
styleCounts.putAll(nextStyleCounts)
|
|
83
|
+
layers.putAll(nextLayers)
|
|
84
|
+
shapes.putAll(nextShapes)
|
|
85
|
+
layerSpecs.forEach { spec -> nextLayers.getValue(spec.string("id")).setVisible(spec.boolean("visible", true)) }
|
|
86
|
+
return counts()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fun command(command: Map<String, Any?>): Map<String, Any?> =
|
|
90
|
+
when (command.string("type")) {
|
|
91
|
+
"setVisible" -> {
|
|
92
|
+
val targets = entries(command.stringList("ids"))
|
|
93
|
+
val isVisible = command.boolean("visible")
|
|
94
|
+
targets.forEach { it.setVisible(isVisible) }
|
|
95
|
+
none()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
"setPosition" -> {
|
|
99
|
+
entry(command.string("id")).setPosition(command.coordinate("position"))
|
|
100
|
+
none()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
"setZIndex" -> {
|
|
104
|
+
entry(command.string("id")).setZIndex(command.int("zIndex"))
|
|
105
|
+
none()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
"setOrientation" -> {
|
|
109
|
+
throw unsupported("Android Kakao Maps does not expose shape orientation.")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
"changeData" -> {
|
|
113
|
+
changeData(command)
|
|
114
|
+
none()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
"startWave" -> {
|
|
118
|
+
startWave(command.map("animation"))
|
|
119
|
+
none()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
"stopAnimation" -> {
|
|
123
|
+
stopAnimation(command.string("animationId"))
|
|
124
|
+
none()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
"getState" -> {
|
|
128
|
+
state(command.string("id"))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
"getCounts" -> {
|
|
132
|
+
counts()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
"getCapabilities" -> {
|
|
136
|
+
capabilities() + ("type" to "capabilities")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
"remove" -> {
|
|
140
|
+
val ids = command.stringList("ids")
|
|
141
|
+
entries(ids)
|
|
142
|
+
ids.forEach(::remove)
|
|
143
|
+
none()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
else -> {
|
|
147
|
+
invalid("Unknown shape command type.")
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fun capabilities() =
|
|
152
|
+
mapOf(
|
|
153
|
+
"layerPasses" to listOf("default", "overlay", "route"),
|
|
154
|
+
"geographicPrimitives" to true,
|
|
155
|
+
"modelPrimitives" to true,
|
|
156
|
+
"geographicPosition" to true,
|
|
157
|
+
"geographicOrientation" to false,
|
|
158
|
+
"modelPosition" to true,
|
|
159
|
+
"modelOrientation" to false,
|
|
160
|
+
"zIndexMutation" to true,
|
|
161
|
+
"fractionalStyleWidth" to true,
|
|
162
|
+
"waveAnimation" to true,
|
|
163
|
+
"waveTargets" to listOf("polygon"),
|
|
164
|
+
"waveNaturalStopEvent" to false,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
fun resolvePolygon(id: String): Polygon? = shapes[id]?.native as? Polygon
|
|
168
|
+
|
|
169
|
+
fun dispose() {
|
|
170
|
+
disposeObjects()
|
|
171
|
+
polygonStyles.clear()
|
|
172
|
+
polylineStyles.clear()
|
|
173
|
+
styleCounts.clear()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private fun validateScene(
|
|
177
|
+
styleSpecs: List<Map<String, Any?>>,
|
|
178
|
+
layerSpecs: List<Map<String, Any?>>,
|
|
179
|
+
shapeSpecs: List<Map<String, Any?>>,
|
|
180
|
+
) {
|
|
181
|
+
requireUnique(styleSpecs, "id", "style set")
|
|
182
|
+
requireUnique(layerSpecs, "id", "shape layer")
|
|
183
|
+
requireUnique(shapeSpecs, "id", "shape")
|
|
184
|
+
styleSpecs.forEach(::validateStyleSet)
|
|
185
|
+
layerSpecs.forEach { layer ->
|
|
186
|
+
layer.int("zIndex", DEFAULT_SHAPE_LAYER_Z_INDEX)
|
|
187
|
+
layerPass(layer.stringOrNull("pass") ?: "default")
|
|
188
|
+
layer.boolean("visible", true)
|
|
189
|
+
}
|
|
190
|
+
val styleTypes = styleSpecs.associate { it.string("id") to it.string("type") }
|
|
191
|
+
val layerIds = layerSpecs.mapTo(mutableSetOf()) { it.string("id") }
|
|
192
|
+
shapeSpecs.forEach { spec ->
|
|
193
|
+
spec.int("zIndex", 0)
|
|
194
|
+
spec.boolean("visible", true)
|
|
195
|
+
val geometry = spec.map("geometry")
|
|
196
|
+
val geometryType = geometry.string("type")
|
|
197
|
+
val requiredStyle =
|
|
198
|
+
if (geometryType.contains("Polyline") || geometryType in setOf("geographicCurve", "modelArc", "geographicArc")) {
|
|
199
|
+
"polyline"
|
|
200
|
+
} else {
|
|
201
|
+
"polygon"
|
|
202
|
+
}
|
|
203
|
+
if (spec.string("layerId") !in layerIds) invalid("Shape ${spec.string("id")} references an unknown layer.")
|
|
204
|
+
if (styleTypes[spec.string("styleSetId")] != requiredStyle) {
|
|
205
|
+
invalid("Shape ${spec.string("id")} references an incompatible style set.")
|
|
206
|
+
}
|
|
207
|
+
val normalized = normalizeGeometry(geometry)
|
|
208
|
+
rejectAndroidOrientation(normalized)
|
|
209
|
+
validateGeometry(normalized, styleSpecs.first { it.string("id") == spec.string("styleSetId") }.list("styles").size)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private fun validateStyleSet(spec: Map<String, Any?>) {
|
|
214
|
+
val styles = spec.list("styles")
|
|
215
|
+
if (styles.isEmpty()) invalid("Shape style sets require styles.")
|
|
216
|
+
when (spec.string("type")) {
|
|
217
|
+
"polygon" -> {
|
|
218
|
+
mapShapePolygonStylesSet(spec)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
"polyline" -> {
|
|
222
|
+
polylineCap(spec.stringOrNull("cap") ?: "square")
|
|
223
|
+
styles.forEach { style ->
|
|
224
|
+
val levels = style.list("levels")
|
|
225
|
+
if (levels.isEmpty()) invalid("Polyline styles require levels.")
|
|
226
|
+
levels.forEach { level ->
|
|
227
|
+
level.int("level")
|
|
228
|
+
if (level.float("width") < 0 || level.float("strokeWidth", 0f) < 0) {
|
|
229
|
+
invalid("Polyline widths must be non-negative.")
|
|
230
|
+
}
|
|
231
|
+
level.color("color")
|
|
232
|
+
level.color("strokeColor", Color.TRANSPARENT)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
else -> {
|
|
238
|
+
invalid("Unknown shape style type.")
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private fun registerStyles(
|
|
244
|
+
specs: List<Map<String, Any?>>,
|
|
245
|
+
targetPolygonStyles: MutableMap<String, PolygonStylesSet>,
|
|
246
|
+
targetPolylineStyles: MutableMap<String, PolylineStylesSet>,
|
|
247
|
+
targetStyleCounts: MutableMap<String, Int>,
|
|
248
|
+
) {
|
|
249
|
+
specs.forEach { spec ->
|
|
250
|
+
val id = spec.string("id")
|
|
251
|
+
val nativeSpec = spec + ("id" to mapStyleNativeID("rnk-shape-style", spec))
|
|
252
|
+
if (spec.string("type") == "polygon") {
|
|
253
|
+
targetPolygonStyles[id] = manager.addPolygonStyles(mapShapePolygonStylesSet(nativeSpec, density))
|
|
254
|
+
} else {
|
|
255
|
+
val styles =
|
|
256
|
+
spec.list("styles").map { style ->
|
|
257
|
+
PolylineStyles.from(
|
|
258
|
+
style.list("levels").map { level ->
|
|
259
|
+
PolylineStyle.from(
|
|
260
|
+
level.int("level"),
|
|
261
|
+
level.float("width") * density,
|
|
262
|
+
level.color("color"),
|
|
263
|
+
level.float("strokeWidth", 0f) * density,
|
|
264
|
+
level.color("strokeColor", Color.TRANSPARENT),
|
|
265
|
+
)
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
val set = PolylineStylesSet.from(nativeSpec.string("id"), styles).setPolylineCap(polylineCap(spec.stringOrNull("cap") ?: "square"))
|
|
270
|
+
targetPolylineStyles[id] = manager.addPolylineStyles(set)
|
|
271
|
+
}
|
|
272
|
+
targetStyleCounts[id] = spec.list("styles").size
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private fun addLayer(
|
|
277
|
+
spec: Map<String, Any?>,
|
|
278
|
+
targetLayers: MutableMap<String, ShapeLayer>,
|
|
279
|
+
scenePrefix: String,
|
|
280
|
+
) {
|
|
281
|
+
val id = spec.string("id")
|
|
282
|
+
val options =
|
|
283
|
+
ShapeLayerOptions
|
|
284
|
+
.from(
|
|
285
|
+
"$scenePrefix-layer-$id",
|
|
286
|
+
spec.int("zIndex", DEFAULT_SHAPE_LAYER_Z_INDEX),
|
|
287
|
+
layerPass(spec.stringOrNull("pass") ?: "default"),
|
|
288
|
+
).setVisible(false)
|
|
289
|
+
targetLayers[id] = manager.addLayer(options) ?: invalid("Could not create shape layer $id.")
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private fun addShape(
|
|
293
|
+
spec: Map<String, Any?>,
|
|
294
|
+
targetLayers: Map<String, ShapeLayer>,
|
|
295
|
+
targetShapes: MutableMap<String, ShapeEntry>,
|
|
296
|
+
targetPolygonStyles: Map<String, PolygonStylesSet>,
|
|
297
|
+
targetPolylineStyles: Map<String, PolylineStylesSet>,
|
|
298
|
+
nativeId: String = spec.string("id"),
|
|
299
|
+
) {
|
|
300
|
+
val id = spec.string("id")
|
|
301
|
+
val layerId = spec.string("layerId")
|
|
302
|
+
val layer = targetLayers[layerId] ?: invalid("Unknown shape layer $layerId.")
|
|
303
|
+
val styleId = spec.string("styleSetId")
|
|
304
|
+
val rawGeometry = spec.map("geometry")
|
|
305
|
+
val geometryType = rawGeometry.string("type")
|
|
306
|
+
val geometry = normalizeGeometry(rawGeometry)
|
|
307
|
+
val entry =
|
|
308
|
+
when (geometry.string("type")) {
|
|
309
|
+
"geographicPolygon" -> {
|
|
310
|
+
val options =
|
|
311
|
+
PolygonOptions
|
|
312
|
+
.from(
|
|
313
|
+
nativeId,
|
|
314
|
+
).setStylesSet(targetPolygonStyles[styleId] ?: unknown("polygon style set", styleId))
|
|
315
|
+
.setZOrder(spec.int("zIndex", 0))
|
|
316
|
+
.setVisible(spec.boolean("visible", true))
|
|
317
|
+
geometry.list("parts").forEach { options.addPolygon(mapShapeMapPoints(it), it.int("styleIndex", 0)) }
|
|
318
|
+
ShapeEntry(layerId, geometryType, layer.addPolygon(options) ?: invalid("Could not create polygon $id."))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
"modelPolygon" -> {
|
|
322
|
+
rejectAndroidOrientation(geometry)
|
|
323
|
+
val options =
|
|
324
|
+
PolygonOptions
|
|
325
|
+
.from(
|
|
326
|
+
nativeId,
|
|
327
|
+
).setStylesSet(targetPolygonStyles[styleId] ?: unknown("polygon style set", styleId))
|
|
328
|
+
.setZOrder(spec.int("zIndex", 0))
|
|
329
|
+
.setVisible(spec.boolean("visible", true))
|
|
330
|
+
geometry.list("parts").forEach {
|
|
331
|
+
options.addPolygon(mapShapeDotPoints(geometry.coordinate("base"), it), it.int("styleIndex", 0))
|
|
332
|
+
}
|
|
333
|
+
ShapeEntry(layerId, geometryType, layer.addPolygon(options) ?: invalid("Could not create model polygon $id."))
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
"geographicPolyline", "geographicCurve" -> {
|
|
337
|
+
val options =
|
|
338
|
+
PolylineOptions
|
|
339
|
+
.from(
|
|
340
|
+
nativeId,
|
|
341
|
+
).setStylesSet(targetPolylineStyles[styleId] ?: unknown("polyline style set", styleId))
|
|
342
|
+
.setZOrder(spec.int("zIndex", 0))
|
|
343
|
+
.setVisible(spec.boolean("visible", true))
|
|
344
|
+
geometry.list("parts").forEach { options.addPolyline(mapLine(it), it.int("styleIndex", 0), 0) }
|
|
345
|
+
ShapeEntry(layerId, geometryType, layer.addPolyline(options) ?: invalid("Could not create polyline $id."))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
"modelPolyline" -> {
|
|
349
|
+
rejectAndroidOrientation(geometry)
|
|
350
|
+
val options =
|
|
351
|
+
PolylineOptions
|
|
352
|
+
.from(
|
|
353
|
+
nativeId,
|
|
354
|
+
).setStylesSet(targetPolylineStyles[styleId] ?: unknown("polyline style set", styleId))
|
|
355
|
+
.setZOrder(spec.int("zIndex", 0))
|
|
356
|
+
.setVisible(spec.boolean("visible", true))
|
|
357
|
+
geometry
|
|
358
|
+
.list(
|
|
359
|
+
"parts",
|
|
360
|
+
).forEach { options.addPolyline(dotLine(geometry.coordinate("base"), it), it.int("styleIndex", 0), 0) }
|
|
361
|
+
ShapeEntry(layerId, geometryType, layer.addPolyline(options) ?: invalid("Could not create model polyline $id."))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
else -> {
|
|
365
|
+
invalid("Unsupported normalized shape geometry.")
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
targetShapes[id] = entry
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private fun normalizeGeometry(geometry: Map<String, Any?>): Map<String, Any?> =
|
|
372
|
+
when (geometry.string("type")) {
|
|
373
|
+
"modelCircle" -> geometry.primitiveModel(circle = true)
|
|
374
|
+
"modelRectangle" -> geometry.primitiveModel(circle = false)
|
|
375
|
+
"geographicCircle" -> geometry.primitiveGeographic(circle = true)
|
|
376
|
+
"geographicRectangle" -> geometry.primitiveGeographic(circle = false)
|
|
377
|
+
"geographicCurve" -> geometry.curveGeometry()
|
|
378
|
+
"modelArc" -> geometry.modelArcGeometry()
|
|
379
|
+
"geographicArc" -> geometry.geographicArcGeometry()
|
|
380
|
+
else -> geometry
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private fun changeData(command: Map<String, Any?>) {
|
|
384
|
+
val id = command.string("id")
|
|
385
|
+
val old = entry(id)
|
|
386
|
+
val styleId = command.string("styleSetId")
|
|
387
|
+
val geometry = normalizeGeometry(command.map("geometry"))
|
|
388
|
+
rejectAndroidOrientation(geometry)
|
|
389
|
+
val expectsPolyline = geometry.string("type").contains("Polyline") || geometry.string("type") == "geographicCurve"
|
|
390
|
+
if ((expectsPolyline && styleId !in polylineStyles) || (!expectsPolyline && styleId !in polygonStyles)) {
|
|
391
|
+
invalid("Shape $id references an incompatible style set.")
|
|
392
|
+
}
|
|
393
|
+
validateGeometry(geometry, styleCounts[styleId] ?: invalid("Unknown shape style set $styleId."))
|
|
394
|
+
val visible = old.visible()
|
|
395
|
+
val zIndex = old.zIndex()
|
|
396
|
+
val spec =
|
|
397
|
+
mapOf(
|
|
398
|
+
"id" to id,
|
|
399
|
+
"layerId" to old.layerId,
|
|
400
|
+
"styleSetId" to styleId,
|
|
401
|
+
"zIndex" to zIndex,
|
|
402
|
+
"visible" to false,
|
|
403
|
+
"geometry" to command.map("geometry"),
|
|
404
|
+
)
|
|
405
|
+
val replacement = mutableMapOf<String, ShapeEntry>()
|
|
406
|
+
addShape(
|
|
407
|
+
spec,
|
|
408
|
+
layers,
|
|
409
|
+
replacement,
|
|
410
|
+
polygonStyles,
|
|
411
|
+
polylineStyles,
|
|
412
|
+
"$nativePrefix-replacement-${replacementSequence++}",
|
|
413
|
+
)
|
|
414
|
+
remove(id)
|
|
415
|
+
replacement.getValue(id).also {
|
|
416
|
+
it.setVisible(visible)
|
|
417
|
+
shapes[id] = it
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private fun startWave(animation: Map<String, Any?>) {
|
|
422
|
+
val id = animation.string("animationId")
|
|
423
|
+
if (id.isBlank()) invalid("Shape animationId must not be empty.")
|
|
424
|
+
if (animators.containsKey(id)) invalid("Shape animation $id already exists.")
|
|
425
|
+
val definition = mapCanonicalValue(animation - "animationId")
|
|
426
|
+
val nativeId =
|
|
427
|
+
animatorIdsByDefinition.getOrPut(definition) {
|
|
428
|
+
"rnk-shape-${System.identityHashCode(this)}-${animationSequence++}"
|
|
429
|
+
}
|
|
430
|
+
if (nativeId in animationNativeIds.values) invalid("An equivalent shape animation is already running.")
|
|
431
|
+
val polygons =
|
|
432
|
+
animation.stringList("shapeIds").map { shapeId ->
|
|
433
|
+
val native = entry(shapeId).native
|
|
434
|
+
native as? Polygon ?: invalid("Wave animation supports polygons only: $shapeId.")
|
|
435
|
+
}
|
|
436
|
+
if (polygons.isEmpty()) invalid("Shape wave animation requires shapeIds.")
|
|
437
|
+
val animator =
|
|
438
|
+
animatorCache[nativeId] ?: run {
|
|
439
|
+
val effect = mapShapeCircleWaves(animation + ("animationId" to nativeId), density)
|
|
440
|
+
(manager.addAnimator(effect) ?: invalid("Could not create shape animation $id.")).also {
|
|
441
|
+
animatorCache[nativeId] = it
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
animator.removeAllPolygon()
|
|
445
|
+
animator.addPolygons(polygons)
|
|
446
|
+
animators[id] = animator
|
|
447
|
+
animationTargets[id] = animation.stringList("shapeIds").toSet()
|
|
448
|
+
animationNativeIds[id] = nativeId
|
|
449
|
+
animator.start()
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private fun stopAnimation(id: String) {
|
|
453
|
+
val animator = animators.remove(id) ?: unknown("shape animation", id)
|
|
454
|
+
animationTargets.remove(id)
|
|
455
|
+
animationNativeIds.remove(id)
|
|
456
|
+
animator.stop()
|
|
457
|
+
animator.removeAllPolygon()
|
|
458
|
+
emit("onShapeAnimationStop", mapOf("animationId" to id))
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private fun remove(id: String) {
|
|
462
|
+
animationTargets
|
|
463
|
+
.filterValues { id in it }
|
|
464
|
+
.keys
|
|
465
|
+
.toList()
|
|
466
|
+
.forEach(::stopAnimation)
|
|
467
|
+
val shape = shapes.remove(id) ?: unknown("shape", id)
|
|
468
|
+
shape.remove()
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private fun disposeObjects() {
|
|
472
|
+
animators.values.forEach {
|
|
473
|
+
it.stop()
|
|
474
|
+
it.removeAllPolygon()
|
|
475
|
+
}
|
|
476
|
+
animators.clear()
|
|
477
|
+
animationTargets.clear()
|
|
478
|
+
animationNativeIds.clear()
|
|
479
|
+
layers.values.forEach(manager::remove)
|
|
480
|
+
layers.clear()
|
|
481
|
+
shapes.clear()
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private fun state(id: String): Map<String, Any?> {
|
|
485
|
+
val value = entry(id)
|
|
486
|
+
return mapOf(
|
|
487
|
+
"type" to "state",
|
|
488
|
+
"id" to id,
|
|
489
|
+
"layerId" to value.layerId,
|
|
490
|
+
"visible" to value.visible(),
|
|
491
|
+
"zIndex" to value.zIndex(),
|
|
492
|
+
"geometryType" to value.geometryType,
|
|
493
|
+
"position" to null,
|
|
494
|
+
"orientation" to null,
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private fun counts() = mapOf("type" to "counts", "layers" to layers.size, "shapes" to shapes.size, "animations" to animators.size)
|
|
499
|
+
|
|
500
|
+
private fun none() = mapOf<String, Any?>("type" to "none")
|
|
501
|
+
|
|
502
|
+
private fun entry(id: String) = shapes[id] ?: unknown("shape", id)
|
|
503
|
+
|
|
504
|
+
private fun entries(ids: List<String>): List<ShapeEntry> {
|
|
505
|
+
if (ids.toSet().size != ids.size) invalid("Shape command IDs must be unique.")
|
|
506
|
+
return ids.map(::entry)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private data class ShapeEntry(
|
|
510
|
+
val layerId: String,
|
|
511
|
+
val geometryType: String,
|
|
512
|
+
val native: Any,
|
|
513
|
+
) {
|
|
514
|
+
fun setVisible(visible: Boolean) =
|
|
515
|
+
when (val value = native) {
|
|
516
|
+
is Polygon -> if (visible) value.show() else value.hide()
|
|
517
|
+
is Polyline -> if (visible) value.show() else value.hide()
|
|
518
|
+
else -> Unit
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
fun visible() =
|
|
522
|
+
when (val value = native) {
|
|
523
|
+
is Polygon -> value.isShow
|
|
524
|
+
is Polyline -> value.isShow
|
|
525
|
+
else -> false
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
fun zIndex() =
|
|
529
|
+
when (val value = native) {
|
|
530
|
+
is Polygon -> value.zOrder
|
|
531
|
+
is Polyline -> value.zOrder
|
|
532
|
+
else -> 0
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
fun setPosition(position: LatLng) =
|
|
536
|
+
when (val value = native) {
|
|
537
|
+
is Polygon -> value.setPosition(position)
|
|
538
|
+
is Polyline -> value.setPosition(position)
|
|
539
|
+
else -> Unit
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
fun setZIndex(zIndex: Int) =
|
|
543
|
+
when (val value = native) {
|
|
544
|
+
is Polygon -> value.setZOrder(zIndex)
|
|
545
|
+
is Polyline -> value.setZOrder(zIndex)
|
|
546
|
+
else -> Unit
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
fun remove() =
|
|
550
|
+
when (val value = native) {
|
|
551
|
+
is Polygon -> value.remove()
|
|
552
|
+
is Polyline -> value.remove()
|
|
553
|
+
else -> Unit
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private const val SHAPE_ERROR = "ERR_KAKAO_MAP_SHAPE"
|
|
559
|
+
private const val DEFAULT_SHAPE_LAYER_Z_INDEX = 10_000
|
|
560
|
+
private const val EARTH_RADIUS_METERS = 6_378_137.0
|
|
561
|
+
|
|
562
|
+
internal fun mapStyleNativeID(
|
|
563
|
+
prefix: String,
|
|
564
|
+
spec: Map<String, Any?>,
|
|
565
|
+
): String {
|
|
566
|
+
val canonical = mapCanonicalValue(spec)
|
|
567
|
+
return "$prefix-${canonical.length}-${canonical.hashCode().toUInt().toString(16)}"
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
internal fun mapCanonicalValue(value: Any?): String =
|
|
571
|
+
when (value) {
|
|
572
|
+
null -> {
|
|
573
|
+
"n"
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
is Map<*, *> -> {
|
|
577
|
+
value.entries
|
|
578
|
+
.sortedBy { it.key.toString() }
|
|
579
|
+
.joinToString(prefix = "{", postfix = "}") { entry ->
|
|
580
|
+
val key = entry.key.toString()
|
|
581
|
+
"${key.length}:$key=${mapCanonicalValue(entry.value)}"
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
is List<*> -> {
|
|
586
|
+
value.joinToString(prefix = "[", postfix = "]", transform = ::mapCanonicalValue)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
is String -> {
|
|
590
|
+
"s${value.length}:$value"
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
is Number -> {
|
|
594
|
+
"d${value.toDouble()}"
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
is Boolean -> {
|
|
598
|
+
if (value) "t" else "f"
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
else -> {
|
|
602
|
+
"o${value.toString().length}:$value"
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private fun validateGeometry(
|
|
607
|
+
geometry: Map<String, Any?>,
|
|
608
|
+
styleCount: Int,
|
|
609
|
+
) {
|
|
610
|
+
val type = geometry.string("type")
|
|
611
|
+
val parts =
|
|
612
|
+
when (type) {
|
|
613
|
+
"geographicPolygon", "modelPolygon", "geographicPolyline", "modelPolyline" -> geometry.list("parts")
|
|
614
|
+
else -> emptyList()
|
|
615
|
+
}
|
|
616
|
+
if (parts.isEmpty() &&
|
|
617
|
+
type in setOf("geographicPolygon", "modelPolygon", "geographicPolyline", "modelPolyline")
|
|
618
|
+
) {
|
|
619
|
+
invalid("Shape geometry requires parts.")
|
|
620
|
+
}
|
|
621
|
+
parts.forEach { part ->
|
|
622
|
+
val index = part.int("styleIndex", 0)
|
|
623
|
+
if (index !in 0 until styleCount) invalid("Shape styleIndex $index is out of range.")
|
|
624
|
+
val points = (part["outer"] ?: part["points"]) as? List<*> ?: invalid("Shape part requires points.")
|
|
625
|
+
val minimum = if (type.contains("Polygon")) 3 else 2
|
|
626
|
+
if (points.size < minimum) invalid("Shape part requires at least $minimum points.")
|
|
627
|
+
if (type.contains("Polygon")) {
|
|
628
|
+
part.listOrNull("holes")?.forEach { hole ->
|
|
629
|
+
if (hole.size < 3) invalid("Shape polygon holes require at least 3 points.")
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
when (type) {
|
|
634
|
+
"geographicPolygon" -> {
|
|
635
|
+
parts.forEach(::mapShapeMapPoints)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
"modelPolygon" -> {
|
|
639
|
+
val base = geometry.coordinate("base")
|
|
640
|
+
parts.forEach { mapShapeDotPoints(base, it) }
|
|
641
|
+
geometry.double("orientation", 0.0)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
"geographicPolyline", "geographicCurve" -> {
|
|
645
|
+
parts.forEach(::mapLine)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
"modelPolyline" -> {
|
|
649
|
+
val base = geometry.coordinate("base")
|
|
650
|
+
parts.forEach { dotLine(base, it) }
|
|
651
|
+
geometry.double("orientation", 0.0)
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private fun Map<String, Any?>.primitiveModel(circle: Boolean): Map<String, Any?> {
|
|
657
|
+
val base = map("base")
|
|
658
|
+
val points =
|
|
659
|
+
if (circle) {
|
|
660
|
+
circlePoints(
|
|
661
|
+
double("radius"),
|
|
662
|
+
int("pointCount", 64),
|
|
663
|
+
boolean("clockwise", true),
|
|
664
|
+
)
|
|
665
|
+
} else {
|
|
666
|
+
rectanglePoints(double("width"), double("height"), boolean("clockwise", true))
|
|
667
|
+
}
|
|
668
|
+
return mapOf(
|
|
669
|
+
"type" to "modelPolygon",
|
|
670
|
+
"base" to base,
|
|
671
|
+
"orientation" to double("orientation", 0.0),
|
|
672
|
+
"parts" to listOf(mapOf("outer" to points, "styleIndex" to int("styleIndex", 0))),
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private fun Map<String, Any?>.primitiveGeographic(circle: Boolean): Map<String, Any?> {
|
|
677
|
+
val center = coordinate("center")
|
|
678
|
+
val points =
|
|
679
|
+
if (circle) {
|
|
680
|
+
geographicCircle(
|
|
681
|
+
center,
|
|
682
|
+
double("radius"),
|
|
683
|
+
int("pointCount", 64),
|
|
684
|
+
boolean("clockwise", true),
|
|
685
|
+
)
|
|
686
|
+
} else {
|
|
687
|
+
geographicRectangle(center, double("width"), double("height"), boolean("clockwise", true))
|
|
688
|
+
}
|
|
689
|
+
return mapOf("type" to "geographicPolygon", "parts" to listOf(mapOf("outer" to points, "styleIndex" to int("styleIndex", 0))))
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
private fun Map<String, Any?>.curveGeometry(): Map<String, Any?> {
|
|
693
|
+
val start = coordinate("start")
|
|
694
|
+
val end = coordinate("end")
|
|
695
|
+
val left =
|
|
696
|
+
when (string("side")) {
|
|
697
|
+
"left" -> true
|
|
698
|
+
"right" -> false
|
|
699
|
+
else -> invalid("Curve side must be left or right.")
|
|
700
|
+
}
|
|
701
|
+
val points = quadraticCurve(start, end, left)
|
|
702
|
+
return mapOf("type" to "geographicCurve", "parts" to listOf(mapOf("points" to points, "styleIndex" to int("styleIndex", 0))))
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private fun Map<String, Any?>.modelArcGeometry(): Map<String, Any?> =
|
|
706
|
+
mapOf(
|
|
707
|
+
"type" to "modelPolyline",
|
|
708
|
+
"base" to map("base"),
|
|
709
|
+
"orientation" to double("orientation", 0.0),
|
|
710
|
+
"parts" to listOf(mapOf("points" to arcPoints(), "styleIndex" to int("styleIndex", 0))),
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
private fun Map<String, Any?>.geographicArcGeometry(): Map<String, Any?> {
|
|
714
|
+
val center = coordinate("center")
|
|
715
|
+
return mapOf(
|
|
716
|
+
"type" to "geographicPolyline",
|
|
717
|
+
"parts" to
|
|
718
|
+
listOf(
|
|
719
|
+
mapOf(
|
|
720
|
+
"points" to arcPoints().map { offset(center, it.getValue("x"), it.getValue("y")) },
|
|
721
|
+
"styleIndex" to int("styleIndex", 0),
|
|
722
|
+
),
|
|
723
|
+
),
|
|
724
|
+
)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
private fun Map<String, Any?>.arcPoints(): List<Map<String, Double>> {
|
|
728
|
+
val radius = double("radius")
|
|
729
|
+
val count = int("pointCount", 64)
|
|
730
|
+
if (radius <= 0 || count < 2) invalid("Arc radius must be positive and pointCount at least 2.")
|
|
731
|
+
val startDegrees = double("startAngle")
|
|
732
|
+
val endDegrees = double("endAngle")
|
|
733
|
+
val clockwise = boolean("clockwise", true)
|
|
734
|
+
val (start, sweep) = arcAngles(startDegrees, endDegrees, clockwise)
|
|
735
|
+
return List(count) { index ->
|
|
736
|
+
val angle = start + sweep * index / (count - 1)
|
|
737
|
+
mapOf("x" to radius * sin(angle), "y" to radius * cos(angle))
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
private fun arcAngles(
|
|
742
|
+
start: Double,
|
|
743
|
+
end: Double,
|
|
744
|
+
clockwise: Boolean,
|
|
745
|
+
): Pair<Double, Double> {
|
|
746
|
+
val normalizedStart = normalizedDegrees(start)
|
|
747
|
+
val normalizedEnd = normalizedDegrees(end)
|
|
748
|
+
val delta = normalizedEnd - normalizedStart
|
|
749
|
+
val sweep =
|
|
750
|
+
if (delta == 0.0) {
|
|
751
|
+
if (start == end) {
|
|
752
|
+
0.0
|
|
753
|
+
} else if (clockwise) {
|
|
754
|
+
360.0
|
|
755
|
+
} else {
|
|
756
|
+
-360.0
|
|
757
|
+
}
|
|
758
|
+
} else if (clockwise) {
|
|
759
|
+
if (delta > 0) delta else delta + 360.0
|
|
760
|
+
} else {
|
|
761
|
+
if (delta < 0) delta else delta - 360.0
|
|
762
|
+
}
|
|
763
|
+
return Math.toRadians(normalizedStart) to Math.toRadians(sweep)
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
private fun normalizedDegrees(value: Double): Double {
|
|
767
|
+
val remainder = value % 360.0
|
|
768
|
+
return if (remainder < 0) remainder + 360.0 else remainder
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
internal fun mapShapeMapPoints(part: Map<String, Any?>): MapPoints {
|
|
772
|
+
val value = MapPoints.fromLatLng(part.coordinateList("outer"))
|
|
773
|
+
val holes =
|
|
774
|
+
part
|
|
775
|
+
.listOrNull("holes")
|
|
776
|
+
?.map { hole -> LatLngVertex.from(hole.map { it.coordinateValue() }) }
|
|
777
|
+
.orEmpty()
|
|
778
|
+
if (holes.isNotEmpty()) value.setHolePoints(*holes.toTypedArray())
|
|
779
|
+
return value.setMergeOverlappingHoles(part.boolean("mergeOverlappedHoles", false))
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
private fun mapLine(part: Map<String, Any?>) = MapPoints.fromLatLng(part.coordinateList("points"))
|
|
783
|
+
|
|
784
|
+
internal fun mapShapeDotPoints(
|
|
785
|
+
base: LatLng,
|
|
786
|
+
part: Map<String, Any?>,
|
|
787
|
+
): DotPoints {
|
|
788
|
+
val value = DotPoints.fromPoints(base, part.pointList("outer"), true)
|
|
789
|
+
val holes =
|
|
790
|
+
part
|
|
791
|
+
.listOrNull("holes")
|
|
792
|
+
?.map { hole -> PointVertex.fromPoint(hole.map { point -> point.pointValue() }, true) }
|
|
793
|
+
.orEmpty()
|
|
794
|
+
if (holes.isNotEmpty()) value.setHolePoints(*holes.toTypedArray())
|
|
795
|
+
return value.setMergeOverlappingHoles(part.boolean("mergeOverlappedHoles", false))
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
private fun dotLine(
|
|
799
|
+
base: LatLng,
|
|
800
|
+
part: Map<String, Any?>,
|
|
801
|
+
) = DotPoints.fromPoints(base, part.pointList("points"), true)
|
|
802
|
+
|
|
803
|
+
private fun layerPass(value: String) =
|
|
804
|
+
when (value) {
|
|
805
|
+
"default" -> ShapeLayerPass.Default
|
|
806
|
+
"overlay" -> ShapeLayerPass.Overlay
|
|
807
|
+
"route" -> ShapeLayerPass.Route
|
|
808
|
+
else -> invalid("Unknown shape layer pass $value.")
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private fun polylineCap(value: String) =
|
|
812
|
+
when (value) {
|
|
813
|
+
"butt" -> PolylineCap.Butt
|
|
814
|
+
"round" -> PolylineCap.Round
|
|
815
|
+
"square" -> PolylineCap.Square
|
|
816
|
+
else -> invalid("Unknown polyline cap $value.")
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
private fun interpolation(value: String) =
|
|
820
|
+
when (value) {
|
|
821
|
+
"linear" -> Interpolation.Linear
|
|
822
|
+
"cubicIn" -> Interpolation.CubicIn
|
|
823
|
+
"cubicOut" -> Interpolation.CubicOut
|
|
824
|
+
"cubicInOut" -> Interpolation.CubicInOut
|
|
825
|
+
else -> invalid("Unknown interpolation $value.")
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
internal fun mapShapePolygonStylesSet(
|
|
829
|
+
spec: Map<String, Any?>,
|
|
830
|
+
density: Float = 1f,
|
|
831
|
+
): PolygonStylesSet =
|
|
832
|
+
PolygonStylesSet.from(
|
|
833
|
+
spec.string("id"),
|
|
834
|
+
spec.list("styles").map { style ->
|
|
835
|
+
val levels = style.list("levels")
|
|
836
|
+
if (levels.isEmpty()) invalid("Polygon styles require levels.")
|
|
837
|
+
PolygonStyles.from(
|
|
838
|
+
levels.map { level ->
|
|
839
|
+
val strokeWidth = level.float("strokeWidth", 0f)
|
|
840
|
+
if (strokeWidth < 0) invalid("Polygon strokeWidth must be non-negative.")
|
|
841
|
+
PolygonStyle.from(
|
|
842
|
+
level.int("level"),
|
|
843
|
+
level.color("fillColor"),
|
|
844
|
+
strokeWidth * density,
|
|
845
|
+
level.color("strokeColor", Color.TRANSPARENT),
|
|
846
|
+
)
|
|
847
|
+
},
|
|
848
|
+
)
|
|
849
|
+
},
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
internal fun mapShapeCircleWaves(
|
|
853
|
+
animation: Map<String, Any?>,
|
|
854
|
+
density: Float = 1f,
|
|
855
|
+
): CircleWaves {
|
|
856
|
+
val duration = animation.int("duration")
|
|
857
|
+
if (duration <= 0) invalid("Shape animation duration must be positive.")
|
|
858
|
+
val repeat = animation.int("repeat", 1)
|
|
859
|
+
if (repeat <= 0) invalid("Shape animation repeat must be positive.")
|
|
860
|
+
val waves =
|
|
861
|
+
animation.list("frames").map { frame ->
|
|
862
|
+
val startAlpha = frame.float("startAlpha")
|
|
863
|
+
val endAlpha = frame.float("endAlpha")
|
|
864
|
+
val startRadius = frame.float("startRadius")
|
|
865
|
+
val endRadius = frame.float("endRadius")
|
|
866
|
+
if (startAlpha !in 0f..1f || endAlpha !in 0f..1f) invalid("Shape animation alpha must be between 0 and 1.")
|
|
867
|
+
if (startRadius < 0f || endRadius < 0f) invalid("Shape animation radius must be non-negative.")
|
|
868
|
+
CircleWave.from(
|
|
869
|
+
frame.int("level"),
|
|
870
|
+
startAlpha,
|
|
871
|
+
endAlpha,
|
|
872
|
+
startRadius * density,
|
|
873
|
+
endRadius * density,
|
|
874
|
+
)
|
|
875
|
+
}
|
|
876
|
+
if (waves.isEmpty()) invalid("Shape wave animation requires frames.")
|
|
877
|
+
return CircleWaves
|
|
878
|
+
.from(animation.string("animationId"), *waves.toTypedArray())
|
|
879
|
+
.setDuration(duration)
|
|
880
|
+
.setRepeatCount(repeat)
|
|
881
|
+
.setHideShapeAtStop(animation.boolean("hideAtStop", false))
|
|
882
|
+
.setInterpolation(interpolation(animation.stringOrNull("interpolation") ?: "linear"))
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
private fun rejectAndroidOrientation(geometry: Map<String, Any?>) {
|
|
886
|
+
if (geometry.double("orientation", 0.0) != 0.0) throw unsupported("Android Kakao Maps does not expose shape orientation.")
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
private fun circlePoints(
|
|
890
|
+
radius: Double,
|
|
891
|
+
count: Int,
|
|
892
|
+
clockwise: Boolean,
|
|
893
|
+
): List<Map<String, Double>> {
|
|
894
|
+
if (radius <= 0 || count < 3) invalid("Circle radius must be positive and pointCount at least 3.")
|
|
895
|
+
val sign = if (clockwise) 1 else -1
|
|
896
|
+
return List(count) { index ->
|
|
897
|
+
val angle = sign * 2 * Math.PI * index / count
|
|
898
|
+
mapOf("x" to radius * cos(angle), "y" to radius * sin(angle))
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
private fun rectanglePoints(
|
|
903
|
+
width: Double,
|
|
904
|
+
height: Double,
|
|
905
|
+
clockwise: Boolean,
|
|
906
|
+
): List<Map<String, Double>> {
|
|
907
|
+
if (width <= 0 || height <= 0) invalid("Rectangle dimensions must be positive.")
|
|
908
|
+
val points =
|
|
909
|
+
listOf(
|
|
910
|
+
mapOf("x" to -width / 2, "y" to -height / 2),
|
|
911
|
+
mapOf("x" to width / 2, "y" to -height / 2),
|
|
912
|
+
mapOf(
|
|
913
|
+
"x" to width / 2,
|
|
914
|
+
"y" to height / 2,
|
|
915
|
+
),
|
|
916
|
+
mapOf("x" to -width / 2, "y" to height / 2),
|
|
917
|
+
)
|
|
918
|
+
return if (clockwise) points else points.reversed()
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
private fun geographicCircle(
|
|
922
|
+
center: LatLng,
|
|
923
|
+
radius: Double,
|
|
924
|
+
count: Int,
|
|
925
|
+
clockwise: Boolean,
|
|
926
|
+
) = circlePoints(radius, count, clockwise).map { offset(center, it.getValue("x"), it.getValue("y")) }
|
|
927
|
+
|
|
928
|
+
private fun geographicRectangle(
|
|
929
|
+
center: LatLng,
|
|
930
|
+
width: Double,
|
|
931
|
+
height: Double,
|
|
932
|
+
clockwise: Boolean,
|
|
933
|
+
) = rectanglePoints(width, height, clockwise).map { offset(center, it.getValue("x"), it.getValue("y")) }
|
|
934
|
+
|
|
935
|
+
private fun offset(
|
|
936
|
+
center: LatLng,
|
|
937
|
+
east: Double,
|
|
938
|
+
north: Double,
|
|
939
|
+
): Map<String, Double> {
|
|
940
|
+
val distance = kotlin.math.sqrt(east * east + north * north)
|
|
941
|
+
val bearing = atan2(east, north)
|
|
942
|
+
val latitude = Math.toRadians(center.latitude)
|
|
943
|
+
val longitude = Math.toRadians(center.longitude)
|
|
944
|
+
val angular = distance / EARTH_RADIUS_METERS
|
|
945
|
+
val targetLatitude = asin(sin(latitude) * cos(angular) + cos(latitude) * sin(angular) * cos(bearing))
|
|
946
|
+
val targetLongitude = longitude + atan2(sin(bearing) * sin(angular) * cos(latitude), cos(angular) - sin(latitude) * sin(targetLatitude))
|
|
947
|
+
return mapOf("latitude" to Math.toDegrees(targetLatitude), "longitude" to Math.toDegrees(targetLongitude))
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
private fun quadraticCurve(
|
|
951
|
+
start: LatLng,
|
|
952
|
+
end: LatLng,
|
|
953
|
+
left: Boolean,
|
|
954
|
+
): List<Map<String, Double>> {
|
|
955
|
+
val dx = end.longitude - start.longitude
|
|
956
|
+
val dy = end.latitude - start.latitude
|
|
957
|
+
val side = if (left) 0.25 else -0.25
|
|
958
|
+
val controlLat = (start.latitude + end.latitude) / 2 + dx * side
|
|
959
|
+
val controlLng = (start.longitude + end.longitude) / 2 - dy * side
|
|
960
|
+
return List(33) { index ->
|
|
961
|
+
val t = index / 32.0
|
|
962
|
+
val u = 1 - t
|
|
963
|
+
mapOf(
|
|
964
|
+
"latitude" to u * u * start.latitude + 2 * u * t * controlLat + t * t * end.latitude,
|
|
965
|
+
"longitude" to u * u * start.longitude + 2 * u * t * controlLng + t * t * end.longitude,
|
|
966
|
+
)
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
private fun requireUnique(
|
|
971
|
+
values: List<Map<String, Any?>>,
|
|
972
|
+
key: String,
|
|
973
|
+
noun: String,
|
|
974
|
+
) {
|
|
975
|
+
val ids = values.map { it.string(key) }
|
|
976
|
+
if (ids.any(String::isBlank)) invalid("$noun ID must not be empty.")
|
|
977
|
+
if (ids.toSet().size != ids.size) invalid("Duplicate $noun ID.")
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
private fun Map<String, Any?>.map(key: String) = this[key] as? Map<String, Any?> ?: invalid("$key must be an object.")
|
|
981
|
+
|
|
982
|
+
private fun Map<String, Any?>.list(key: String): List<Map<String, Any?>> {
|
|
983
|
+
if (!containsKey(key)) return emptyList()
|
|
984
|
+
val raw = this[key] as? List<*> ?: invalid("$key must be an array of objects.")
|
|
985
|
+
return raw.map { it as? Map<String, Any?> ?: invalid("$key must contain objects.") }
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
private fun Map<String, Any?>.listOrNull(key: String): List<List<*>>? {
|
|
989
|
+
if (!containsKey(key)) return null
|
|
990
|
+
val raw = this[key] as? List<*> ?: invalid("$key must be an array of arrays.")
|
|
991
|
+
return raw.map {
|
|
992
|
+
it as? List<*>
|
|
993
|
+
?: invalid("$key must contain arrays.")
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
private fun Map<String, Any?>.string(key: String) = this[key] as? String ?: invalid("$key must be a string.")
|
|
998
|
+
|
|
999
|
+
private fun Map<String, Any?>.stringOrNull(key: String): String? {
|
|
1000
|
+
if (!containsKey(key)) return null
|
|
1001
|
+
return this[key] as? String ?: invalid("$key must be a string.")
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
private fun Map<String, Any?>.stringList(key: String) =
|
|
1005
|
+
(this[key] as? List<*>)?.map { it as? String ?: invalid("$key must contain strings.") } ?: invalid("$key must be an array.")
|
|
1006
|
+
|
|
1007
|
+
private fun Map<String, Any?>.boolean(
|
|
1008
|
+
key: String,
|
|
1009
|
+
fallback: Boolean? = null,
|
|
1010
|
+
): Boolean {
|
|
1011
|
+
if (!containsKey(key)) return fallback ?: invalid("$key must be a boolean.")
|
|
1012
|
+
return this[key] as? Boolean ?: invalid("$key must be a boolean.")
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
private fun Map<String, Any?>.double(
|
|
1016
|
+
key: String,
|
|
1017
|
+
fallback: Double? = null,
|
|
1018
|
+
): Double {
|
|
1019
|
+
if (!containsKey(key)) return fallback ?: invalid("$key must be finite.")
|
|
1020
|
+
return (this[key] as? Number)?.toDouble()?.takeIf { it.isFinite() } ?: invalid("$key must be finite.")
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
private fun Map<String, Any?>.float(
|
|
1024
|
+
key: String,
|
|
1025
|
+
fallback: Float? = null,
|
|
1026
|
+
) = double(key, fallback?.toDouble()).toFloat()
|
|
1027
|
+
|
|
1028
|
+
private fun Map<String, Any?>.int(
|
|
1029
|
+
key: String,
|
|
1030
|
+
fallback: Int? = null,
|
|
1031
|
+
): Int {
|
|
1032
|
+
val value = double(key, fallback?.toDouble())
|
|
1033
|
+
if (value % 1 !=
|
|
1034
|
+
0.0 ||
|
|
1035
|
+
value !in Int.MIN_VALUE.toDouble()..Int.MAX_VALUE.toDouble()
|
|
1036
|
+
) {
|
|
1037
|
+
invalid("$key must be an integer.")
|
|
1038
|
+
}
|
|
1039
|
+
return value.toInt()
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
private fun Map<String, Any?>.color(
|
|
1043
|
+
key: String,
|
|
1044
|
+
fallback: Int? = null,
|
|
1045
|
+
): Int {
|
|
1046
|
+
if (!containsKey(key)) return fallback ?: invalid("$key must be a color.")
|
|
1047
|
+
return (this[key] as? String)?.let(::parseColor) ?: invalid("$key must be a color.")
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
private fun Map<String, Any?>.coordinate(key: String) = map(key).coordinateValue()
|
|
1051
|
+
|
|
1052
|
+
private fun Map<String, Any?>.coordinateList(key: String) =
|
|
1053
|
+
(this[key] as? List<*>)?.map { it.coordinateValue() } ?: invalid("$key must be an array.")
|
|
1054
|
+
|
|
1055
|
+
private fun Map<String, Any?>.pointList(key: String) = (this[key] as? List<*>)?.map { it.pointValue() } ?: invalid("$key must be an array.")
|
|
1056
|
+
|
|
1057
|
+
private fun Any?.coordinateValue(): LatLng {
|
|
1058
|
+
val value =
|
|
1059
|
+
this as? Map<String, Any?> ?: invalid("Coordinate must be an object.")
|
|
1060
|
+
val latitude = value.double("latitude")
|
|
1061
|
+
val longitude = value.double("longitude")
|
|
1062
|
+
if (latitude !in
|
|
1063
|
+
-90.0..90.0 ||
|
|
1064
|
+
longitude !in -180.0..180.0
|
|
1065
|
+
) {
|
|
1066
|
+
invalid("Invalid coordinate.")
|
|
1067
|
+
}
|
|
1068
|
+
return LatLng.from(latitude, longitude)
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
private fun Any?.pointValue(): PointF {
|
|
1072
|
+
val value =
|
|
1073
|
+
this as? Map<String, Any?> ?: invalid("Point must be an object.")
|
|
1074
|
+
return PointF(value.float("x"), value.float("y"))
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
private fun parseColor(value: String): Int {
|
|
1078
|
+
if (!Regex("^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$").matches(value)) invalid("Colors must be #RRGGBB or #RRGGBBAA.")
|
|
1079
|
+
val raw = value.drop(1)
|
|
1080
|
+
return if (raw.length ==
|
|
1081
|
+
6
|
|
1082
|
+
) {
|
|
1083
|
+
Color.parseColor(value)
|
|
1084
|
+
} else {
|
|
1085
|
+
((raw.takeLast(2).toLong(16) shl 24) or raw.dropLast(2).toLong(16)).toInt()
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
private fun invalid(message: String): Nothing = throw CodedException("${SHAPE_ERROR}_INVALID_ARGUMENT", message, null)
|
|
1090
|
+
|
|
1091
|
+
private fun unsupported(message: String): CodedException = CodedException("${SHAPE_ERROR}_UNSUPPORTED", message, null)
|
|
1092
|
+
|
|
1093
|
+
private fun unknown(
|
|
1094
|
+
noun: String,
|
|
1095
|
+
id: String,
|
|
1096
|
+
): Nothing = throw CodedException("${SHAPE_ERROR}_NOT_FOUND", "Unknown $noun $id.", null)
|