@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.
Files changed (165) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +5 -38
  3. package/RNCKakaoMap.podspec +16 -52
  4. package/android/build.gradle +12 -107
  5. package/android/consumer-rules.pro +2 -0
  6. package/android/src/main/AndroidManifest.xml +3 -1
  7. package/android/src/main/java/net/mjstudio/rnkakao/map/CameraOptions.kt +61 -0
  8. package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapModule.kt +131 -0
  9. package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapView.kt +1257 -0
  10. package/android/src/main/java/net/mjstudio/rnkakao/map/MapCameraCommands.kt +306 -0
  11. package/android/src/main/java/net/mjstudio/rnkakao/map/MapDimScreen.kt +431 -0
  12. package/android/src/main/java/net/mjstudio/rnkakao/map/MapGui.kt +899 -0
  13. package/android/src/main/java/net/mjstudio/rnkakao/map/MapImageLoader.kt +232 -0
  14. package/android/src/main/java/net/mjstudio/rnkakao/map/MapLabels.kt +1572 -0
  15. package/android/src/main/java/net/mjstudio/rnkakao/map/MapOptions.kt +360 -0
  16. package/android/src/main/java/net/mjstudio/rnkakao/map/MapRoutes.kt +805 -0
  17. package/android/src/main/java/net/mjstudio/rnkakao/map/MapShapes.kt +1096 -0
  18. package/android/src/main/java/net/mjstudio/rnkakao/map/MapUtilities.kt +495 -0
  19. package/expo-module.config.json +5 -0
  20. package/ios/CameraOptions.swift +41 -0
  21. package/ios/KakaoMapModule.swift +204 -0
  22. package/ios/KakaoMapView.swift +1018 -0
  23. package/ios/MapCameraCommands.swift +451 -0
  24. package/ios/MapDimScreen.swift +463 -0
  25. package/ios/MapGui.swift +1099 -0
  26. package/ios/MapImageLoader.swift +177 -0
  27. package/ios/MapLabels.swift +1676 -0
  28. package/ios/MapOptions.swift +374 -0
  29. package/ios/MapRoutes.swift +903 -0
  30. package/ios/MapShapes.swift +1294 -0
  31. package/ios/MapUnits.swift +20 -0
  32. package/ios/MapUtilities.swift +365 -0
  33. package/lib/commonjs/index.js +180 -29
  34. package/lib/commonjs/index.js.map +1 -1
  35. package/lib/commonjs/index.web.js +18 -0
  36. package/lib/commonjs/index.web.js.map +1 -0
  37. package/lib/commonjs/package.json +1 -0
  38. package/lib/commonjs/types-assets.js +2 -0
  39. package/lib/commonjs/types-assets.js.map +1 -0
  40. package/lib/commonjs/types-camera.js +6 -0
  41. package/lib/commonjs/types-camera.js.map +1 -0
  42. package/lib/commonjs/types-dim.js +6 -0
  43. package/lib/commonjs/types-dim.js.map +1 -0
  44. package/lib/commonjs/types-gui.js +6 -0
  45. package/lib/commonjs/types-gui.js.map +1 -0
  46. package/lib/commonjs/types-labels.js +6 -0
  47. package/lib/commonjs/types-labels.js.map +1 -0
  48. package/lib/commonjs/types-routes.js +6 -0
  49. package/lib/commonjs/types-routes.js.map +1 -0
  50. package/lib/commonjs/types-shapes.js +6 -0
  51. package/lib/commonjs/types-shapes.js.map +1 -0
  52. package/lib/commonjs/types-utilities.js +6 -0
  53. package/lib/commonjs/types-utilities.js.map +1 -0
  54. package/lib/commonjs/types.js +6 -0
  55. package/lib/commonjs/types.js.map +1 -0
  56. package/lib/module/index.js +180 -22
  57. package/lib/module/index.js.map +1 -1
  58. package/lib/module/index.web.js +13 -0
  59. package/lib/module/index.web.js.map +1 -0
  60. package/lib/module/types-assets.js +2 -0
  61. package/lib/module/types-assets.js.map +1 -0
  62. package/lib/module/types-camera.js +4 -0
  63. package/lib/module/types-camera.js.map +1 -0
  64. package/lib/module/types-dim.js +4 -0
  65. package/lib/module/types-dim.js.map +1 -0
  66. package/lib/module/types-gui.js +4 -0
  67. package/lib/module/types-gui.js.map +1 -0
  68. package/lib/module/types-labels.js +4 -0
  69. package/lib/module/types-labels.js.map +1 -0
  70. package/lib/module/types-routes.js +4 -0
  71. package/lib/module/types-routes.js.map +1 -0
  72. package/lib/module/types-shapes.js +4 -0
  73. package/lib/module/types-shapes.js.map +1 -0
  74. package/lib/module/types-utilities.js +4 -0
  75. package/lib/module/types-utilities.js.map +1 -0
  76. package/lib/module/types.js +4 -0
  77. package/lib/module/types.js.map +1 -0
  78. package/lib/typescript/src/index.d.ts +59 -9
  79. package/lib/typescript/src/index.d.ts.map +1 -1
  80. package/lib/typescript/src/types-assets.d.ts +9 -0
  81. package/lib/typescript/src/types-assets.d.ts.map +1 -0
  82. package/lib/typescript/src/types-camera.d.ts +123 -0
  83. package/lib/typescript/src/types-camera.d.ts.map +1 -0
  84. package/lib/typescript/src/types-dim.d.ts +54 -0
  85. package/lib/typescript/src/types-dim.d.ts.map +1 -0
  86. package/lib/typescript/src/types-gui.d.ts +189 -0
  87. package/lib/typescript/src/types-gui.d.ts.map +1 -0
  88. package/lib/typescript/src/types-labels.d.ts +246 -0
  89. package/lib/typescript/src/types-labels.d.ts.map +1 -0
  90. package/lib/typescript/src/types-routes.d.ts +162 -0
  91. package/lib/typescript/src/types-routes.d.ts.map +1 -0
  92. package/lib/typescript/src/types-shapes.d.ts +253 -0
  93. package/lib/typescript/src/types-shapes.d.ts.map +1 -0
  94. package/lib/typescript/src/types-utilities.d.ts +130 -0
  95. package/lib/typescript/src/types-utilities.d.ts.map +1 -0
  96. package/lib/typescript/src/types.d.ts +222 -0
  97. package/lib/typescript/src/types.d.ts.map +1 -0
  98. package/package.json +24 -77
  99. package/src/index.tsx +322 -0
  100. package/src/index.web.tsx +46 -0
  101. package/src/types-assets.ts +8 -0
  102. package/src/types-camera.ts +97 -0
  103. package/src/types-dim.ts +54 -0
  104. package/src/types-gui.ts +171 -0
  105. package/src/types-labels.ts +207 -0
  106. package/src/types-routes.ts +118 -0
  107. package/src/types-shapes.ts +244 -0
  108. package/src/types-utilities.ts +115 -0
  109. package/src/types.ts +223 -0
  110. package/android/gradle.properties +0 -1
  111. package/android/src/main/AndroidManifestNew.xml +0 -2
  112. package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapModule.kt +0 -25
  113. package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapPackage.kt +0 -35
  114. package/android/src/newarch/KakaoMapSpec.kt +0 -7
  115. package/android/src/oldarch/KakaoMapSpec.kt +0 -14
  116. package/ios/Component/Map/RNCKakaoMapView.h +0 -23
  117. package/ios/Component/Map/RNCKakaoMapView.mm +0 -209
  118. package/ios/Component/Map/RNCKakaoMapViewManager.mm +0 -19
  119. package/ios/NativeModule/RNCKakaoMap.h +0 -7
  120. package/ios/NativeModule/RNCKakaoMap.mm +0 -19
  121. package/ios/NativeModule/RNCKakaoMapManager.swift +0 -7
  122. package/ios/Util/Util.h +0 -97
  123. package/lib/commonjs/component/KakaoMapView.js +0 -54
  124. package/lib/commonjs/component/KakaoMapView.js.map +0 -1
  125. package/lib/commonjs/spec/NativeKakaoMap.js +0 -9
  126. package/lib/commonjs/spec/NativeKakaoMap.js.map +0 -1
  127. package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js +0 -15
  128. package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
  129. package/lib/commonjs/type/type.js +0 -2
  130. package/lib/commonjs/type/type.js.map +0 -1
  131. package/lib/commonjs/util/Const.js +0 -14
  132. package/lib/commonjs/util/Const.js.map +0 -1
  133. package/lib/commonjs/util/Util.js +0 -15
  134. package/lib/commonjs/util/Util.js.map +0 -1
  135. package/lib/module/component/KakaoMapView.js +0 -48
  136. package/lib/module/component/KakaoMapView.js.map +0 -1
  137. package/lib/module/spec/NativeKakaoMap.js +0 -3
  138. package/lib/module/spec/NativeKakaoMap.js.map +0 -1
  139. package/lib/module/spec/RNCKakaoMapViewNativeComponent.js +0 -10
  140. package/lib/module/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
  141. package/lib/module/type/type.js +0 -2
  142. package/lib/module/type/type.js.map +0 -1
  143. package/lib/module/util/Const.js +0 -8
  144. package/lib/module/util/Const.js.map +0 -1
  145. package/lib/module/util/Util.js +0 -9
  146. package/lib/module/util/Util.js.map +0 -1
  147. package/lib/typescript/src/component/KakaoMapView.d.ts +0 -25
  148. package/lib/typescript/src/component/KakaoMapView.d.ts.map +0 -1
  149. package/lib/typescript/src/spec/NativeKakaoMap.d.ts +0 -7
  150. package/lib/typescript/src/spec/NativeKakaoMap.d.ts.map +0 -1
  151. package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts +0 -32
  152. package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts.map +0 -1
  153. package/lib/typescript/src/type/type.d.ts +0 -8
  154. package/lib/typescript/src/type/type.d.ts.map +0 -1
  155. package/lib/typescript/src/util/Const.d.ts +0 -8
  156. package/lib/typescript/src/util/Const.d.ts.map +0 -1
  157. package/lib/typescript/src/util/Util.d.ts +0 -3
  158. package/lib/typescript/src/util/Util.d.ts.map +0 -1
  159. package/src/component/KakaoMapView.tsx +0 -82
  160. package/src/index.ts +0 -43
  161. package/src/spec/NativeKakaoMap.ts +0 -8
  162. package/src/spec/RNCKakaoMapViewNativeComponent.ts +0 -38
  163. package/src/type/type.ts +0 -7
  164. package/src/util/Const.ts +0 -7
  165. package/src/util/Util.ts +0 -11
