@react-native-kakao/map 2.2.6 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +5 -38
- package/RNCKakaoMap.podspec +16 -52
- package/android/build.gradle +12 -107
- package/android/consumer-rules.pro +2 -0
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/net/mjstudio/rnkakao/map/CameraOptions.kt +61 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapModule.kt +131 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapView.kt +1257 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapCameraCommands.kt +306 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapDimScreen.kt +431 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapGui.kt +899 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapImageLoader.kt +232 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapLabels.kt +1572 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapOptions.kt +360 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapRoutes.kt +805 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapShapes.kt +1096 -0
- package/android/src/main/java/net/mjstudio/rnkakao/map/MapUtilities.kt +495 -0
- package/expo-module.config.json +5 -0
- package/ios/CameraOptions.swift +41 -0
- package/ios/KakaoMapModule.swift +204 -0
- package/ios/KakaoMapView.swift +1018 -0
- package/ios/MapCameraCommands.swift +451 -0
- package/ios/MapDimScreen.swift +463 -0
- package/ios/MapGui.swift +1099 -0
- package/ios/MapImageLoader.swift +177 -0
- package/ios/MapLabels.swift +1676 -0
- package/ios/MapOptions.swift +374 -0
- package/ios/MapRoutes.swift +903 -0
- package/ios/MapShapes.swift +1294 -0
- package/ios/MapUnits.swift +20 -0
- package/ios/MapUtilities.swift +365 -0
- package/lib/commonjs/index.js +180 -29
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +18 -0
- package/lib/commonjs/index.web.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types-assets.js +2 -0
- package/lib/commonjs/types-assets.js.map +1 -0
- package/lib/commonjs/types-camera.js +6 -0
- package/lib/commonjs/types-camera.js.map +1 -0
- package/lib/commonjs/types-dim.js +6 -0
- package/lib/commonjs/types-dim.js.map +1 -0
- package/lib/commonjs/types-gui.js +6 -0
- package/lib/commonjs/types-gui.js.map +1 -0
- package/lib/commonjs/types-labels.js +6 -0
- package/lib/commonjs/types-labels.js.map +1 -0
- package/lib/commonjs/types-routes.js +6 -0
- package/lib/commonjs/types-routes.js.map +1 -0
- package/lib/commonjs/types-shapes.js +6 -0
- package/lib/commonjs/types-shapes.js.map +1 -0
- package/lib/commonjs/types-utilities.js +6 -0
- package/lib/commonjs/types-utilities.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/index.js +180 -22
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +13 -0
- package/lib/module/index.web.js.map +1 -0
- package/lib/module/types-assets.js +2 -0
- package/lib/module/types-assets.js.map +1 -0
- package/lib/module/types-camera.js +4 -0
- package/lib/module/types-camera.js.map +1 -0
- package/lib/module/types-dim.js +4 -0
- package/lib/module/types-dim.js.map +1 -0
- package/lib/module/types-gui.js +4 -0
- package/lib/module/types-gui.js.map +1 -0
- package/lib/module/types-labels.js +4 -0
- package/lib/module/types-labels.js.map +1 -0
- package/lib/module/types-routes.js +4 -0
- package/lib/module/types-routes.js.map +1 -0
- package/lib/module/types-shapes.js +4 -0
- package/lib/module/types-shapes.js.map +1 -0
- package/lib/module/types-utilities.js +4 -0
- package/lib/module/types-utilities.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/src/index.d.ts +59 -9
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types-assets.d.ts +9 -0
- package/lib/typescript/src/types-assets.d.ts.map +1 -0
- package/lib/typescript/src/types-camera.d.ts +123 -0
- package/lib/typescript/src/types-camera.d.ts.map +1 -0
- package/lib/typescript/src/types-dim.d.ts +54 -0
- package/lib/typescript/src/types-dim.d.ts.map +1 -0
- package/lib/typescript/src/types-gui.d.ts +189 -0
- package/lib/typescript/src/types-gui.d.ts.map +1 -0
- package/lib/typescript/src/types-labels.d.ts +246 -0
- package/lib/typescript/src/types-labels.d.ts.map +1 -0
- package/lib/typescript/src/types-routes.d.ts +162 -0
- package/lib/typescript/src/types-routes.d.ts.map +1 -0
- package/lib/typescript/src/types-shapes.d.ts +253 -0
- package/lib/typescript/src/types-shapes.d.ts.map +1 -0
- package/lib/typescript/src/types-utilities.d.ts +130 -0
- package/lib/typescript/src/types-utilities.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +222 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +24 -77
- package/src/index.tsx +322 -0
- package/src/index.web.tsx +46 -0
- package/src/types-assets.ts +8 -0
- package/src/types-camera.ts +97 -0
- package/src/types-dim.ts +54 -0
- package/src/types-gui.ts +171 -0
- package/src/types-labels.ts +207 -0
- package/src/types-routes.ts +118 -0
- package/src/types-shapes.ts +244 -0
- package/src/types-utilities.ts +115 -0
- package/src/types.ts +223 -0
- package/android/gradle.properties +0 -1
- package/android/src/main/AndroidManifestNew.xml +0 -2
- package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapModule.kt +0 -25
- package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapPackage.kt +0 -35
- package/android/src/newarch/KakaoMapSpec.kt +0 -7
- package/android/src/oldarch/KakaoMapSpec.kt +0 -14
- package/ios/Component/Map/RNCKakaoMapView.h +0 -23
- package/ios/Component/Map/RNCKakaoMapView.mm +0 -209
- package/ios/Component/Map/RNCKakaoMapViewManager.mm +0 -19
- package/ios/NativeModule/RNCKakaoMap.h +0 -7
- package/ios/NativeModule/RNCKakaoMap.mm +0 -19
- package/ios/NativeModule/RNCKakaoMapManager.swift +0 -7
- package/ios/Util/Util.h +0 -97
- package/lib/commonjs/component/KakaoMapView.js +0 -54
- package/lib/commonjs/component/KakaoMapView.js.map +0 -1
- package/lib/commonjs/spec/NativeKakaoMap.js +0 -9
- package/lib/commonjs/spec/NativeKakaoMap.js.map +0 -1
- package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js +0 -15
- package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
- package/lib/commonjs/type/type.js +0 -2
- package/lib/commonjs/type/type.js.map +0 -1
- package/lib/commonjs/util/Const.js +0 -14
- package/lib/commonjs/util/Const.js.map +0 -1
- package/lib/commonjs/util/Util.js +0 -15
- package/lib/commonjs/util/Util.js.map +0 -1
- package/lib/module/component/KakaoMapView.js +0 -48
- package/lib/module/component/KakaoMapView.js.map +0 -1
- package/lib/module/spec/NativeKakaoMap.js +0 -3
- package/lib/module/spec/NativeKakaoMap.js.map +0 -1
- package/lib/module/spec/RNCKakaoMapViewNativeComponent.js +0 -10
- package/lib/module/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
- package/lib/module/type/type.js +0 -2
- package/lib/module/type/type.js.map +0 -1
- package/lib/module/util/Const.js +0 -8
- package/lib/module/util/Const.js.map +0 -1
- package/lib/module/util/Util.js +0 -9
- package/lib/module/util/Util.js.map +0 -1
- package/lib/typescript/src/component/KakaoMapView.d.ts +0 -25
- package/lib/typescript/src/component/KakaoMapView.d.ts.map +0 -1
- package/lib/typescript/src/spec/NativeKakaoMap.d.ts +0 -7
- package/lib/typescript/src/spec/NativeKakaoMap.d.ts.map +0 -1
- package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts +0 -32
- package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts.map +0 -1
- package/lib/typescript/src/type/type.d.ts +0 -8
- package/lib/typescript/src/type/type.d.ts.map +0 -1
- package/lib/typescript/src/util/Const.d.ts +0 -8
- package/lib/typescript/src/util/Const.d.ts.map +0 -1
- package/lib/typescript/src/util/Util.d.ts +0 -3
- package/lib/typescript/src/util/Util.d.ts.map +0 -1
- package/src/component/KakaoMapView.tsx +0 -82
- package/src/index.ts +0 -43
- package/src/spec/NativeKakaoMap.ts +0 -8
- package/src/spec/RNCKakaoMapViewNativeComponent.ts +0 -38
- package/src/type/type.ts +0 -7
- package/src/util/Const.ts +0 -7
- package/src/util/Util.ts +0 -11
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
package net.mjstudio.rnkakao.map
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.graphics.PointF
|
|
6
|
+
import com.kakao.vectormap.KakaoMap
|
|
7
|
+
import com.kakao.vectormap.LatLng
|
|
8
|
+
import com.kakao.vectormap.MapGravity
|
|
9
|
+
import com.kakao.vectormap.mapwidget.InfoWindow
|
|
10
|
+
import com.kakao.vectormap.mapwidget.InfoWindowOptions
|
|
11
|
+
import com.kakao.vectormap.mapwidget.MapWidget
|
|
12
|
+
import com.kakao.vectormap.mapwidget.MapWidgetOptions
|
|
13
|
+
import com.kakao.vectormap.mapwidget.component.GuiImage
|
|
14
|
+
import com.kakao.vectormap.mapwidget.component.GuiLayout
|
|
15
|
+
import com.kakao.vectormap.mapwidget.component.GuiText
|
|
16
|
+
import com.kakao.vectormap.mapwidget.component.GuiView
|
|
17
|
+
import com.kakao.vectormap.mapwidget.component.Horizontal
|
|
18
|
+
import com.kakao.vectormap.mapwidget.component.Orientation
|
|
19
|
+
import com.kakao.vectormap.mapwidget.component.Vertical
|
|
20
|
+
import expo.modules.kotlin.exception.CodedException
|
|
21
|
+
import expo.modules.kotlin.records.Field
|
|
22
|
+
import expo.modules.kotlin.records.Record
|
|
23
|
+
import expo.modules.kotlin.records.Required
|
|
24
|
+
import java.lang.ref.WeakReference
|
|
25
|
+
import kotlin.math.roundToInt
|
|
26
|
+
|
|
27
|
+
private const val GUI_INVALID_ARGUMENT = "ERR_KAKAO_MAP_GUI_INVALID_ARGUMENT"
|
|
28
|
+
private const val GUI_NOT_FOUND = "ERR_KAKAO_MAP_GUI_NOT_FOUND"
|
|
29
|
+
private const val GUI_UNSUPPORTED = "ERR_KAKAO_MAP_UNSUPPORTED_PLATFORM_FEATURE"
|
|
30
|
+
private const val GUI_APPLY_CANCELLED = "ERR_KAKAO_MAP_GUI_APPLY_CANCELLED"
|
|
31
|
+
private const val GUI_NATIVE_ERROR = "ERR_KAKAO_MAP_GUI_NATIVE"
|
|
32
|
+
|
|
33
|
+
class MapGuiImageSourceRecord : Record {
|
|
34
|
+
@Field @Required
|
|
35
|
+
var uri: String = ""
|
|
36
|
+
|
|
37
|
+
@Field var width: Double? = null
|
|
38
|
+
|
|
39
|
+
@Field var height: Double? = null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class MapGuiSizeRecord : Record {
|
|
43
|
+
@Field @Required
|
|
44
|
+
var width: Double = Double.NaN
|
|
45
|
+
|
|
46
|
+
@Field @Required
|
|
47
|
+
var height: Double = Double.NaN
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class MapGuiStrokeRecord : Record {
|
|
51
|
+
@Field @Required
|
|
52
|
+
var color: String = ""
|
|
53
|
+
|
|
54
|
+
@Field @Required
|
|
55
|
+
var width: Double = Double.NaN
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class MapGuiSplitLineRecord : Record {
|
|
59
|
+
@Field @Required
|
|
60
|
+
var color: String = ""
|
|
61
|
+
|
|
62
|
+
@Field @Required
|
|
63
|
+
var width: Double = Double.NaN
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class MapGuiIosLayoutRecord : Record {
|
|
67
|
+
@Field var backgroundColor: String? = null
|
|
68
|
+
|
|
69
|
+
@Field var splitLine: MapGuiSplitLineRecord? = null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class MapGuiAndroidLayoutRecord : Record {
|
|
73
|
+
@Field var background: MapGuiNodeRecord? = null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class MapGuiNodeRecord : Record {
|
|
77
|
+
@Field @Required
|
|
78
|
+
var id: String = ""
|
|
79
|
+
|
|
80
|
+
@Field @Required
|
|
81
|
+
var type: String = ""
|
|
82
|
+
|
|
83
|
+
@Field var platform: String? = null
|
|
84
|
+
|
|
85
|
+
@Field var clickable: Boolean = false
|
|
86
|
+
|
|
87
|
+
@Field var padding: MapPaddingRecord? = null
|
|
88
|
+
|
|
89
|
+
@Field var origin: String? = null
|
|
90
|
+
|
|
91
|
+
@Field var align: String? = null
|
|
92
|
+
|
|
93
|
+
@Field var orientation: String? = null
|
|
94
|
+
|
|
95
|
+
@Field var children: List<MapGuiNodeRecord> = emptyList()
|
|
96
|
+
|
|
97
|
+
@Field var child: MapGuiNodeRecord? = null
|
|
98
|
+
|
|
99
|
+
@Field var source: MapGuiImageSourceRecord? = null
|
|
100
|
+
|
|
101
|
+
@Field var pressedSource: MapGuiImageSourceRecord? = null
|
|
102
|
+
|
|
103
|
+
@Field var sources: List<MapGuiImageSourceRecord> = emptyList()
|
|
104
|
+
|
|
105
|
+
@Field var size: MapGuiSizeRecord? = null
|
|
106
|
+
|
|
107
|
+
@Field var pressedSize: MapGuiSizeRecord? = null
|
|
108
|
+
|
|
109
|
+
@Field var stretch: MapPaddingRecord? = null
|
|
110
|
+
|
|
111
|
+
@Field var text: String? = null
|
|
112
|
+
|
|
113
|
+
@Field var textSize: Double? = null
|
|
114
|
+
|
|
115
|
+
@Field var color: String? = null
|
|
116
|
+
|
|
117
|
+
@Field var stroke: MapGuiStrokeRecord? = null
|
|
118
|
+
|
|
119
|
+
@Field var duration: Double? = null
|
|
120
|
+
|
|
121
|
+
@Field var repeat: Double? = null
|
|
122
|
+
|
|
123
|
+
@Field var autoStart: Boolean = false
|
|
124
|
+
|
|
125
|
+
@Field var ios: MapGuiIosLayoutRecord? = null
|
|
126
|
+
|
|
127
|
+
@Field var android: MapGuiAndroidLayoutRecord? = null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class MapGuiAutoMoveRecord : Record {
|
|
131
|
+
@Field @Required
|
|
132
|
+
var enabled: Boolean = false
|
|
133
|
+
|
|
134
|
+
@Field var duration: Double? = null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
class MapGuiInfoWindowRecord : Record {
|
|
138
|
+
@Field @Required
|
|
139
|
+
var id: String = ""
|
|
140
|
+
|
|
141
|
+
@Field @Required
|
|
142
|
+
var position: CoordinateRecord = CoordinateRecord()
|
|
143
|
+
|
|
144
|
+
@Field @Required
|
|
145
|
+
var body: MapGuiNodeRecord = MapGuiNodeRecord()
|
|
146
|
+
|
|
147
|
+
@Field var tail: MapGuiNodeRecord? = null
|
|
148
|
+
|
|
149
|
+
@Field var bodyOffset: ScreenPointRecord? = null
|
|
150
|
+
|
|
151
|
+
@Field var tailOffset: ScreenPointRecord? = null
|
|
152
|
+
|
|
153
|
+
@Field var visible: Boolean = true
|
|
154
|
+
|
|
155
|
+
@Field var zIndex: Int = 0
|
|
156
|
+
|
|
157
|
+
@Field var autoMove: MapGuiAutoMoveRecord? = null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
class MapGuiWidgetRecord : Record {
|
|
161
|
+
@Field @Required
|
|
162
|
+
var id: String = ""
|
|
163
|
+
|
|
164
|
+
@Field @Required
|
|
165
|
+
var position: MapWidgetPositionRecord = MapWidgetPositionRecord()
|
|
166
|
+
|
|
167
|
+
@Field @Required
|
|
168
|
+
var root: MapGuiNodeRecord = MapGuiNodeRecord()
|
|
169
|
+
|
|
170
|
+
@Field var visible: Boolean = true
|
|
171
|
+
|
|
172
|
+
@Field var zIndex: Int = 0
|
|
173
|
+
|
|
174
|
+
@Field var rotation: Double = 0.0
|
|
175
|
+
|
|
176
|
+
@Field var ios: MapGuiIosLayoutRecord? = null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
class MapGuiDropAnimationRecord : Record {
|
|
180
|
+
@Field @Required
|
|
181
|
+
var id: String = ""
|
|
182
|
+
|
|
183
|
+
@Field @Required
|
|
184
|
+
var platform: String = "ios"
|
|
185
|
+
|
|
186
|
+
@Field @Required
|
|
187
|
+
var infoWindowIds: List<String> = emptyList()
|
|
188
|
+
|
|
189
|
+
@Field @Required
|
|
190
|
+
var duration: Double = Double.NaN
|
|
191
|
+
|
|
192
|
+
@Field @Required
|
|
193
|
+
var pixelHeight: Double = Double.NaN
|
|
194
|
+
|
|
195
|
+
@Field var interpolation: String = "linear"
|
|
196
|
+
|
|
197
|
+
@Field var repeat: Double = 1.0
|
|
198
|
+
|
|
199
|
+
@Field var hideAtStop: Boolean = false
|
|
200
|
+
|
|
201
|
+
@Field var removeAtStop: Boolean = false
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
class MapGuiSceneRecord : Record {
|
|
205
|
+
@Field var infoWindows: List<MapGuiInfoWindowRecord> = emptyList()
|
|
206
|
+
|
|
207
|
+
@Field var widgets: List<MapGuiWidgetRecord> = emptyList()
|
|
208
|
+
|
|
209
|
+
@Field var animations: List<MapGuiDropAnimationRecord> = emptyList()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
internal class MapGui(
|
|
213
|
+
private val map: KakaoMap,
|
|
214
|
+
private val density: Float,
|
|
215
|
+
private val imageLoader: MapImageLoader,
|
|
216
|
+
private val emit: (Map<String, Any>) -> Unit,
|
|
217
|
+
) {
|
|
218
|
+
private val infoWindows = mutableMapOf<String, InfoWindow>()
|
|
219
|
+
private val widgets = mutableMapOf<String, MapWidget>()
|
|
220
|
+
private val imageTokens = mutableListOf<MapImageLoadToken>()
|
|
221
|
+
private var applyGeneration = 0
|
|
222
|
+
private var pendingApply: ((CodedException?) -> Unit)? = null
|
|
223
|
+
private var isDisposed = false
|
|
224
|
+
|
|
225
|
+
init {
|
|
226
|
+
val owner = WeakReference(this)
|
|
227
|
+
map.setOnInfoWindowClickListener { _, infoWindow, componentId ->
|
|
228
|
+
owner.get()?.takeUnless { it.isDisposed }?.run {
|
|
229
|
+
if (infoWindows[infoWindow.id] !== infoWindow) return@run
|
|
230
|
+
emit(
|
|
231
|
+
mapOf(
|
|
232
|
+
"type" to "tap",
|
|
233
|
+
"ownerType" to "infoWindow",
|
|
234
|
+
"ownerId" to infoWindow.id,
|
|
235
|
+
"componentId" to componentId,
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
map.setOnMapWidgetClickListener { _, widget, componentId ->
|
|
241
|
+
owner.get()?.takeUnless { it.isDisposed }?.run {
|
|
242
|
+
if (widgets[widget.id] !== widget) return@run
|
|
243
|
+
emit(
|
|
244
|
+
mapOf(
|
|
245
|
+
"type" to "tap",
|
|
246
|
+
"ownerType" to "widget",
|
|
247
|
+
"ownerId" to widget.id,
|
|
248
|
+
"componentId" to componentId,
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fun apply(
|
|
256
|
+
scene: MapGuiSceneRecord,
|
|
257
|
+
completion: (CodedException?) -> Unit,
|
|
258
|
+
) {
|
|
259
|
+
if (isDisposed) {
|
|
260
|
+
completion(CodedException(GUI_NOT_FOUND, "The GUI manager was disposed.", null))
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
validate(scene)
|
|
265
|
+
} catch (error: CodedException) {
|
|
266
|
+
completion(error)
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
applyGeneration += 1
|
|
270
|
+
pendingApply?.invoke(CodedException(GUI_APPLY_CANCELLED, "A newer GUI scene replaced this apply request.", null))
|
|
271
|
+
pendingApply = completion
|
|
272
|
+
val generation = applyGeneration
|
|
273
|
+
imageTokens.forEach(MapImageLoadToken::cancel)
|
|
274
|
+
imageTokens.clear()
|
|
275
|
+
loadImages(scene, generation) { result ->
|
|
276
|
+
if (isDisposed || generation != applyGeneration) return@loadImages
|
|
277
|
+
result.fold(
|
|
278
|
+
onSuccess = { images ->
|
|
279
|
+
try {
|
|
280
|
+
replaceScene(scene, images)
|
|
281
|
+
pendingApply = null
|
|
282
|
+
completion(null)
|
|
283
|
+
} catch (error: CodedException) {
|
|
284
|
+
runCatching(::clearScene)
|
|
285
|
+
pendingApply = null
|
|
286
|
+
completion(error)
|
|
287
|
+
} catch (error: Throwable) {
|
|
288
|
+
runCatching(::clearScene)
|
|
289
|
+
pendingApply = null
|
|
290
|
+
completion(CodedException(GUI_NATIVE_ERROR, error.message ?: "The native GUI operation failed.", error))
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
onFailure = { error ->
|
|
294
|
+
pendingApply = null
|
|
295
|
+
completion(CodedException("ERR_KAKAO_MAP_IMAGE_LOAD", error.message ?: "Image load failed.", error))
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
fun showInfoWindow(
|
|
302
|
+
id: String,
|
|
303
|
+
autoMove: Boolean,
|
|
304
|
+
duration: Double?,
|
|
305
|
+
) {
|
|
306
|
+
val infoWindow = requireInfoWindow(id)
|
|
307
|
+
val nativeDuration = duration?.let(::duration) ?: 0
|
|
308
|
+
when {
|
|
309
|
+
autoMove && nativeDuration > 0 -> infoWindow.show(true, nativeDuration)
|
|
310
|
+
autoMove -> infoWindow.show(true)
|
|
311
|
+
else -> infoWindow.show()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
fun hideInfoWindow(id: String) = requireInfoWindow(id).hide()
|
|
316
|
+
|
|
317
|
+
fun moveInfoWindow(
|
|
318
|
+
id: String,
|
|
319
|
+
position: CoordinateRecord,
|
|
320
|
+
duration: Double,
|
|
321
|
+
) {
|
|
322
|
+
position.validate()
|
|
323
|
+
requireInfoWindow(id).moveTo(position.latLng(), duration(duration).toLong())
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
fun showWidget(id: String) = requireWidget(id).show()
|
|
327
|
+
|
|
328
|
+
fun hideWidget(id: String) = requireWidget(id).hide()
|
|
329
|
+
|
|
330
|
+
fun positionWidget(
|
|
331
|
+
id: String,
|
|
332
|
+
position: MapWidgetPositionRecord,
|
|
333
|
+
) {
|
|
334
|
+
position.validate("Widget position")
|
|
335
|
+
requireWidget(id).setPosition(position.nativeGravity(), position.x.toDpPixels(), position.y.toDpPixels())
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
fun rotateWidget(
|
|
339
|
+
id: String,
|
|
340
|
+
rotation: Double,
|
|
341
|
+
) {
|
|
342
|
+
if (!rotation.isFinite()) invalid("Widget rotation must be finite.")
|
|
343
|
+
requireWidget(id).setRotation(Math.toRadians(rotation).toFloat())
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
fun startAnimation(id: String): Unit =
|
|
347
|
+
throw CodedException(
|
|
348
|
+
GUI_UNSUPPORTED,
|
|
349
|
+
"Info-window animator $id is unavailable in Android SDK 2.15.2.",
|
|
350
|
+
null,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
fun stopAnimation(id: String): Unit = startAnimation(id)
|
|
354
|
+
|
|
355
|
+
fun controlAnimatedImage(
|
|
356
|
+
ownerType: String,
|
|
357
|
+
ownerId: String,
|
|
358
|
+
componentId: String,
|
|
359
|
+
action: String,
|
|
360
|
+
): Unit =
|
|
361
|
+
throw CodedException(
|
|
362
|
+
GUI_UNSUPPORTED,
|
|
363
|
+
"Animated GUI image $componentId on $ownerType $ownerId cannot run $action on Android SDK 2.15.2.",
|
|
364
|
+
null,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
fun status(): Map<String, Any> {
|
|
368
|
+
if (isDisposed) throw CodedException(GUI_NOT_FOUND, "The GUI manager was disposed.", null)
|
|
369
|
+
val manager = requireManager()
|
|
370
|
+
val infoWindowStatus =
|
|
371
|
+
manager.infoWindowLayer.allInfoWindows.orEmpty().sortedBy(InfoWindow::getId).map { infoWindow ->
|
|
372
|
+
mapOf(
|
|
373
|
+
"id" to infoWindow.id,
|
|
374
|
+
"visible" to infoWindow.isShow,
|
|
375
|
+
"position" to
|
|
376
|
+
mapOf(
|
|
377
|
+
"latitude" to infoWindow.position.latitude,
|
|
378
|
+
"longitude" to infoWindow.position.longitude,
|
|
379
|
+
),
|
|
380
|
+
"zIndex" to infoWindow.zOrder,
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
val widgetStatus =
|
|
384
|
+
manager.mapWidgetLayer.allMapWidgets.orEmpty().sortedBy(MapWidget::getId).map { widget ->
|
|
385
|
+
mapOf(
|
|
386
|
+
"id" to widget.id,
|
|
387
|
+
"visible" to widget.isShow,
|
|
388
|
+
"position" to
|
|
389
|
+
mapOf(
|
|
390
|
+
"gravity" to gravityName(widget.mapGravity),
|
|
391
|
+
"x" to widget.position.x / density,
|
|
392
|
+
"y" to widget.position.y / density,
|
|
393
|
+
),
|
|
394
|
+
"rotation" to Math.toDegrees(widget.rotation.toDouble()),
|
|
395
|
+
"zIndex" to widget.zOrder,
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
return mapOf(
|
|
399
|
+
"infoWindows" to infoWindowStatus,
|
|
400
|
+
"widgets" to widgetStatus,
|
|
401
|
+
"animations" to emptyList<Map<String, Any>>(),
|
|
402
|
+
"counts" to
|
|
403
|
+
mapOf(
|
|
404
|
+
"infoWindows" to infoWindowStatus.size,
|
|
405
|
+
"widgets" to widgetStatus.size,
|
|
406
|
+
"animations" to 0,
|
|
407
|
+
),
|
|
408
|
+
"capabilities" to
|
|
409
|
+
mapOf(
|
|
410
|
+
"button" to false,
|
|
411
|
+
"animatedImage" to false,
|
|
412
|
+
"infoWindowAnimation" to false,
|
|
413
|
+
"arbitraryComponentClicks" to true,
|
|
414
|
+
"componentAnimationEvents" to false,
|
|
415
|
+
"guiMoveEvents" to false,
|
|
416
|
+
"autoMoveDuration" to true,
|
|
417
|
+
),
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
fun dispose() {
|
|
422
|
+
if (isDisposed) return
|
|
423
|
+
isDisposed = true
|
|
424
|
+
applyGeneration += 1
|
|
425
|
+
pendingApply?.invoke(CodedException(GUI_APPLY_CANCELLED, "The GUI manager was disposed.", null))
|
|
426
|
+
pendingApply = null
|
|
427
|
+
imageTokens.forEach(MapImageLoadToken::cancel)
|
|
428
|
+
imageTokens.clear()
|
|
429
|
+
runCatching(::clearScene)
|
|
430
|
+
infoWindows.clear()
|
|
431
|
+
widgets.clear()
|
|
432
|
+
runCatching { map.setOnInfoWindowClickListener(null) }
|
|
433
|
+
runCatching { map.setOnMapWidgetClickListener(null) }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private fun replaceScene(
|
|
437
|
+
scene: MapGuiSceneRecord,
|
|
438
|
+
images: Map<String, Bitmap>,
|
|
439
|
+
) {
|
|
440
|
+
clearScene()
|
|
441
|
+
val manager = requireManager()
|
|
442
|
+
scene.infoWindows.forEach { record ->
|
|
443
|
+
val options =
|
|
444
|
+
InfoWindowOptions
|
|
445
|
+
.from(record.id, record.position.latLng())
|
|
446
|
+
.setBody(component(record.body, images))
|
|
447
|
+
.setApplyDpScale(false)
|
|
448
|
+
.setVisible(false)
|
|
449
|
+
.setZOrder(record.zIndex)
|
|
450
|
+
record.tail?.let { options.setTail(image(it, images)) }
|
|
451
|
+
record.bodyOffset?.let {
|
|
452
|
+
options.setBodyOffset(it.x.dpPixelsFloat("Info-window body offset x"), it.y.dpPixelsFloat("Info-window body offset y"))
|
|
453
|
+
}
|
|
454
|
+
record.tailOffset?.let {
|
|
455
|
+
options.setTailOffset(it.x.dpPixelsFloat("Info-window tail offset x"), it.y.dpPixelsFloat("Info-window tail offset y"))
|
|
456
|
+
}
|
|
457
|
+
val infoWindow =
|
|
458
|
+
manager.infoWindowLayer.addInfoWindow(options)
|
|
459
|
+
?: throw CodedException(GUI_NATIVE_ERROR, "Could not add info window ${record.id}.", null)
|
|
460
|
+
if (record.visible) {
|
|
461
|
+
val autoMove = record.autoMove
|
|
462
|
+
when {
|
|
463
|
+
autoMove?.enabled == true && autoMove.duration != null -> {
|
|
464
|
+
infoWindow.show(true, duration(autoMove.duration!!))
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
autoMove?.enabled == true -> {
|
|
468
|
+
infoWindow.show(true)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
else -> {
|
|
472
|
+
infoWindow.show()
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
infoWindows[record.id] = infoWindow
|
|
477
|
+
}
|
|
478
|
+
scene.widgets.forEach { record ->
|
|
479
|
+
val options =
|
|
480
|
+
MapWidgetOptions
|
|
481
|
+
.from(
|
|
482
|
+
record.id,
|
|
483
|
+
record.position.nativeGravity(),
|
|
484
|
+
record.position.x.dpPixelsInt("Widget position x"),
|
|
485
|
+
record.position.y.dpPixelsInt("Widget position y"),
|
|
486
|
+
).setRootView(component(record.root, images))
|
|
487
|
+
.setVisible(record.visible)
|
|
488
|
+
.setZOrder(record.zIndex)
|
|
489
|
+
.setRotation(Math.toRadians(record.rotation).toFloat())
|
|
490
|
+
val widget =
|
|
491
|
+
manager.mapWidgetLayer.addMapWidget(options)
|
|
492
|
+
?: throw CodedException(GUI_NATIVE_ERROR, "Could not add widget ${record.id}.", null)
|
|
493
|
+
widgets[record.id] = widget
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private fun clearScene() {
|
|
498
|
+
val manager = requireManager()
|
|
499
|
+
manager.infoWindowLayer.removeAll()
|
|
500
|
+
manager.mapWidgetLayer.removeAll()
|
|
501
|
+
infoWindows.clear()
|
|
502
|
+
widgets.clear()
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private fun component(
|
|
506
|
+
record: MapGuiNodeRecord,
|
|
507
|
+
images: Map<String, Bitmap>,
|
|
508
|
+
): GuiView {
|
|
509
|
+
val result =
|
|
510
|
+
when (record.type) {
|
|
511
|
+
"layout" -> {
|
|
512
|
+
GuiLayout(if (record.orientation == "horizontal") Orientation.Horizontal else Orientation.Vertical).apply {
|
|
513
|
+
record.children.forEach { addView(component(it, images)) }
|
|
514
|
+
record.android?.background?.let { setBackground(image(it, images)) }
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
"image" -> {
|
|
519
|
+
image(record, images)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
"text" -> {
|
|
523
|
+
val text = record.text ?: invalid("GUI text is required.")
|
|
524
|
+
GuiText(text).apply {
|
|
525
|
+
setTextSize(positiveDpPixels(record.textSize ?: 0.0, "Text size"))
|
|
526
|
+
setTextColor(color(record.color ?: ""))
|
|
527
|
+
record.stroke?.let {
|
|
528
|
+
setStrokeColor(color(it.color))
|
|
529
|
+
setStrokeSize(nonnegativeDpPixels(it.width, "Text stroke width"))
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
"button", "animatedImage" -> {
|
|
535
|
+
throw CodedException(
|
|
536
|
+
GUI_UNSUPPORTED,
|
|
537
|
+
"${record.type} is unavailable in Android SDK 2.15.2.",
|
|
538
|
+
null,
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
else -> {
|
|
543
|
+
invalid("Unknown GUI component type: ${record.type}.")
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
result.id = record.id
|
|
547
|
+
result.isClickable = record.clickable
|
|
548
|
+
record.padding?.let {
|
|
549
|
+
result.setPadding(
|
|
550
|
+
nonnegativeDpPixels(it.left, "GUI padding left"),
|
|
551
|
+
nonnegativeDpPixels(it.top, "GUI padding top"),
|
|
552
|
+
nonnegativeDpPixels(it.right, "GUI padding right"),
|
|
553
|
+
nonnegativeDpPixels(it.bottom, "GUI padding bottom"),
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
val origin = alignment(record.origin ?: "topLeft")
|
|
557
|
+
result.setOrigin(origin.first, origin.second)
|
|
558
|
+
val align = alignment(record.align ?: "topLeft")
|
|
559
|
+
result.setAlign(align.first, align.second)
|
|
560
|
+
return result
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private fun image(
|
|
564
|
+
record: MapGuiNodeRecord,
|
|
565
|
+
images: Map<String, Bitmap>,
|
|
566
|
+
): GuiImage {
|
|
567
|
+
val source = record.source
|
|
568
|
+
val nativeImage = requiredImage(source, images)
|
|
569
|
+
val requestedWidth = record.size?.width ?: source?.width
|
|
570
|
+
val requestedHeight = record.size?.height ?: source?.height
|
|
571
|
+
val aspectRatio = nativeImage.width.toDouble() / nativeImage.height
|
|
572
|
+
val width = requestedWidth ?: requestedHeight?.times(aspectRatio)
|
|
573
|
+
val height = requestedHeight ?: requestedWidth?.div(aspectRatio)
|
|
574
|
+
val sizedImage =
|
|
575
|
+
if (width != null && height != null) {
|
|
576
|
+
Bitmap.createScaledBitmap(
|
|
577
|
+
nativeImage,
|
|
578
|
+
positiveDpPixels(width, "Image width"),
|
|
579
|
+
positiveDpPixels(height, "Image height"),
|
|
580
|
+
true,
|
|
581
|
+
)
|
|
582
|
+
} else {
|
|
583
|
+
nativeImage
|
|
584
|
+
}
|
|
585
|
+
val displayedImage =
|
|
586
|
+
(if (sizedImage === nativeImage) sizedImage.copy(Bitmap.Config.ARGB_8888, false) else sizedImage).also {
|
|
587
|
+
it.density = Bitmap.DENSITY_NONE
|
|
588
|
+
}
|
|
589
|
+
val guiImage = GuiImage(displayedImage)
|
|
590
|
+
record.stretch?.let {
|
|
591
|
+
guiImage.setFixedArea(
|
|
592
|
+
nonnegativeDpPixels(it.left, "Image stretch left"),
|
|
593
|
+
nonnegativeDpPixels(it.top, "Image stretch top"),
|
|
594
|
+
nonnegativeDpPixels(it.right, "Image stretch right"),
|
|
595
|
+
nonnegativeDpPixels(it.bottom, "Image stretch bottom"),
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
record.child?.let { guiImage.addChild(component(it, images)) }
|
|
599
|
+
return guiImage
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private fun loadImages(
|
|
603
|
+
scene: MapGuiSceneRecord,
|
|
604
|
+
generation: Int,
|
|
605
|
+
completion: (Result<Map<String, Bitmap>>) -> Unit,
|
|
606
|
+
) {
|
|
607
|
+
val sources = mutableMapOf<String, MapGuiImageSourceRecord>()
|
|
608
|
+
scene.infoWindows.forEach {
|
|
609
|
+
collectImages(it.body, sources)
|
|
610
|
+
it.tail?.let { tail -> collectImages(tail, sources) }
|
|
611
|
+
}
|
|
612
|
+
scene.widgets.forEach { collectImages(it.root, sources) }
|
|
613
|
+
if (sources.isEmpty()) {
|
|
614
|
+
completion(Result.success(emptyMap()))
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
val images = mutableMapOf<String, Bitmap>()
|
|
618
|
+
var remaining = sources.size
|
|
619
|
+
var failure: Throwable? = null
|
|
620
|
+
sources.values.forEach { source ->
|
|
621
|
+
imageTokens +=
|
|
622
|
+
imageLoader.load(source.uri) { result ->
|
|
623
|
+
if (isDisposed || generation != applyGeneration) return@load
|
|
624
|
+
result.onSuccess { images[source.uri] = it }
|
|
625
|
+
result.onFailure { if (failure == null) failure = it }
|
|
626
|
+
remaining -= 1
|
|
627
|
+
if (remaining == 0) {
|
|
628
|
+
imageTokens.clear()
|
|
629
|
+
completion(failure?.let { Result.failure(it) } ?: Result.success(images))
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private fun collectImages(
|
|
636
|
+
record: MapGuiNodeRecord,
|
|
637
|
+
sources: MutableMap<String, MapGuiImageSourceRecord>,
|
|
638
|
+
) {
|
|
639
|
+
listOfNotNull(record.source, record.pressedSource).forEach { sources[it.uri] = it }
|
|
640
|
+
record.sources.forEach { sources[it.uri] = it }
|
|
641
|
+
record.children.forEach { collectImages(it, sources) }
|
|
642
|
+
record.child?.let { collectImages(it, sources) }
|
|
643
|
+
record.android?.background?.let { collectImages(it, sources) }
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private fun validate(scene: MapGuiSceneRecord) {
|
|
647
|
+
unique(scene.infoWindows.map { it.id }, "info-window")
|
|
648
|
+
unique(scene.widgets.map { it.id }, "widget")
|
|
649
|
+
unique(scene.animations.map { it.id }, "GUI animation")
|
|
650
|
+
if (scene.animations.isNotEmpty()) {
|
|
651
|
+
throw CodedException(GUI_UNSUPPORTED, "Info-window animators are unavailable in Android SDK 2.15.2.", null)
|
|
652
|
+
}
|
|
653
|
+
scene.infoWindows.forEach { record ->
|
|
654
|
+
nonempty(record.id, "Info-window ID")
|
|
655
|
+
record.position.validate()
|
|
656
|
+
record.bodyOffset?.let {
|
|
657
|
+
it.validate("Info-window body offset")
|
|
658
|
+
it.x.dpPixelsFloat("Info-window body offset x")
|
|
659
|
+
it.y.dpPixelsFloat("Info-window body offset y")
|
|
660
|
+
}
|
|
661
|
+
record.tailOffset?.let {
|
|
662
|
+
it.validate("Info-window tail offset")
|
|
663
|
+
it.x.dpPixelsFloat("Info-window tail offset x")
|
|
664
|
+
it.y.dpPixelsFloat("Info-window tail offset y")
|
|
665
|
+
}
|
|
666
|
+
val componentIds = mutableSetOf<String>()
|
|
667
|
+
validate(record.body, componentIds)
|
|
668
|
+
record.tail?.let {
|
|
669
|
+
if (it.type != "image") invalid("Info-window tails must be images.")
|
|
670
|
+
validate(it, componentIds)
|
|
671
|
+
}
|
|
672
|
+
record.autoMove?.duration?.let(::duration)
|
|
673
|
+
}
|
|
674
|
+
scene.widgets.forEach { record ->
|
|
675
|
+
nonempty(record.id, "Widget ID")
|
|
676
|
+
if (record.root.type != "layout") invalid("Widget roots must be layouts.")
|
|
677
|
+
record.position.validate("Widget position")
|
|
678
|
+
record.position.x.dpPixelsInt("Widget position x")
|
|
679
|
+
record.position.y.dpPixelsInt("Widget position y")
|
|
680
|
+
if (!record.rotation.isFinite()) invalid("Widget rotation must be finite.")
|
|
681
|
+
validate(record.root, mutableSetOf())
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
private fun validate(
|
|
686
|
+
record: MapGuiNodeRecord,
|
|
687
|
+
ids: MutableSet<String>,
|
|
688
|
+
) {
|
|
689
|
+
nonempty(record.id, "GUI component ID")
|
|
690
|
+
if (!ids.add(record.id)) invalid("Duplicate GUI component ID: ${record.id}.")
|
|
691
|
+
alignment(record.origin ?: "topLeft")
|
|
692
|
+
alignment(record.align ?: "topLeft")
|
|
693
|
+
record.size?.validate("Image size")
|
|
694
|
+
record.pressedSize?.validate("Pressed image size")
|
|
695
|
+
record.stretch?.validateNonnegative("Image stretch")
|
|
696
|
+
record.padding?.validateNonnegative("GUI padding")
|
|
697
|
+
when (record.type) {
|
|
698
|
+
"layout" -> {
|
|
699
|
+
if (record.orientation !in setOf("horizontal", "vertical")) {
|
|
700
|
+
invalid("GUI layout orientation must be horizontal or vertical.")
|
|
701
|
+
}
|
|
702
|
+
record.children.forEach { validate(it, ids) }
|
|
703
|
+
record.android?.background?.let { validate(it, ids) }
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
"image" -> {
|
|
707
|
+
validateSource(record.source)
|
|
708
|
+
record.child?.let { validate(it, ids) }
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
"text" -> {
|
|
712
|
+
if (record.text == null) invalid("GUI text is required.")
|
|
713
|
+
positiveDpPixels(record.textSize ?: 0.0, "Text size")
|
|
714
|
+
color(record.color ?: "")
|
|
715
|
+
record.stroke?.let {
|
|
716
|
+
color(it.color)
|
|
717
|
+
nonnegativeDpPixels(it.width, "Text stroke width")
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
"button", "animatedImage" -> {
|
|
722
|
+
throw CodedException(GUI_UNSUPPORTED, "${record.type} is unavailable in Android SDK 2.15.2.", null)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
else -> {
|
|
726
|
+
invalid("Unknown GUI component type: ${record.type}.")
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private fun validateSource(source: MapGuiImageSourceRecord?) {
|
|
732
|
+
if (source == null || source.uri.isBlank()) invalid("GUI image URI must not be empty.")
|
|
733
|
+
source.width?.let { positiveDpPixels(it, "Image source width") }
|
|
734
|
+
source.height?.let { positiveDpPixels(it, "Image source height") }
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private fun requiredImage(
|
|
738
|
+
source: MapGuiImageSourceRecord?,
|
|
739
|
+
images: Map<String, Bitmap>,
|
|
740
|
+
): Bitmap {
|
|
741
|
+
validateSource(source)
|
|
742
|
+
return images[source!!.uri] ?: invalid("GUI image was not loaded.")
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private fun requireInfoWindow(id: String): InfoWindow {
|
|
746
|
+
if (isDisposed) throw CodedException(GUI_NOT_FOUND, "The GUI manager was disposed.", null)
|
|
747
|
+
return infoWindows[id] ?: throw CodedException(GUI_NOT_FOUND, "Info window $id was not found.", null)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private fun requireWidget(id: String): MapWidget {
|
|
751
|
+
if (isDisposed) throw CodedException(GUI_NOT_FOUND, "The GUI manager was disposed.", null)
|
|
752
|
+
return widgets[id] ?: throw CodedException(GUI_NOT_FOUND, "Widget $id was not found.", null)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
private fun requireManager() = map.mapWidgetManager ?: throw CodedException(GUI_NOT_FOUND, "The map widget manager is unavailable.", null)
|
|
756
|
+
|
|
757
|
+
private fun CoordinateRecord.latLng() = LatLng.from(latitude, longitude)
|
|
758
|
+
|
|
759
|
+
private fun Double.toDpPixels() = dpPixelsFloat("Screen position")
|
|
760
|
+
|
|
761
|
+
private fun Double.dpPixelsFloat(name: String): Float {
|
|
762
|
+
val pixels = this * density
|
|
763
|
+
if (!pixels.isFinite() || pixels > Float.MAX_VALUE || pixels < -Float.MAX_VALUE) {
|
|
764
|
+
invalid("$name must fit a 32-bit floating-point pixel value.")
|
|
765
|
+
}
|
|
766
|
+
return pixels.toFloat()
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private fun Double.dpPixelsInt(name: String): Int {
|
|
770
|
+
val pixels = this * density
|
|
771
|
+
if (!pixels.isFinite() || pixels > Int.MAX_VALUE || pixels < Int.MIN_VALUE) {
|
|
772
|
+
invalid("$name is out of range after pixel conversion.")
|
|
773
|
+
}
|
|
774
|
+
return pixels.roundToInt()
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private fun positiveDpPixels(
|
|
778
|
+
value: Double,
|
|
779
|
+
name: String,
|
|
780
|
+
): Int {
|
|
781
|
+
positiveInt(value, name)
|
|
782
|
+
return pixelInt(value, name, isPositive = true)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
private fun nonnegativeDpPixels(
|
|
786
|
+
value: Double,
|
|
787
|
+
name: String,
|
|
788
|
+
): Int {
|
|
789
|
+
nonnegativeInt(value, name)
|
|
790
|
+
return pixelInt(value, name, isPositive = false)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
private fun pixelInt(
|
|
794
|
+
value: Double,
|
|
795
|
+
name: String,
|
|
796
|
+
isPositive: Boolean,
|
|
797
|
+
): Int {
|
|
798
|
+
val pixels = value * density
|
|
799
|
+
if (!pixels.isFinite() || pixels > Int.MAX_VALUE) invalid("$name is too large after pixel conversion.")
|
|
800
|
+
val result = pixels.roundToInt()
|
|
801
|
+
if (isPositive && result <= 0) invalid("$name is too small after pixel conversion.")
|
|
802
|
+
return result
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
private fun MapGuiSizeRecord.validate(name: String) {
|
|
806
|
+
positiveDpPixels(width, "$name width")
|
|
807
|
+
positiveDpPixels(height, "$name height")
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private fun MapPaddingRecord.validateNonnegative(name: String) {
|
|
811
|
+
nonnegativeDpPixels(top, "$name top")
|
|
812
|
+
nonnegativeDpPixels(right, "$name right")
|
|
813
|
+
nonnegativeDpPixels(bottom, "$name bottom")
|
|
814
|
+
nonnegativeDpPixels(left, "$name left")
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
private fun alignment(value: String): Pair<Vertical, Horizontal> =
|
|
818
|
+
when (value) {
|
|
819
|
+
"topLeft" -> Vertical.Top to Horizontal.Left
|
|
820
|
+
"topCenter" -> Vertical.Top to Horizontal.Center
|
|
821
|
+
"topRight" -> Vertical.Top to Horizontal.Right
|
|
822
|
+
"centerLeft" -> Vertical.Center to Horizontal.Left
|
|
823
|
+
"center" -> Vertical.Center to Horizontal.Center
|
|
824
|
+
"centerRight" -> Vertical.Center to Horizontal.Right
|
|
825
|
+
"bottomLeft" -> Vertical.Bottom to Horizontal.Left
|
|
826
|
+
"bottomCenter" -> Vertical.Bottom to Horizontal.Center
|
|
827
|
+
"bottomRight" -> Vertical.Bottom to Horizontal.Right
|
|
828
|
+
else -> invalid("Unknown GUI alignment: $value.")
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
private fun gravityName(gravity: Int) =
|
|
832
|
+
when (gravity) {
|
|
833
|
+
MapGravity.TOP or MapGravity.LEFT -> "topLeft"
|
|
834
|
+
MapGravity.TOP or MapGravity.CENTER_HORIZONTAL -> "topCenter"
|
|
835
|
+
MapGravity.TOP or MapGravity.RIGHT -> "topRight"
|
|
836
|
+
MapGravity.CENTER_VERTICAL or MapGravity.LEFT -> "centerLeft"
|
|
837
|
+
MapGravity.CENTER -> "center"
|
|
838
|
+
MapGravity.CENTER_VERTICAL or MapGravity.RIGHT -> "centerRight"
|
|
839
|
+
MapGravity.BOTTOM or MapGravity.LEFT -> "bottomLeft"
|
|
840
|
+
MapGravity.BOTTOM or MapGravity.CENTER_HORIZONTAL -> "bottomCenter"
|
|
841
|
+
MapGravity.BOTTOM or MapGravity.RIGHT -> "bottomRight"
|
|
842
|
+
else -> "topLeft"
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
private fun color(value: String): Int {
|
|
846
|
+
if (!Regex("^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$").matches(value)) {
|
|
847
|
+
invalid("Colors must use #RRGGBB or #RRGGBBAA.")
|
|
848
|
+
}
|
|
849
|
+
return if (value.length == 7) {
|
|
850
|
+
Color.parseColor(value)
|
|
851
|
+
} else {
|
|
852
|
+
val rgba = value.drop(1).toLong(16)
|
|
853
|
+
Color.argb(
|
|
854
|
+
(rgba and 0xFF).toInt(),
|
|
855
|
+
((rgba shr 24) and 0xFF).toInt(),
|
|
856
|
+
((rgba shr 16) and 0xFF).toInt(),
|
|
857
|
+
((rgba shr 8) and 0xFF).toInt(),
|
|
858
|
+
)
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private fun duration(value: Double): Int = nonnegativeInt(value, "Duration")
|
|
863
|
+
|
|
864
|
+
private fun positiveInt(
|
|
865
|
+
value: Double,
|
|
866
|
+
name: String,
|
|
867
|
+
): Int {
|
|
868
|
+
if (!value.isFinite() || value <= 0 || value > Int.MAX_VALUE || value % 1.0 != 0.0) {
|
|
869
|
+
invalid("$name must be a positive integer.")
|
|
870
|
+
}
|
|
871
|
+
return value.toInt()
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
private fun nonnegativeInt(
|
|
875
|
+
value: Double,
|
|
876
|
+
name: String,
|
|
877
|
+
): Int {
|
|
878
|
+
if (!value.isFinite() || value < 0 || value > Int.MAX_VALUE || value % 1.0 != 0.0) {
|
|
879
|
+
invalid("$name must be a nonnegative integer.")
|
|
880
|
+
}
|
|
881
|
+
return value.toInt()
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
private fun unique(
|
|
885
|
+
values: List<String>,
|
|
886
|
+
name: String,
|
|
887
|
+
) {
|
|
888
|
+
if (values.toSet().size != values.size) invalid("Duplicate $name ID.")
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private fun nonempty(
|
|
892
|
+
value: String,
|
|
893
|
+
name: String,
|
|
894
|
+
) {
|
|
895
|
+
if (value.isBlank()) invalid("$name must not be empty.")
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
private fun invalid(message: String): Nothing = throw CodedException(GUI_INVALID_ARGUMENT, message, null)
|
|
899
|
+
}
|