@@ -0,0 +1,1572 @@
1
+ package net.mjstudio.rnkakao.map
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Color
5
+ import android.view.Gravity
6
+ import com.kakao.vectormap.KakaoMap
7
+ import com.kakao.vectormap.LatLng
8
+ import com.kakao.vectormap.animation.Interpolation
9
+ import com.kakao.vectormap.label.Badge
10
+ import com.kakao.vectormap.label.CompetitionType
11
+ import com.kakao.vectormap.label.CompetitionUnit
12
+ import com.kakao.vectormap.label.Label
13
+ import com.kakao.vectormap.label.LabelLayer
14
+ import com.kakao.vectormap.label.LabelLayerOptions
15
+ import com.kakao.vectormap.label.LabelOptions
16
+ import com.kakao.vectormap.label.LabelStyle
17
+ import com.kakao.vectormap.label.LabelStyles
18
+ import com.kakao.vectormap.label.LabelTextBuilder
19
+ import com.kakao.vectormap.label.LabelTextStyle
20
+ import com.kakao.vectormap.label.LabelTransition
21
+ import com.kakao.vectormap.label.LodLabel
22
+ import com.kakao.vectormap.label.LodLabelLayer
23
+ import com.kakao.vectormap.label.OrderingType
24
+ import com.kakao.vectormap.label.PathOptions
25
+ import com.kakao.vectormap.label.PolylineLabel
26
+ import com.kakao.vectormap.label.PolylineLabelOptions
27
+ import com.kakao.vectormap.label.PolylineLabelStyle
28
+ import com.kakao.vectormap.label.PolylineLabelStyles
29
+ import com.kakao.vectormap.label.TrackingManager
30
+ import com.kakao.vectormap.label.TransformMethod
31
+ import com.kakao.vectormap.label.Transition
32
+ import com.kakao.vectormap.label.animation.AlphaAnimation
33
+ import com.kakao.vectormap.label.animation.AlphaAnimations
34
+ import com.kakao.vectormap.label.animation.DropAnimation
35
+ import com.kakao.vectormap.label.animation.ScaleAlphaAnimation
36
+ import com.kakao.vectormap.label.animation.ScaleAlphaAnimations
37
+ import com.kakao.vectormap.label.animation.ScaleAnimation
38
+ import com.kakao.vectormap.label.animation.ScaleAnimations
39
+ import com.kakao.vectormap.label.animation.TransformAnimation
40
+ import com.kakao.vectormap.label.animation.TransformAnimations
41
+ import com.kakao.vectormap.shape.Polygon
42
+ import expo.modules.kotlin.exception.CodedException
43
+ import expo.modules.kotlin.records.Field
44
+ import expo.modules.kotlin.records.Record
45
+ import expo.modules.kotlin.records.Required
46
+ import java.util.UUID
47
+ import kotlin.math.PI
48
+ import kotlin.math.roundToInt
49
+
50
+ internal const val LABEL_INVALID_ARGUMENT = "ERR_KAKAO_MAP_LABEL_INVALID_ARGUMENT"
51
+ internal const val LABEL_NOT_FOUND = "ERR_KAKAO_MAP_LABEL_NOT_FOUND"
52
+ internal const val LABEL_UNSUPPORTED = "ERR_KAKAO_MAP_LABEL_UNSUPPORTED"
53
+ internal const val LABEL_APPLY_CANCELLED = "ERR_KAKAO_MAP_LABEL_APPLY_CANCELLED"
54
+ private const val DEFAULT_LABEL_LAYER_Z_ORDER = 1000
55
+ private const val MIN_CHARACTER_SPACE_PX = -2
56
+
57
+ class MapLabelPoint : Record {
58
+ @Field @Required
59
+ var latitude = Double.NaN
60
+
61
+ @Field @Required
62
+ var longitude = Double.NaN
63
+ }
64
+
65
+ class MapLabelVector : Record {
66
+ @Field @Required
67
+ var x = Double.NaN
68
+
69
+ @Field @Required
70
+ var y = Double.NaN
71
+ }
72
+
73
+ class MapLabelImage : Record {
74
+ @Field @Required
75
+ var uri = ""
76
+
77
+ @Field var width: Double? = null
78
+
79
+ @Field var height: Double? = null
80
+ }
81
+
82
+ class MapLabelTextStyleRecord : Record {
83
+ @Field var fontFamily: String? = null
84
+
85
+ @Field @Required
86
+ var fontSize = Double.NaN
87
+
88
+ @Field @Required
89
+ var color = ""
90
+
91
+ @Field var strokeColor: String? = null
92
+
93
+ @Field var strokeWidth = 0.0
94
+
95
+ @Field var characterSpacing = 0.0
96
+
97
+ @Field var lineSpacing = 1.0
98
+
99
+ @Field var aspectRatio = 1.0
100
+ }
101
+
102
+ class MapLabelTransitionRecord : Record {
103
+ @Field @Required
104
+ var entrance = "none"
105
+
106
+ @Field @Required
107
+ var exit = "none"
108
+
109
+ @Field var enableEntrance = true
110
+
111
+ @Field var enableExit = true
112
+ }
113
+
114
+ class MapLabelBadgeStyleRecord : Record {
115
+ @Field @Required
116
+ var id = ""
117
+
118
+ @Field @Required
119
+ var image = MapLabelImage()
120
+
121
+ @Field var offset: MapLabelVector? = null
122
+
123
+ @Field var zOrder = 0
124
+ }
125
+
126
+ class MapLabelPerLevelStyleRecord : Record {
127
+ @Field @Required
128
+ var level = 0
129
+
130
+ @Field var image: MapLabelImage? = null
131
+
132
+ @Field var anchor: MapLabelVector? = null
133
+
134
+ @Field var padding = 0.0
135
+
136
+ @Field var iconTransition: MapLabelTransitionRecord? = null
137
+
138
+ @Field var textTransition: MapLabelTransitionRecord? = null
139
+
140
+ @Field var textLayout = "center"
141
+
142
+ @Field var textLines: List<MapLabelTextStyleRecord> = emptyList()
143
+
144
+ @Field var badges: List<MapLabelBadgeStyleRecord> = emptyList()
145
+ }
146
+
147
+ class MapLabelStyleRecord : Record {
148
+ @Field @Required
149
+ var id = ""
150
+
151
+ @Field @Required
152
+ var levels: List<MapLabelPerLevelStyleRecord> = emptyList()
153
+ }
154
+
155
+ class MapPathTextLevelStyleRecord : Record {
156
+ @Field @Required
157
+ var level = 0
158
+
159
+ @Field @Required
160
+ var text = MapLabelTextStyleRecord()
161
+ }
162
+
163
+ class MapPathTextStyleRecord : Record {
164
+ @Field @Required
165
+ var id = ""
166
+
167
+ @Field @Required
168
+ var levels: List<MapPathTextLevelStyleRecord> = emptyList()
169
+ }
170
+
171
+ open class MapLabelLayerRecord : Record {
172
+ @Field @Required
173
+ var id = ""
174
+
175
+ @Field var visible = true
176
+
177
+ @Field var clickable = true
178
+
179
+ @Field var zOrder = DEFAULT_LABEL_LAYER_Z_ORDER
180
+
181
+ @Field var competition = "none"
182
+
183
+ @Field var competitionUnit = "iconAndText"
184
+
185
+ @Field var ordering = "rank"
186
+
187
+ @Field var competitionExceptions: List<String> = emptyList()
188
+ }
189
+
190
+ class MapLodLabelLayerRecord : MapLabelLayerRecord() {
191
+ @Field var radius = 20.0
192
+ }
193
+
194
+ class MapLabelTextRecord : Record {
195
+ @Field @Required
196
+ var value = ""
197
+
198
+ @Field var styleIndex = 0
199
+ }
200
+
201
+ class MapLabelBadgeRecord : Record {
202
+ @Field @Required
203
+ var id = ""
204
+
205
+ @Field @Required
206
+ var image = MapLabelImage()
207
+
208
+ @Field var offset: MapLabelVector? = null
209
+
210
+ @Field var zOrder = 0
211
+
212
+ @Field var visible = true
213
+ }
214
+
215
+ open class MapLabelRecord : Record {
216
+ @Field @Required
217
+ var id = ""
218
+
219
+ @Field @Required
220
+ var layerId = ""
221
+
222
+ @Field @Required
223
+ var styleId = ""
224
+
225
+ @Field @Required
226
+ var coordinate = MapLabelPoint()
227
+
228
+ @Field var text: List<MapLabelTextRecord> = emptyList()
229
+
230
+ @Field var rank = 0.0
231
+
232
+ @Field var clickable = false
233
+
234
+ @Field var visible = true
235
+
236
+ @Field var transform = "default"
237
+
238
+ @Field var pixelOffset: MapLabelVector? = null
239
+
240
+ @Field var orientation = 0.0
241
+
242
+ @Field var scale: MapLabelVector? = null
243
+
244
+ @Field var badges: List<MapLabelBadgeRecord> = emptyList()
245
+ }
246
+
247
+ class MapLodLabelRecord : MapLabelRecord()
248
+
249
+ class MapPathLabelRecord : Record {
250
+ @Field @Required
251
+ var id = ""
252
+
253
+ @Field @Required
254
+ var layerId = ""
255
+
256
+ @Field @Required
257
+ var styleId = ""
258
+
259
+ @Field @Required
260
+ var points: List<MapLabelPoint> = emptyList()
261
+
262
+ @Field @Required
263
+ var text = ""
264
+
265
+ @Field var rank = 0.0
266
+
267
+ @Field var visible = true
268
+ }
269
+
270
+ class MapLabelAnimationKeyframeRecord : Record {
271
+ @Field @Required
272
+ var duration = 0.0
273
+
274
+ @Field var interpolation = "linear"
275
+
276
+ @Field var alpha: Double? = null
277
+
278
+ @Field var scale: MapLabelVector? = null
279
+
280
+ @Field var translation: MapLabelVector? = null
281
+
282
+ @Field var rotation: Double? = null
283
+ }
284
+
285
+ class MapLabelAnimationRecord : Record {
286
+ @Field @Required
287
+ var id = ""
288
+
289
+ @Field @Required
290
+ var labelIds: List<String> = emptyList()
291
+
292
+ @Field @Required
293
+ var effect = ""
294
+
295
+ @Field @Required
296
+ var keyframes: List<MapLabelAnimationKeyframeRecord> = emptyList()
297
+
298
+ @Field var repeatCount = 1
299
+
300
+ @Field var hideAtStop = false
301
+
302
+ @Field var removeAtStop = false
303
+
304
+ @Field var resetToInitialState = false
305
+ }
306
+
307
+ class MapLabelSceneRecord : Record {
308
+ @Field var styles: List<MapLabelStyleRecord> = emptyList()
309
+
310
+ @Field var pathTextStyles: List<MapPathTextStyleRecord> = emptyList()
311
+
312
+ @Field var layers: List<MapLabelLayerRecord> = emptyList()
313
+
314
+ @Field var lodLayers: List<MapLodLabelLayerRecord> = emptyList()
315
+
316
+ @Field var labels: List<MapLabelRecord> = emptyList()
317
+
318
+ @Field var lodLabels: List<MapLodLabelRecord> = emptyList()
319
+
320
+ @Field var pathLabels: List<MapPathLabelRecord> = emptyList()
321
+
322
+ @Field var animations: List<MapLabelAnimationRecord> = emptyList()
323
+ }
324
+
325
+ class MapLabelCommandRecord : Record {
326
+ @Field @Required
327
+ var type = ""
328
+
329
+ @Field var id: String? = null
330
+
331
+ @Field var sourceId: String? = null
332
+
333
+ @Field var targetId: String? = null
334
+
335
+ @Field var badgeId: String? = null
336
+
337
+ @Field var style = false
338
+
339
+ @Field var visible: Boolean? = null
340
+
341
+ @Field var enabled: Boolean? = null
342
+
343
+ @Field var coordinate: MapLabelPoint? = null
344
+
345
+ @Field var points: List<MapLabelPoint> = emptyList()
346
+
347
+ @Field var duration: Double? = null
348
+
349
+ @Field var cornerRadius = 0.0
350
+
351
+ @Field var jumpThreshold = 0.0
352
+
353
+ @Field var rotate = false
354
+
355
+ @Field var autoMove = false
356
+
357
+ @Field var baseRotation = 0.0
358
+
359
+ @Field var rotation: Double? = null
360
+
361
+ @Field var scale: MapLabelVector? = null
362
+ }
363
+
364
+ internal class MapLabels(
365
+ private val map: KakaoMap,
366
+ private val imageLoader: MapImageLoader,
367
+ private val emitClick: (Map<String, Any>) -> Unit,
368
+ private val emitAnimationStopped: (Map<String, Any>) -> Unit,
369
+ private val resolvePolygon: (String) -> Polygon?,
370
+ private val density: Float,
371
+ ) {
372
+ private data class ActiveAnimationRun(
373
+ val nativeId: String,
374
+ val epoch: Long,
375
+ val definition: MapLabelAnimationRecord,
376
+ )
377
+
378
+ init {
379
+ if (!density.isFinite() || density <= 0) invalid("Display density must be positive.")
380
+ }
381
+
382
+ private val manager = map.labelManager ?: invalid("The label manager is unavailable before map readiness.")
383
+ private val nativeNamespace = UUID.randomUUID().toString().replace("-", "")
384
+ private val trackingManager: TrackingManager =
385
+ map.trackingManager ?: invalid("The tracking manager is unavailable before map readiness.")
386
+ private val labels = mutableMapOf<String, Label>()
387
+ private val lodLabels = mutableMapOf<String, LodLabel>()
388
+ private val pathLabels = mutableMapOf<String, PolylineLabel>()
389
+ private val dynamicBadges = mutableMapOf<String, MutableMap<String, Badge>>()
390
+ private val ownedLabelLayers = mutableMapOf<String, LabelLayer>()
391
+ private val ownedLodLabelLayers = mutableMapOf<String, LodLabelLayer>()
392
+ private val publicLayerIdsByNative = mutableMapOf<String, String>()
393
+ private val nativeStyleIdsByDefinition = mutableMapOf<String, String>()
394
+ private var nextNativeObjectId = 0L
395
+ private val animationDefinitions = mutableMapOf<String, MapLabelAnimationRecord>()
396
+ private val activeAnimationRuns = mutableMapOf<String, ActiveAnimationRun>()
397
+ private var nextAnimationEpoch = 0L
398
+ private var generation = 0
399
+ private val imageRequests = mutableListOf<MapImageLoadToken>()
400
+ private var pendingApplyCompletion: ((Result<Unit>) -> Unit)? = null
401
+ private var isDisposed = false
402
+ private var isTracking = false
403
+
404
+ fun apply(
405
+ scene: MapLabelSceneRecord,
406
+ completion: (Result<Unit>) -> Unit,
407
+ ) {
408
+ requireActive()
409
+ validateScene(scene, density)
410
+ cancelPendingApply("A newer label scene replaced this apply operation.")
411
+ pendingApplyCompletion = completion
412
+ val currentGeneration = ++generation
413
+ cancelImageRequests()
414
+ loadImages(scene.imageUris(), currentGeneration) { result ->
415
+ if (isDisposed || currentGeneration != generation) return@loadImages
416
+ result.fold(
417
+ onSuccess = { images -> finishApply(runCatching { rebuild(scene, images) }) },
418
+ onFailure = { finishApply(Result.failure(it)) },
419
+ )
420
+ }
421
+ }
422
+
423
+ fun command(command: MapLabelCommandRecord) {
424
+ requireActive()
425
+ val duration = (command.duration ?: 0.0).duration()
426
+ when (command.type) {
427
+ "show" -> {
428
+ show(command.requiredId(), command.visible.required(), command.autoMove, duration)
429
+ }
430
+
431
+ "move" -> {
432
+ label(command.requiredId()).moveTo(command.coordinate.required().latLng(), duration)
433
+ }
434
+
435
+ "movePath" -> {
436
+ command.duration.required()
437
+ if (command.points.size < 2) invalid("movePath requires at least two points.")
438
+ if (command.cornerRadius < 0 || command.jumpThreshold < 0) {
439
+ invalid("cornerRadius and jumpThreshold must be non-negative.")
440
+ }
441
+ val path =
442
+ PathOptions
443
+ .fromPath(command.points.map(MapLabelPoint::latLng))
444
+ .setDuration(duration)
445
+ .setCornerRadius(command.cornerRadius.finiteFloat("cornerRadius"))
446
+ .setJumpThreshold(command.jumpThreshold.finiteFloat("jumpThreshold"))
447
+ .setBaseRadian(Math.toRadians(command.baseRotation.finiteFloat("baseRotation").toDouble()).toFloat())
448
+ label(command.requiredId()).moveOnPath(path, command.rotate)
449
+ }
450
+
451
+ "rotate" -> {
452
+ label(
453
+ command.requiredId(),
454
+ ).rotateTo(
455
+ Math
456
+ .toRadians(
457
+ command.rotation
458
+ .required()
459
+ .finiteFloat("rotation")
460
+ .toDouble(),
461
+ ).toFloat(),
462
+ duration,
463
+ )
464
+ }
465
+
466
+ "scale" -> {
467
+ command.scale.required().also {
468
+ it.validateScale()
469
+ label(command.requiredId()).scaleTo(it.x.finiteFloat("scale.x"), it.y.finiteFloat("scale.y"), duration)
470
+ }
471
+ }
472
+
473
+ "sharePosition" -> {
474
+ share(command, transform = false)
475
+ }
476
+
477
+ "shareTransform" -> {
478
+ share(command, transform = true)
479
+ }
480
+
481
+ "shareShapeTransform" -> {
482
+ val isEnabled = command.enabled.required()
483
+ val source = label(command.sourceId ?: invalid("sourceId is required."))
484
+ val targetId = command.targetId ?: invalid("targetId is required.")
485
+ val target = resolvePolygon(targetId) ?: notFound("polygon", targetId)
486
+ if (isEnabled) source.addShareTransform(target) else source.removeShareTransform(target)
487
+ }
488
+
489
+ "setBadgeVisibility" -> {
490
+ setBadgeVisibility(command)
491
+ }
492
+
493
+ "startAnimation" -> {
494
+ startAnimation(command.requiredId())
495
+ }
496
+
497
+ "stopAnimation" -> {
498
+ val id = command.requiredId()
499
+ if (id !in animationDefinitions) notFound("animation", id)
500
+ activeAnimationRuns[id]?.let { finishAnimationRun(id, it, finished = false, stopNative = true) }
501
+ }
502
+
503
+ "startTracking" -> {
504
+ trackingManager.startTracking(label(command.requiredId()))
505
+ trackingManager.setTrackingRotation(command.rotate)
506
+ isTracking = true
507
+ }
508
+
509
+ "stopTracking" -> {
510
+ trackingManager.stopTracking()
511
+ isTracking = false
512
+ }
513
+
514
+ else -> {
515
+ invalid("Unknown label command: ${command.type}")
516
+ }
517
+ }
518
+ }
519
+
520
+ private fun startAnimation(id: String) {
521
+ var definition = animationDefinitions[id] ?: notFound("animation", id)
522
+ activeAnimationRuns[id]?.let { run ->
523
+ finishAnimationRun(id, run, finished = false, stopNative = true)
524
+ definition = animationDefinitions[id] ?: notFound("animation", id)
525
+ }
526
+ val targets = definition.labelIds.map(::label)
527
+ val epoch = ++nextAnimationEpoch
528
+ val nativeId = makeNativeId("animation")
529
+ val animator =
530
+ manager.addAnimator(definition.nativeAnimation(density, nativeId))
531
+ ?: invalid("Could not create animation $id.")
532
+ animator.addLabels(*targets.toTypedArray())
533
+ val run = ActiveAnimationRun(nativeId, epoch, definition)
534
+ activeAnimationRuns[id] = run
535
+ animator.setOnAnimationStopListener {
536
+ val active = activeAnimationRuns[id]
537
+ if (!isDisposed && active?.nativeId == nativeId && active.epoch == epoch) {
538
+ finishAnimationRun(id, active, finished = true, stopNative = false)
539
+ }
540
+ }
541
+ animator.start()
542
+ }
543
+
544
+ private fun finishAnimationRun(
545
+ id: String,
546
+ run: ActiveAnimationRun,
547
+ finished: Boolean,
548
+ stopNative: Boolean,
549
+ ) {
550
+ val active = activeAnimationRuns[id]
551
+ if (active?.nativeId != run.nativeId || active.epoch != run.epoch) return
552
+ activeAnimationRuns.remove(id)
553
+ if (stopNative) manager.getAnimator(run.nativeId)?.stop()
554
+ manager.removeAnimator(run.nativeId)
555
+ if (run.definition.removeAtStop) removeAnimationTargets(run.definition.labelIds)
556
+ emitAnimationStopped(mapOf("id" to id, "finished" to finished))
557
+ }
558
+
559
+ private fun clearAnimationRuns() {
560
+ val runs = activeAnimationRuns.values.toList()
561
+ activeAnimationRuns.clear()
562
+ runs.forEach { run ->
563
+ manager.getAnimator(run.nativeId)?.stop()
564
+ manager.removeAnimator(run.nativeId)
565
+ }
566
+ }
567
+
568
+ private fun removeAnimationTargets(ids: List<String>) {
569
+ val removedIds = ids.toMutableSet()
570
+ if (removedIds.isEmpty()) return
571
+ val canceledRuns = mutableListOf<Pair<String, ActiveAnimationRun>>()
572
+ while (true) {
573
+ val invalidDefinitionIds =
574
+ animationDefinitions
575
+ .filterValues { definition -> definition.labelIds.any(removedIds::contains) }
576
+ .keys
577
+ .toList()
578
+ if (invalidDefinitionIds.isEmpty()) break
579
+ var didAddRemovedId = false
580
+ invalidDefinitionIds.forEach { invalidId ->
581
+ animationDefinitions.remove(invalidId)
582
+ activeAnimationRuns.remove(invalidId)?.let { run ->
583
+ canceledRuns += invalidId to run
584
+ if (run.definition.removeAtStop) {
585
+ run.definition.labelIds.forEach { targetId ->
586
+ if (removedIds.add(targetId)) didAddRemovedId = true
587
+ }
588
+ }
589
+ }
590
+ }
591
+ if (!didAddRemovedId) break
592
+ }
593
+ canceledRuns.forEach { (canceledId, run) ->
594
+ manager.getAnimator(run.nativeId)?.stop()
595
+ manager.removeAnimator(run.nativeId)
596
+ emitAnimationStopped(mapOf("id" to canceledId, "finished" to false))
597
+ }
598
+ removedIds.forEach {
599
+ labels.remove(it)
600
+ dynamicBadges.remove(it)
601
+ }
602
+ trackingManager.stopTracking()
603
+ isTracking = false
604
+ }
605
+
606
+ fun capabilities() =
607
+ mapOf(
608
+ "pathText" to "polylineLabel",
609
+ "pathTextRank" to false,
610
+ "pathTextStyleAddition" to true,
611
+ "pathTextStyleDefinitionUpdate" to true,
612
+ "pathTextStyleRegistrationRemoval" to true,
613
+ "lodPositionMutation" to false,
614
+ "lodAutoMove" to false,
615
+ "autoMoveDuration" to true,
616
+ "labelScaleCommand" to true,
617
+ "sharedShapeTransform" to true,
618
+ "sharedShapeTarget" to "polygon",
619
+ "competitionExceptions" to true,
620
+ "supportedTransforms" to
621
+ listOf("none", "absoluteRotation", "default", "absoluteRotationKeepUpright", "absoluteRotationDecal", "decal"),
622
+ "keyframeRepeatCount" to false,
623
+ "trackingStateSource" to "managed",
624
+ "remoteImages" to true,
625
+ )
626
+
627
+ fun state(): Map<String, Any> {
628
+ requireActive()
629
+ val liveLabels =
630
+ labels.mapNotNull { (id, cached) ->
631
+ manager.getLayer(cached.layerId)?.getLabel(id)?.let {
632
+ mapOf(
633
+ "id" to id,
634
+ "kind" to "label",
635
+ "visible" to it.isShow,
636
+ "coordinate" to mapOf("latitude" to it.position.latitude, "longitude" to it.position.longitude),
637
+ )
638
+ }
639
+ }
640
+ val liveLodLabels =
641
+ lodLabels.mapNotNull { (id, cached) ->
642
+ manager.getLodLayer(cached.layerId)?.getLabel(id)?.let {
643
+ mapOf(
644
+ "id" to id,
645
+ "kind" to "lodLabel",
646
+ "visible" to it.isShow,
647
+ "coordinate" to mapOf("latitude" to it.position.latitude, "longitude" to it.position.longitude),
648
+ )
649
+ }
650
+ }
651
+ val livePathLabels =
652
+ pathLabels.mapNotNull { (id, cached) ->
653
+ manager.getLayer(cached.layerId)?.getPolylineLabel(id)?.let {
654
+ mapOf("id" to id, "kind" to "pathLabel", "visible" to it.isShow)
655
+ }
656
+ }
657
+ labels.keys.retainAll(liveLabels.mapTo(mutableSetOf()) { it["id"].toString() })
658
+ lodLabels.keys.retainAll(liveLodLabels.mapTo(mutableSetOf()) { it["id"].toString() })
659
+ pathLabels.keys.retainAll(livePathLabels.mapTo(mutableSetOf()) { it["id"].toString() })
660
+ dynamicBadges.keys.retainAll(labels.keys + lodLabels.keys)
661
+ return mapOf(
662
+ "labels" to (liveLabels + liveLodLabels + livePathLabels).sortedBy { it["id"].toString() },
663
+ "animationIds" to animationDefinitions.keys.sorted(),
664
+ "tracking" to mapOf("active" to isTracking, "source" to "managed"),
665
+ )
666
+ }
667
+
668
+ fun handleLabelClick(label: Label) {
669
+ if (isDisposed || labels[label.labelId] !== label) return
670
+ if (manager.getLayer(label.layerId)?.getLabel(label.labelId) !== label) return
671
+ val publicLayerId = publicLayerIdsByNative[label.layerId] ?: return
672
+ emitClick(label.clickPayload("label", publicLayerId))
673
+ }
674
+
675
+ fun handleLodLabelClick(label: LodLabel) {
676
+ if (isDisposed || lodLabels[label.labelId] !== label) return
677
+ if (manager.getLodLayer(label.layerId)?.getLabel(label.labelId) !== label) return
678
+ val publicLayerId = publicLayerIdsByNative[label.layerId] ?: return
679
+ emitClick(label.clickPayload("lodLabel", publicLayerId))
680
+ }
681
+
682
+ fun dispose() {
683
+ if (isDisposed) return
684
+ isDisposed = true
685
+ ++generation
686
+ clearAnimationRuns()
687
+ cancelImageRequests()
688
+ cancelPendingApply("The map view was destroyed.")
689
+ trackingManager.stopTracking()
690
+ clearOwnedLayers()
691
+ labels.clear()
692
+ lodLabels.clear()
693
+ pathLabels.clear()
694
+ dynamicBadges.clear()
695
+ animationDefinitions.clear()
696
+ nativeStyleIdsByDefinition.clear()
697
+ isTracking = false
698
+ }
699
+
700
+ private fun rebuild(
701
+ scene: MapLabelSceneRecord,
702
+ images: Map<String, Bitmap>,
703
+ ) {
704
+ val nativeLayerIds =
705
+ (scene.layers.map { it.id to makeNativeId("layer") } + scene.lodLayers.map { it.id to makeNativeId("lod_layer") }).toMap()
706
+ val nativeStyles =
707
+ scene.styles.associate { style ->
708
+ val nativeId = nativeStyleId("poi_style", style.id, style.fingerprint())
709
+ style.id to style.native(images, nativeId, density)
710
+ }
711
+ val pathStyles =
712
+ scene.pathTextStyles.associate { style ->
713
+ val nativeId = nativeStyleId("path_style", style.id, style.fingerprint())
714
+ style.id to style.native(nativeId, density)
715
+ }
716
+ val layerOptions =
717
+ scene.layers.associate { layer ->
718
+ layer.id to layer.options(nativeLayerIds.getValue(layer.id), nativeLayerIds)
719
+ }
720
+ val lodLayerOptions =
721
+ scene.lodLayers.associate { layer ->
722
+ layer.id to layer.options(nativeLayerIds.getValue(layer.id), nativeLayerIds, density)
723
+ }
724
+ clearAnimationRuns()
725
+ trackingManager.stopTracking()
726
+ clearOwnedLayers()
727
+ labels.clear()
728
+ lodLabels.clear()
729
+ pathLabels.clear()
730
+ dynamicBadges.clear()
731
+ animationDefinitions.clear()
732
+ isTracking = false
733
+
734
+ val styles =
735
+ nativeStyles.mapValues { manager.addLabelStyles(it.value) }
736
+ val layers =
737
+ layerOptions.mapValues { (id, options) ->
738
+ val layer = manager.addLayer(options) ?: invalid("Could not create label layer $id.")
739
+ ownedLabelLayers[id] = layer
740
+ publicLayerIdsByNative[layer.layerId] = id
741
+ layer
742
+ }
743
+ val lodLayers =
744
+ lodLayerOptions.mapValues { (id, options) ->
745
+ val layer = manager.addLodLayer(options) ?: invalid("Could not create LOD label layer $id.")
746
+ ownedLodLabelLayers[id] = layer
747
+ publicLayerIdsByNative[layer.layerId] = id
748
+ layer
749
+ }
750
+ scene.labels.forEach { item ->
751
+ val layer = layers[item.layerId] ?: notFound("layer", item.layerId)
752
+ val native =
753
+ layer.addLabel(item.options(styles[item.styleId] ?: notFound("style", item.styleId)))
754
+ ?: invalid("Could not create label ${item.id}.")
755
+ native.changePixelOffset(
756
+ item.pixelOffset?.x?.dp("pixelOffset.x", density) ?: 0f,
757
+ item.pixelOffset?.y?.dp("pixelOffset.y", density) ?: 0f,
758
+ false,
759
+ )
760
+ native.rotateTo(Math.toRadians(item.orientation).toFloat())
761
+ item.scale?.let { native.scaleTo(it.x.finiteFloat("scale.x"), it.y.finiteFloat("scale.y")) }
762
+ item.badges.forEach { badge ->
763
+ native.addBadge(badge.native(images, density)).firstOrNull()?.let {
764
+ dynamicBadges.getOrPut(item.id) { mutableMapOf() }[badge.id] = it
765
+ if (!badge.visible) it.hide()
766
+ }
767
+ }
768
+ labels[item.id] = native
769
+ }
770
+ scene.lodLabels.forEach { item ->
771
+ val layer = lodLayers[item.layerId] ?: notFound("LOD layer", item.layerId)
772
+ val native =
773
+ layer.addLodLabel(item.options(styles[item.styleId] ?: notFound("style", item.styleId)))
774
+ ?: invalid("Could not create LOD label ${item.id}.")
775
+ item.badges.forEach { badge ->
776
+ native.addBadge(badge.native(images, density)).firstOrNull()?.let {
777
+ dynamicBadges.getOrPut(item.id) { mutableMapOf() }[badge.id] = it
778
+ if (!badge.visible) it.hide()
779
+ }
780
+ }
781
+ lodLabels[item.id] = native
782
+ }
783
+ scene.pathLabels.forEach { item ->
784
+ val layer = layers[item.layerId] ?: notFound("layer", item.layerId)
785
+ val style = pathStyles[item.styleId] ?: notFound("path text style", item.styleId)
786
+ val native =
787
+ layer.addPolylineLabel(
788
+ PolylineLabelOptions.from(item.id, item.text, item.points.map(MapLabelPoint::latLng)).setStyles(style).setVisible(item.visible),
789
+ ) ?: invalid("Could not create path label ${item.id}.")
790
+ pathLabels[item.id] = native
791
+ }
792
+ scene.animations.forEach { animation -> addAnimation(animation) }
793
+ }
794
+
795
+ private fun addAnimation(animation: MapLabelAnimationRecord) {
796
+ animationDefinitions[animation.id] = animation
797
+ }
798
+
799
+ private fun clearOwnedLayers() {
800
+ ownedLabelLayers.values.toList().forEach(manager::remove)
801
+ ownedLodLabelLayers.values.toList().forEach(manager::remove)
802
+ ownedLabelLayers.clear()
803
+ ownedLodLabelLayers.clear()
804
+ publicLayerIdsByNative.clear()
805
+ }
806
+
807
+ private fun makeNativeId(kind: String): String = "__rnk_${nativeNamespace}_${kind}_${++nextNativeObjectId}"
808
+
809
+ private fun nativeStyleId(
810
+ kind: String,
811
+ publicId: String,
812
+ fingerprint: String,
813
+ ) = nativeStyleIdsByDefinition.getOrPut("$kind\u0000$publicId\u0000$fingerprint") { makeNativeId(kind) }
814
+
815
+ private fun share(
816
+ command: MapLabelCommandRecord,
817
+ transform: Boolean,
818
+ ) {
819
+ val isEnabled = command.enabled.required()
820
+ val source = label(command.sourceId ?: invalid("sourceId is required."))
821
+ val target = label(command.targetId ?: invalid("targetId is required."))
822
+ if (transform) {
823
+ if (isEnabled) source.addShareTransform(target) else source.removeShareTransform(target)
824
+ } else {
825
+ if (isEnabled) source.addSharePosition(target) else source.removeSharePosition(target)
826
+ }
827
+ }
828
+
829
+ private fun setBadgeVisibility(command: MapLabelCommandRecord) {
830
+ val isVisible = command.visible.required()
831
+ val id = command.requiredId()
832
+ val badgeId = command.badgeId ?: invalid("badgeId is required.")
833
+ if (command.style) {
834
+ labels[id]?.let {
835
+ if (isVisible) it.showStyleBadge(badgeId) else it.hideStyleBadge(badgeId)
836
+ return
837
+ }
838
+ lodLabels[id]?.let {
839
+ if (isVisible) it.showStyleBadge(badgeId) else it.hideStyleBadge(badgeId)
840
+ return
841
+ }
842
+ notFound("label", id)
843
+ return
844
+ }
845
+ val badge = dynamicBadges[id]?.get(badgeId) ?: notFound("badge", badgeId)
846
+ if (isVisible) badge.show() else badge.hide()
847
+ }
848
+
849
+ private fun show(
850
+ id: String,
851
+ visible: Boolean,
852
+ autoMove: Boolean,
853
+ duration: Int,
854
+ ) {
855
+ labels[id]?.let {
856
+ if (visible) {
857
+ if (autoMove) it.show(true, duration) else it.show()
858
+ } else {
859
+ it.hide()
860
+ }
861
+ return
862
+ }
863
+ lodLabels[id]?.let {
864
+ if (autoMove) throw CodedException(LABEL_UNSUPPORTED, "Android LOD labels do not expose auto-move.", null)
865
+ if (visible) it.show() else it.hide()
866
+ return
867
+ }
868
+ pathLabels[id]?.let {
869
+ if (autoMove) throw CodedException(LABEL_UNSUPPORTED, "Path labels do not expose auto-move.", null)
870
+ if (visible) it.show() else it.hide()
871
+ return
872
+ }
873
+ notFound("label", id)
874
+ }
875
+
876
+ private fun label(id: String) =
877
+ labels[id] ?: if (lodLabels.containsKey(id)) {
878
+ throw CodedException(LABEL_UNSUPPORTED, "LOD label $id cannot be moved, transformed, animated, or tracked.", null)
879
+ } else {
880
+ notFound("label", id)
881
+ }
882
+
883
+ private fun requireActive() {
884
+ if (isDisposed) throw CodedException("ERR_KAKAO_MAP_VIEW_DESTROYED", "The map view was destroyed.", null)
885
+ }
886
+
887
+ private fun loadImages(
888
+ uris: Set<String>,
889
+ currentGeneration: Int,
890
+ completion: (Result<Map<String, Bitmap>>) -> Unit,
891
+ ) {
892
+ if (uris.isEmpty()) {
893
+ completion(Result.success(emptyMap()))
894
+ return
895
+ }
896
+ val images = mutableMapOf<String, Bitmap>()
897
+ var remaining = uris.size
898
+ var didFinish = false
899
+ uris.forEach { uri ->
900
+ imageRequests +=
901
+ imageLoader.load(uri) { result ->
902
+ if (isDisposed || currentGeneration != generation || didFinish) return@load
903
+ result.fold(
904
+ onSuccess = { image ->
905
+ images[uri] = image
906
+ remaining -= 1
907
+ if (remaining == 0) {
908
+ didFinish = true
909
+ imageRequests.clear()
910
+ completion(Result.success(images))
911
+ }
912
+ },
913
+ onFailure = { error ->
914
+ didFinish = true
915
+ cancelImageRequests()
916
+ completion(Result.failure(error))
917
+ },
918
+ )
919
+ }
920
+ }
921
+ }
922
+
923
+ private fun cancelImageRequests() {
924
+ imageRequests.forEach(MapImageLoadToken::cancel)
925
+ imageRequests.clear()
926
+ }
927
+
928
+ private fun finishApply(result: Result<Unit>) {
929
+ val completion = pendingApplyCompletion ?: return
930
+ pendingApplyCompletion = null
931
+ completion(result)
932
+ }
933
+
934
+ private fun cancelPendingApply(message: String) {
935
+ finishApply(Result.failure(CodedException(LABEL_APPLY_CANCELLED, message, null)))
936
+ }
937
+ }
938
+
939
+ private fun validateScene(
940
+ scene: MapLabelSceneRecord,
941
+ density: Float,
942
+ ) {
943
+ unique("style", scene.styles.map { it.id })
944
+ unique("path text style", scene.pathTextStyles.map { it.id })
945
+ unique("layer", scene.layers.map { it.id } + scene.lodLayers.map { it.id })
946
+ unique("label", scene.labels.map { it.id } + scene.lodLabels.map { it.id } + scene.pathLabels.map { it.id })
947
+ unique("animation", scene.animations.map { it.id })
948
+ val styleById = scene.styles.associateBy { it.id }
949
+ val layerIds = scene.layers.mapTo(mutableSetOf()) { it.id }
950
+ val lodLayerIds = scene.lodLayers.mapTo(mutableSetOf()) { it.id }
951
+ val regularLabelIds = scene.labels.mapTo(mutableSetOf()) { it.id }
952
+ (
953
+ scene.styles.map { it.id } + scene.pathTextStyles.map { it.id } + layerIds + lodLayerIds +
954
+ regularLabelIds + scene.lodLabels.map { it.id } + scene.pathLabels.map { it.id } + scene.animations.map { it.id }
955
+ ).forEach { if (it.isBlank()) invalid("IDs must not be empty.") }
956
+ scene.styles.forEach { style ->
957
+ if (style.levels.isEmpty()) invalid("Style ${style.id} must contain at least one level.")
958
+ unique("style level", style.levels.map { it.level.toString() })
959
+ style.levels.forEach { level ->
960
+ if (level.padding < 0) invalid("Style padding must be non-negative.")
961
+ unique("style badge", level.badges.map { it.id })
962
+ level.image?.validate()
963
+ level.badges.forEach { it.image.validate() }
964
+ }
965
+ }
966
+ scene.pathTextStyles.forEach { style ->
967
+ if (style.levels.isEmpty()) invalid("Path style ${style.id} must contain at least one level.")
968
+ unique("path style level", style.levels.map { it.level.toString() })
969
+ }
970
+ scene.layers.forEach { layer ->
971
+ layer.competitionExceptions.forEach { if (it !in layerIds && it !in lodLayerIds) notFound("competition layer", it) }
972
+ }
973
+ scene.lodLayers.forEach { layer ->
974
+ if (layer.radius < 0) invalid("LOD radius must be non-negative.")
975
+ layer.competitionExceptions.forEach { if (it !in layerIds && it !in lodLayerIds) notFound("competition layer", it) }
976
+ }
977
+ (scene.labels + scene.lodLabels).forEach { item ->
978
+ item.coordinate.validate()
979
+ val style = styleById[item.styleId] ?: notFound("style", item.styleId)
980
+ val expectedLayerIds = if (item is MapLodLabelRecord) lodLayerIds else layerIds
981
+ if (item.layerId !in expectedLayerIds) notFound("layer", item.layerId)
982
+ item.text.forEach { text ->
983
+ if (text.styleIndex < 0 || style.levels.any { text.styleIndex >= it.textLines.size }) {
984
+ invalid("Label ${item.id} has a text style index that is unavailable at one or more levels.")
985
+ }
986
+ }
987
+ item.rank.integerLong("rank")
988
+ transform(item.transform)
989
+ item.pixelOffset?.validateVector("pixelOffset")
990
+ item.orientation.finiteFloat("orientation")
991
+ item.scale?.validateScale()
992
+ unique("label badge", item.badges.map { it.id })
993
+ item.badges.forEach {
994
+ it.image.validate(density)
995
+ it.offset?.validateVector("badge.offset")
996
+ }
997
+ }
998
+ scene.pathLabels.forEach { item ->
999
+ if (item.points.size < 2) invalid("Path label ${item.id} requires at least two points.")
1000
+ item.points.forEach(MapLabelPoint::validate)
1001
+ if (item.layerId !in layerIds) notFound("layer", item.layerId)
1002
+ if (scene.pathTextStyles.none { it.id == item.styleId }) notFound("path text style", item.styleId)
1003
+ if (item.rank != 0.0) {
1004
+ throw CodedException(LABEL_UNSUPPORTED, "Android Kakao Maps SDK cannot set a polyline label rank.", null)
1005
+ }
1006
+ }
1007
+ scene.animations.forEach { animation ->
1008
+ if (animation.labelIds.isEmpty() || animation.keyframes.isEmpty()) invalid("Animation ${animation.id} cannot be empty.")
1009
+ unique("animation label", animation.labelIds)
1010
+ animation.labelIds.forEach { if (it !in regularLabelIds) notFound("animatable label", it) }
1011
+ if (animation.repeatCount < 0) invalid("Animation repeatCount must be non-negative.")
1012
+ if (animation.effect != "drop" && animation.repeatCount != 1) {
1013
+ throw CodedException(LABEL_UNSUPPORTED, "Keyframe label animations do not expose repeatCount on Android.", null)
1014
+ }
1015
+ if (animation.effect !in setOf("drop", "alpha", "scale", "scaleAlpha", "transform")) {
1016
+ invalid("Unknown label animation effect: ${animation.effect}")
1017
+ }
1018
+ if (animation.effect == "drop" && animation.keyframes.size != 1) invalid("Drop animation requires exactly one keyframe.")
1019
+ animation.keyframes.forEach { frame ->
1020
+ frame.duration.duration()
1021
+ interpolation(frame.interpolation)
1022
+ frame.alpha?.unitFloat("alpha")
1023
+ frame.scale?.validateScale()
1024
+ frame.translation?.validateVector("translation")
1025
+ frame.rotation?.finiteFloat("rotation")
1026
+ if (animation.effect in setOf("scale", "scaleAlpha") && frame.scale == null) {
1027
+ invalid("${animation.effect} animation requires scale in every keyframe.")
1028
+ }
1029
+ }
1030
+ }
1031
+ }
1032
+
1033
+ private fun MapLabelImage.validate(density: Float = 1f) {
1034
+ if (uri.isBlank()) invalid("Image URI must not be empty.")
1035
+ width?.positiveSize("image.width", density)
1036
+ height?.positiveSize("image.height", density)
1037
+ }
1038
+
1039
+ private fun MapLabelVector.validateScale() {
1040
+ if (x.finiteFloat("scale.x") < 0 || y.finiteFloat("scale.y") < 0) invalid("Scale must be non-negative.")
1041
+ }
1042
+
1043
+ private fun MapLabelVector.validateVector(name: String) {
1044
+ x.finiteFloat("$name.x")
1045
+ y.finiteFloat("$name.y")
1046
+ }
1047
+
1048
+ private fun MapLabelSceneRecord.imageUris() =
1049
+ buildSet {
1050
+ styles.flatMap { it.levels }.forEach { level ->
1051
+ level.image?.let { add(it.uri) }
1052
+ level.badges.forEach { add(it.image.uri) }
1053
+ }
1054
+ (labels + lodLabels).flatMap { it.badges }.forEach { add(it.image.uri) }
1055
+ }
1056
+
1057
+ private fun MapLabelStyleRecord.fingerprint() =
1058
+ levels.sortedBy { it.level }.joinToString(";") { level ->
1059
+ listOf(
1060
+ level.level.toString(),
1061
+ level.image.fingerprint(),
1062
+ level.anchor.fingerprint(),
1063
+ level.padding.toBits().toString(),
1064
+ level.iconTransition.fingerprint(),
1065
+ level.textTransition.fingerprint(),
1066
+ level.textLayout.fingerprint(),
1067
+ level.textLines.joinToString(",", transform = MapLabelTextStyleRecord::fingerprint),
1068
+ level.badges.joinToString(",", transform = MapLabelBadgeStyleRecord::fingerprint),
1069
+ ).joinToString("|")
1070
+ }
1071
+
1072
+ private fun MapPathTextStyleRecord.fingerprint() = levels.sortedBy { it.level }.joinToString(";") { "${it.level}|${it.text.fingerprint()}" }
1073
+
1074
+ private fun MapLabelTextStyleRecord.fingerprint() =
1075
+ listOf(
1076
+ fontFamily.fingerprint(),
1077
+ fontSize.toBits().toString(),
1078
+ color.fingerprint(),
1079
+ strokeColor.fingerprint(),
1080
+ strokeWidth.toBits().toString(),
1081
+ characterSpacing.toBits().toString(),
1082
+ lineSpacing.toBits().toString(),
1083
+ aspectRatio.toBits().toString(),
1084
+ ).joinToString("|")
1085
+
1086
+ private fun MapLabelBadgeStyleRecord.fingerprint() =
1087
+ listOf(id.fingerprint(), image.fingerprint(), offset.fingerprint(), zOrder.toString()).joinToString("|")
1088
+
1089
+ private fun MapLabelImage?.fingerprint() =
1090
+ this?.let { listOf(uri.fingerprint(), width?.toBits(), height?.toBits()).joinToString("|") } ?: "-"
1091
+
1092
+ private fun MapLabelVector?.fingerprint() = this?.let { "${x.toBits()}|${y.toBits()}" } ?: "-"
1093
+
1094
+ private fun MapLabelTransitionRecord?.fingerprint() =
1095
+ this?.let { "${entrance.fingerprint()}|${exit.fingerprint()}|$enableEntrance|$enableExit" } ?: "-"
1096
+
1097
+ private fun String?.fingerprint() = this?.let { "${it.length}:$it" } ?: "-"
1098
+
1099
+ private fun MapLabelStyleRecord.native(
1100
+ images: Map<String, Bitmap>,
1101
+ nativeId: String,
1102
+ density: Float,
1103
+ ) = LabelStyles.from(
1104
+ nativeId,
1105
+ levels.sortedBy { it.level }.map { level ->
1106
+ val textStyles = level.textLines.map { it.native(density) }.toTypedArray()
1107
+ val image = level.image?.bitmap(images, density)
1108
+ val style =
1109
+ when {
1110
+ image != null -> LabelStyle.from(image).setTextStyles(*textStyles)
1111
+ textStyles.isNotEmpty() -> LabelStyle.from(*textStyles)
1112
+ else -> invalid("Style $id level ${level.level} needs an image or text style.")
1113
+ }
1114
+ style
1115
+ .setZoomLevel(level.level)
1116
+ .setPadding(level.padding.dp("padding", density))
1117
+ .setApplyDpScale(false)
1118
+ level.anchor?.let { style.setAnchorPoint(it.x.finiteFloat("anchor.x"), it.y.finiteFloat("anchor.y")) }
1119
+ level.iconTransition?.let { style.setIconTransition(it.native()) }
1120
+ level.textTransition?.let { style.setTextTransition(it.native()) }
1121
+ style.setTextGravity(textGravity(level.textLayout))
1122
+ if (level.badges.isNotEmpty()) style.setBadges(*level.badges.map { it.native(images, density) }.toTypedArray())
1123
+ style
1124
+ },
1125
+ )
1126
+
1127
+ private fun MapPathTextStyleRecord.native(
1128
+ nativeId: String,
1129
+ density: Float,
1130
+ ) = PolylineLabelStyles.from(
1131
+ nativeId,
1132
+ levels.sortedBy { it.level }.map { level ->
1133
+ val text = level.text.native(density)
1134
+ PolylineLabelStyle
1135
+ .from(text.size, text.color, text.stroke, text.strokeColor)
1136
+ .setZoomLevel(level.level)
1137
+ .setApplyDpScale(false)
1138
+ },
1139
+ )
1140
+
1141
+ private fun MapLabelTextStyleRecord.native(density: Float): LabelTextStyle {
1142
+ val nativeFontSize = fontSize.positiveDpInt("fontSize", density)
1143
+ val nativeLineSpace = lineSpacing.finiteFloat("lineSpacing")
1144
+ if (nativeLineSpace < 1) invalid("lineSpacing must be at least 1.")
1145
+ val nativeCharacterSpace = characterSpacing.dpInt("characterSpacing", density)
1146
+ if (nativeCharacterSpace < MIN_CHARACTER_SPACE_PX) {
1147
+ invalid("characterSpacing must resolve to at least $MIN_CHARACTER_SPACE_PX native pixels on Android.")
1148
+ }
1149
+ val result =
1150
+ LabelTextStyle.from(
1151
+ nativeFontSize,
1152
+ parseColor(color),
1153
+ strokeWidth.nonnegativeDpInt("strokeWidth", density),
1154
+ parseColor(strokeColor ?: "#00000000"),
1155
+ )
1156
+ fontFamily?.let(result::setFont)
1157
+ result.setCharacterSpace(nativeCharacterSpace)
1158
+ result.setLineSpace(nativeLineSpace)
1159
+ val ratio = aspectRatio.finiteFloat("aspectRatio")
1160
+ if (ratio <= 0) invalid("aspectRatio must be positive.")
1161
+ result.setAspectRatio(ratio)
1162
+ return result
1163
+ }
1164
+
1165
+ private fun MapLabelTransitionRecord.native() =
1166
+ LabelTransition.from(transition(entrance), transition(exit)).enableTransitionWhenChange(enableEntrance, enableExit)
1167
+
1168
+ private fun MapLabelLayerRecord.options(
1169
+ nativeId: String,
1170
+ nativeLayerIds: Map<String, String>,
1171
+ ) = LabelLayerOptions
1172
+ .from(nativeId)
1173
+ .setCompetitionType(competitionType(competition))
1174
+ .setCompetitionUnit(competitionUnit(competitionUnit))
1175
+ .setOrderingType(ordering(ordering))
1176
+ .setZOrder(zOrder)
1177
+ .setVisible(visible)
1178
+ .setClickable(clickable)
1179
+ .addCompetitionExceptions(*competitionExceptions.map(nativeLayerIds::getValue).toTypedArray())
1180
+
1181
+ private fun MapLodLabelLayerRecord.options(
1182
+ nativeId: String,
1183
+ nativeLayerIds: Map<String, String>,
1184
+ density: Float,
1185
+ ) = (this as MapLabelLayerRecord).options(nativeId, nativeLayerIds).setLodRadius(radius.dp("radius", density))
1186
+
1187
+ private fun MapLabelRecord.options(style: LabelStyles) =
1188
+ LabelOptions
1189
+ .from(id, coordinate.latLng())
1190
+ .setStyles(style)
1191
+ .setTexts(LabelTextBuilder().apply { text.forEach { addTextLine(it.value, it.styleIndex) } })
1192
+ .setRank(rank.integerLong("rank"))
1193
+ .setClickable(clickable)
1194
+ .setVisible(visible)
1195
+ .setTransform(transform(transform))
1196
+
1197
+ private fun MapLabelBadgeStyleRecord.native(
1198
+ images: Map<String, Bitmap>,
1199
+ density: Float,
1200
+ ) = com.kakao.vectormap.label.BadgeOptions
1201
+ .from(id, image.bitmap(images, density))
1202
+ .setOffset(offset?.x?.dp("badge.offset.x", density) ?: 0f, offset?.y?.dp("badge.offset.y", density) ?: 0f)
1203
+ .setZOrder(zOrder)
1204
+
1205
+ private fun MapLabelBadgeRecord.native(
1206
+ images: Map<String, Bitmap>,
1207
+ density: Float,
1208
+ ) = com.kakao.vectormap.label.BadgeOptions
1209
+ .from(id, image.bitmap(images, density))
1210
+ .setOffset(offset?.x?.dp("badge.offset.x", density) ?: 0f, offset?.y?.dp("badge.offset.y", density) ?: 0f)
1211
+ .setZOrder(zOrder)
1212
+
1213
+ private fun MapLabelImage.bitmap(
1214
+ images: Map<String, Bitmap>,
1215
+ scale: Float = 1f,
1216
+ ): Bitmap {
1217
+ val source = images[uri] ?: invalid("Image was not loaded: $uri")
1218
+ if (width == null && height == null) {
1219
+ return source.copy(Bitmap.Config.ARGB_8888, false).also { it.density = Bitmap.DENSITY_NONE }
1220
+ }
1221
+ val targetWidth = width?.positiveSize("image.width", scale)
1222
+ val targetHeight = height?.positiveSize("image.height", scale)
1223
+ val ratio = source.width.toDouble() / source.height.toDouble()
1224
+ val resolvedWidth = targetWidth ?: ((targetHeight ?: invalid("Image dimensions are unavailable.")) * ratio).roundToInt().coerceAtLeast(1)
1225
+ val resolvedHeight = targetHeight ?: (resolvedWidth / ratio).roundToInt().coerceAtLeast(1)
1226
+ val resized = Bitmap.createScaledBitmap(source, resolvedWidth, resolvedHeight, true)
1227
+ return (if (resized === source) source.copy(Bitmap.Config.ARGB_8888, false) else resized)
1228
+ .also { it.density = Bitmap.DENSITY_NONE }
1229
+ }
1230
+
1231
+ private fun Double.positiveSize(
1232
+ name: String,
1233
+ scale: Float = 1f,
1234
+ ): Int {
1235
+ val scaled = this * scale
1236
+ if (!scaled.isFinite() || scaled <= 0 || scaled > Int.MAX_VALUE) invalid("$name must be positive.")
1237
+ return scaled.roundToInt().coerceAtLeast(1)
1238
+ }
1239
+
1240
+ private fun textGravity(value: String) =
1241
+ when (value) {
1242
+ "center" -> Gravity.CENTER
1243
+ "top" -> Gravity.TOP
1244
+ "bottom" -> Gravity.BOTTOM
1245
+ "left" -> Gravity.LEFT
1246
+ "right" -> Gravity.RIGHT
1247
+ else -> invalid("Unknown label text layout: $value")
1248
+ }
1249
+
1250
+ private fun MapLabelAnimationRecord.nativeAnimation(
1251
+ density: Float,
1252
+ nativeId: String,
1253
+ ): com.kakao.vectormap.animation.Animation =
1254
+ when (effect) {
1255
+ "drop" -> {
1256
+ DropAnimation
1257
+ .from(nativeId)
1258
+ .setPixelHeight(
1259
+ keyframes
1260
+ .firstOrNull()
1261
+ ?.translation
1262
+ ?.y
1263
+ ?.dp("translation.y", density) ?: 0f,
1264
+ ).setDuration(keyframes.firstOrNull()?.duration?.duration() ?: 0)
1265
+ .setRepeatCount(repeatCount)
1266
+ .setHideShapeAtStop(hideAtStop)
1267
+ .setRemoveLabelAtStop(removeAtStop)
1268
+ }
1269
+
1270
+ "alpha" -> {
1271
+ AlphaAnimations
1272
+ .from(
1273
+ nativeId,
1274
+ *keyframes
1275
+ .map {
1276
+ AlphaAnimation
1277
+ .from(
1278
+ (it.alpha ?: 1.0).unitFloat("alpha"),
1279
+ ).setDuration(it.duration.duration())
1280
+ .setInterpolation(interpolation(it.interpolation))
1281
+ }.toTypedArray(),
1282
+ ).setHideLabelAtStop(hideAtStop)
1283
+ .setRemoveLabelAtStop(removeAtStop)
1284
+ .setResetToInitialState(resetToInitialState)
1285
+ }
1286
+
1287
+ "scale" -> {
1288
+ ScaleAnimations
1289
+ .from(
1290
+ nativeId,
1291
+ *keyframes
1292
+ .map { frame ->
1293
+ frame.scale.required().let {
1294
+ ScaleAnimation
1295
+ .from(
1296
+ it.x.finiteFloat("scale.x"),
1297
+ it.y.finiteFloat("scale.y"),
1298
+ ).setDuration(frame.duration.duration())
1299
+ .setInterpolation(interpolation(frame.interpolation))
1300
+ }
1301
+ }.toTypedArray(),
1302
+ ).setHideLabelAtStop(hideAtStop)
1303
+ .setRemoveLabelAtStop(removeAtStop)
1304
+ .setResetToInitialState(resetToInitialState)
1305
+ }
1306
+
1307
+ "scaleAlpha" -> {
1308
+ ScaleAlphaAnimations
1309
+ .from(
1310
+ nativeId,
1311
+ *keyframes
1312
+ .map { frame ->
1313
+ frame.scale.required().let {
1314
+ ScaleAlphaAnimation
1315
+ .from(
1316
+ it.x.finiteFloat("scale.x"),
1317
+ it.y.finiteFloat("scale.y"),
1318
+ (
1319
+ frame.alpha
1320
+ ?: 1.0
1321
+ ).unitFloat("alpha"),
1322
+ ).setDuration(frame.duration.duration())
1323
+ .setInterpolation(interpolation(frame.interpolation))
1324
+ }
1325
+ }.toTypedArray(),
1326
+ ).setHideLabelAtStop(hideAtStop)
1327
+ .setRemoveLabelAtStop(removeAtStop)
1328
+ .setResetToInitialState(resetToInitialState)
1329
+ }
1330
+
1331
+ "transform" -> {
1332
+ TransformAnimations
1333
+ .from(
1334
+ nativeId,
1335
+ *keyframes
1336
+ .map { frame ->
1337
+ TransformAnimation
1338
+ .from()
1339
+ .setDuration(frame.duration.duration())
1340
+ .setInterpolation(interpolation(frame.interpolation))
1341
+ .setAlpha((frame.alpha ?: 1.0).unitFloat("alpha"))
1342
+ .setScale(
1343
+ frame.scale?.let { android.graphics.PointF(it.x.finiteFloat("scale.x"), it.y.finiteFloat("scale.y")) }
1344
+ ?: android.graphics.PointF(1f, 1f),
1345
+ ).setPixelTranslation(
1346
+ frame.translation?.let { android.graphics.PointF(it.x.dp("translation.x", density), it.y.dp("translation.y", density)) }
1347
+ ?: android.graphics.PointF(),
1348
+ ).setRotation(Math.toRadians(frame.rotation ?: 0.0).toFloat())
1349
+ }.toTypedArray(),
1350
+ ).setHideLabelAtStop(hideAtStop)
1351
+ .setRemoveLabelAtStop(removeAtStop)
1352
+ .setResetToInitialState(resetToInitialState)
1353
+ }
1354
+
1355
+ else -> {
1356
+ invalid("Unknown label animation effect: $effect")
1357
+ }
1358
+ }
1359
+
1360
+ private fun Label.clickPayload(
1361
+ kind: String,
1362
+ publicLayerId: String,
1363
+ ) = mapOf(
1364
+ "id" to labelId,
1365
+ "layerId" to publicLayerId,
1366
+ "kind" to kind,
1367
+ "coordinate" to mapOf("latitude" to position.latitude, "longitude" to position.longitude),
1368
+ )
1369
+
1370
+ private fun LodLabel.clickPayload(
1371
+ kind: String,
1372
+ publicLayerId: String,
1373
+ ) = mapOf(
1374
+ "id" to labelId,
1375
+ "layerId" to publicLayerId,
1376
+ "kind" to kind,
1377
+ "coordinate" to mapOf("latitude" to position.latitude, "longitude" to position.longitude),
1378
+ )
1379
+
1380
+ private fun MapLabelPoint.latLng(): LatLng {
1381
+ validate()
1382
+ return LatLng.from(latitude, longitude)
1383
+ }
1384
+
1385
+ private fun MapLabelPoint.validate() {
1386
+ if (!latitude.isFinite() || !longitude.isFinite() || latitude !in -90.0..90.0 ||
1387
+ longitude !in -180.0..180.0
1388
+ ) {
1389
+ invalid("Invalid coordinate.")
1390
+ }
1391
+ }
1392
+
1393
+ private fun Double.duration(): Int {
1394
+ if (!isFinite() || this < 0 || this > Int.MAX_VALUE ||
1395
+ this % 1.0 != 0.0
1396
+ ) {
1397
+ invalid("Duration must be a non-negative 32-bit integer.")
1398
+ }
1399
+ return toInt()
1400
+ }
1401
+
1402
+ private fun Double.finiteFloat(name: String): Float {
1403
+ if (!isFinite() || this > Float.MAX_VALUE ||
1404
+ this < -Float.MAX_VALUE
1405
+ ) {
1406
+ invalid("$name must be finite.")
1407
+ }
1408
+ return toFloat()
1409
+ }
1410
+
1411
+ private fun Double.dp(
1412
+ name: String,
1413
+ density: Float,
1414
+ ): Float {
1415
+ if (!density.isFinite() || density <= 0) invalid("Display density must be positive.")
1416
+ val value = this * density
1417
+ return value.finiteFloat(name)
1418
+ }
1419
+
1420
+ private fun Double.dpInt(
1421
+ name: String,
1422
+ density: Float,
1423
+ ): Int {
1424
+ if (!isFinite()) invalid("$name must be finite.")
1425
+ if (!density.isFinite() || density <= 0) invalid("Display density must be positive.")
1426
+ val value = this * density
1427
+ if (!value.isFinite() || value > Int.MAX_VALUE || value < Int.MIN_VALUE) {
1428
+ invalid("$name is outside the supported range.")
1429
+ }
1430
+ return value.roundToInt()
1431
+ }
1432
+
1433
+ private fun Double.nonnegativeDpInt(
1434
+ name: String,
1435
+ density: Float,
1436
+ ): Int {
1437
+ if (this < 0) invalid("$name must be non-negative.")
1438
+ return dpInt(name, density).also { if (it < 0) invalid("$name must be non-negative.") }
1439
+ }
1440
+
1441
+ private fun Double.positiveDpInt(
1442
+ name: String,
1443
+ density: Float,
1444
+ ): Int {
1445
+ if (this <= 0) invalid("$name must be positive.")
1446
+ return dpInt(name, density).also { if (it <= 0) invalid("$name must be positive.") }
1447
+ }
1448
+
1449
+ private fun Double.unitFloat(name: String): Float {
1450
+ val value = finiteFloat(name)
1451
+ if (value !in 0f..1f) invalid("$name must be between 0 and 1.")
1452
+ return value
1453
+ }
1454
+
1455
+ private fun Double.integerLong(name: String): Long {
1456
+ if (!isFinite() || this % 1.0 != 0.0 || this > Long.MAX_VALUE || this < Long.MIN_VALUE) {
1457
+ invalid("$name must be an integer.")
1458
+ }
1459
+ return toLong()
1460
+ }
1461
+
1462
+ private fun Double.integerInt(name: String): Int {
1463
+ val value = integerLong(name)
1464
+ if (value > Int.MAX_VALUE || value < Int.MIN_VALUE) invalid("$name is outside the supported range.")
1465
+ return value.toInt()
1466
+ }
1467
+
1468
+ private fun Double.nonnegativeInt(name: String): Int {
1469
+ val value = integerInt(name)
1470
+ if (value < 0) invalid("$name must be non-negative.")
1471
+ return value
1472
+ }
1473
+
1474
+ private fun Double.positiveInt(name: String): Int {
1475
+ val value = integerInt(name)
1476
+ if (value <= 0) invalid("$name must be positive.")
1477
+ return value
1478
+ }
1479
+
1480
+ private fun parseColor(value: String): Int {
1481
+ val hex = value.removePrefix("#")
1482
+ if (hex.length != 6 && hex.length != 8) invalid("Invalid color: $value")
1483
+ val number = hex.toLongOrNull(16) ?: invalid("Invalid color: $value")
1484
+ val hasAlpha = hex.length == 8
1485
+ val red = ((number shr (if (hasAlpha) 24 else 16)) and 0xFF).toInt()
1486
+ val green = ((number shr (if (hasAlpha) 16 else 8)) and 0xFF).toInt()
1487
+ val blue = ((number shr (if (hasAlpha) 8 else 0)) and 0xFF).toInt()
1488
+ val alpha = if (hasAlpha) (number and 0xFF).toInt() else 0xFF
1489
+ return Color.argb(alpha, red, green, blue)
1490
+ }
1491
+
1492
+ private fun transition(value: String) =
1493
+ when (value) {
1494
+ "none" -> Transition.None
1495
+ "alpha" -> Transition.Alpha
1496
+ "scale" -> Transition.Scale
1497
+ else -> invalid("Unknown transition: $value")
1498
+ }
1499
+
1500
+ private fun competitionType(value: String) =
1501
+ when (value) {
1502
+ "none" -> CompetitionType.None
1503
+ "all" -> CompetitionType.All
1504
+ "upper" -> CompetitionType.Upper
1505
+ "upperLower" -> CompetitionType.UpperLower
1506
+ "upperSame" -> CompetitionType.UpperSame
1507
+ "same" -> CompetitionType.Same
1508
+ "sameLower" -> CompetitionType.SameLower
1509
+ "lower" -> CompetitionType.Lower
1510
+ else -> invalid("Unknown competition: $value")
1511
+ }
1512
+
1513
+ private fun competitionUnit(value: String) =
1514
+ when (value) {
1515
+ "iconAndText" -> CompetitionUnit.IconAndText
1516
+ "iconFirst" -> CompetitionUnit.IconFirst
1517
+ else -> invalid("Unknown competition unit: $value")
1518
+ }
1519
+
1520
+ private fun ordering(value: String) =
1521
+ when (value) {
1522
+ "rank" -> OrderingType.Rank
1523
+ "leftBottom" -> OrderingType.LeftBottom
1524
+ else -> invalid("Unknown ordering: $value")
1525
+ }
1526
+
1527
+ private fun transform(value: String) =
1528
+ when (value) {
1529
+ "none" -> TransformMethod.None
1530
+ "absoluteRotation" -> TransformMethod.AbsoluteRotation
1531
+ "default" -> TransformMethod.Default
1532
+ "absoluteRotationKeepUpright" -> TransformMethod.AbsoluteRotation_KeepUpright
1533
+ "absoluteRotationDecal" -> TransformMethod.AbsoluteRotation_Decal
1534
+ "decal" -> TransformMethod.Decal
1535
+ else -> invalid("Unknown transform: $value")
1536
+ }
1537
+
1538
+ private fun interpolation(value: String) =
1539
+ when (value) {
1540
+ "linear" -> Interpolation.Linear
1541
+ "cubicIn" -> Interpolation.CubicIn
1542
+ "cubicOut" -> Interpolation.CubicOut
1543
+ "cubicInOut" -> Interpolation.CubicInOut
1544
+ else -> invalid("Unknown interpolation: $value")
1545
+ }
1546
+
1547
+ private fun unique(
1548
+ kind: String,
1549
+ values: List<String>,
1550
+ ) {
1551
+ val duplicate =
1552
+ values
1553
+ .groupingBy { it }
1554
+ .eachCount()
1555
+ .entries
1556
+ .firstOrNull {
1557
+ it.value >
1558
+ 1
1559
+ }?.key
1560
+ ; if (duplicate != null) invalid("Duplicate $kind id: $duplicate")
1561
+ }
1562
+
1563
+ private fun MapLabelCommandRecord.requiredId() = id ?: invalid("id is required.")
1564
+
1565
+ private fun <T : Any> T?.required(): T = this ?: invalid("Required value is missing.")
1566
+
1567
+ private fun invalid(message: String): Nothing = throw CodedException(LABEL_INVALID_ARGUMENT, message, null)
1568
+
1569
+ private fun notFound(
1570
+ kind: String,
1571
+ id: String,
1572
+ ): Nothing = throw CodedException(LABEL_NOT_FOUND, "$kind not found: $id", null)