expo-gaode-map 2.2.15 → 2.2.16
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/android/build.gradle +1 -1
- package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapView.kt +40 -1
- package/android/src/main/java/expo/modules/gaodemap/overlays/ClusterView.kt +427 -61
- package/android/src/main/java/expo/modules/gaodemap/overlays/ClusterViewModule.kt +16 -0
- package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapView.kt +160 -25
- package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapViewModule.kt +13 -1
- package/android/src/main/java/expo/modules/gaodemap/overlays/MultiPointView.kt +165 -13
- package/android/src/main/java/expo/modules/gaodemap/overlays/MultiPointViewModule.kt +9 -1
- package/android/src/main/java/expo/modules/gaodemap/utils/BitmapDescriptorCache.kt +20 -0
- package/build/components/overlays/Cluster.d.ts.map +1 -1
- package/build/components/overlays/Cluster.js +6 -2
- package/build/components/overlays/Cluster.js.map +1 -1
- package/build/components/overlays/HeatMap.d.ts.map +1 -1
- package/build/components/overlays/HeatMap.js +12 -1
- package/build/components/overlays/HeatMap.js.map +1 -1
- package/build/index.d.ts +0 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/types/overlays.types.d.ts +69 -14
- package/build/types/overlays.types.d.ts.map +1 -1
- package/build/types/overlays.types.js.map +1 -1
- package/build/utils/ModuleLoader.js +1 -1
- package/build/utils/ModuleLoader.js.map +1 -1
- package/ios/ExpoGaodeMapView.swift +44 -0
- package/ios/overlays/ClusterAnnotation.swift +32 -0
- package/ios/overlays/ClusterView.swift +251 -45
- package/ios/overlays/ClusterViewModule.swift +14 -0
- package/ios/overlays/CoordinateQuadTree.swift +291 -0
- package/ios/overlays/HeatMapView.swift +119 -5
- package/ios/overlays/HeatMapViewModule.swift +13 -1
- package/ios/overlays/MultiPointView.swift +160 -2
- package/ios/overlays/MultiPointViewModule.swift +22 -0
- package/package.json +1 -1
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
package expo.modules.gaodemap.overlays
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import android.util.Log
|
|
4
6
|
import com.amap.api.maps.AMap
|
|
5
7
|
import com.amap.api.maps.model.LatLng
|
|
6
8
|
import com.amap.api.maps.model.TileOverlay
|
|
7
9
|
import com.amap.api.maps.model.TileOverlayOptions
|
|
8
10
|
import com.amap.api.maps.model.HeatmapTileProvider
|
|
11
|
+
import com.amap.api.maps.CameraUpdateFactory
|
|
9
12
|
import expo.modules.kotlin.AppContext
|
|
10
13
|
import expo.modules.kotlin.views.ExpoView
|
|
14
|
+
import java.util.concurrent.ExecutorService
|
|
15
|
+
import java.util.concurrent.Executors
|
|
11
16
|
|
|
12
17
|
class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
|
13
18
|
|
|
19
|
+
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
20
|
+
private val applyUpdateRunnable = Runnable { applyUpdateOnMain() }
|
|
21
|
+
@Volatile private var updateToken: Int = 0
|
|
22
|
+
@Volatile private var needsRebuild: Boolean = true
|
|
23
|
+
private var visible: Boolean = true
|
|
24
|
+
|
|
14
25
|
private var heatmapOverlay: TileOverlay? = null
|
|
15
26
|
private var aMap: AMap? = null
|
|
16
27
|
private var dataList: MutableList<LatLng> = mutableListOf()
|
|
@@ -23,7 +34,8 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
|
|
|
23
34
|
@Suppress("unused")
|
|
24
35
|
fun setMap(map: AMap) {
|
|
25
36
|
aMap = map
|
|
26
|
-
|
|
37
|
+
needsRebuild = true
|
|
38
|
+
scheduleUpdate()
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
/**
|
|
@@ -34,12 +46,12 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
|
|
|
34
46
|
data.forEach { point ->
|
|
35
47
|
val lat = (point["latitude"] as? Number)?.toDouble()
|
|
36
48
|
val lng = (point["longitude"] as? Number)?.toDouble()
|
|
37
|
-
// 坐标验证
|
|
38
49
|
if (lat != null && lng != null && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
|
|
39
50
|
dataList.add(LatLng(lat, lng))
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
|
-
|
|
53
|
+
needsRebuild = true
|
|
54
|
+
scheduleUpdate()
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
/**
|
|
@@ -47,7 +59,8 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
|
|
|
47
59
|
*/
|
|
48
60
|
fun setRadius(radiusValue: Int) {
|
|
49
61
|
radius = radiusValue
|
|
50
|
-
|
|
62
|
+
needsRebuild = true
|
|
63
|
+
scheduleUpdate()
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
/**
|
|
@@ -55,41 +68,162 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
|
|
|
55
68
|
*/
|
|
56
69
|
fun setOpacity(opacityValue: Double) {
|
|
57
70
|
opacity = opacityValue
|
|
58
|
-
|
|
71
|
+
applyOverlayOpacity()
|
|
59
72
|
}
|
|
60
|
-
|
|
73
|
+
|
|
74
|
+
fun setVisible(visibleValue: Boolean) {
|
|
75
|
+
if (!visibleValue) {
|
|
76
|
+
visible = false
|
|
77
|
+
updateToken += 1
|
|
78
|
+
removeCallbacks(applyUpdateRunnable)
|
|
79
|
+
applyOverlayVisibility()
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
visible = true
|
|
84
|
+
applyOverlayVisibility()
|
|
85
|
+
|
|
86
|
+
if (dataList.isEmpty()) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (heatmapOverlay == null || needsRebuild) {
|
|
91
|
+
scheduleUpdate()
|
|
92
|
+
} else {
|
|
93
|
+
applyOverlayOpacity()
|
|
94
|
+
forceRefresh()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private fun scheduleUpdate() {
|
|
99
|
+
updateToken += 1
|
|
100
|
+
removeCallbacks(applyUpdateRunnable)
|
|
101
|
+
postDelayed(applyUpdateRunnable, 32)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private fun applyOverlayVisibility() {
|
|
105
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
106
|
+
post { applyOverlayVisibility() }
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
val overlay = heatmapOverlay ?: return
|
|
110
|
+
runCatching {
|
|
111
|
+
overlay.javaClass.getMethod("setVisible", Boolean::class.javaPrimitiveType)
|
|
112
|
+
.invoke(overlay, visible)
|
|
113
|
+
}.onFailure {
|
|
114
|
+
if (!visible) {
|
|
115
|
+
overlay.remove()
|
|
116
|
+
heatmapOverlay = null
|
|
117
|
+
needsRebuild = true
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private fun applyOverlayOpacity() {
|
|
123
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
124
|
+
post { applyOverlayOpacity() }
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
val overlay = heatmapOverlay ?: return
|
|
128
|
+
val opacityValue = opacity.coerceIn(0.0, 1.0)
|
|
129
|
+
val transparency = (1.0 - opacityValue).toFloat()
|
|
130
|
+
runCatching {
|
|
131
|
+
overlay.javaClass.getMethod("setTransparency", Float::class.javaPrimitiveType)
|
|
132
|
+
.invoke(overlay, transparency)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
61
136
|
/**
|
|
62
|
-
*
|
|
137
|
+
* 在主线程应用更新(构建 TileProvider 在后台线程)
|
|
63
138
|
*/
|
|
64
|
-
private fun
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
139
|
+
private fun applyUpdateOnMain() {
|
|
140
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
141
|
+
post { applyUpdateOnMain() }
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
val map = aMap ?: return
|
|
146
|
+
val token = updateToken
|
|
147
|
+
val pointsSnapshot = ArrayList(dataList)
|
|
148
|
+
val radiusValue = radius.coerceIn(10, 200)
|
|
149
|
+
val opacityValue = opacity.coerceIn(0.0, 1.0)
|
|
150
|
+
|
|
151
|
+
if (!visible) {
|
|
152
|
+
applyOverlayVisibility()
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (pointsSnapshot.isEmpty()) {
|
|
157
|
+
heatmapOverlay?.remove()
|
|
158
|
+
heatmapOverlay = null
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (heatmapOverlay != null && !needsRebuild) {
|
|
163
|
+
applyOverlayVisibility()
|
|
164
|
+
applyOverlayOpacity()
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
executor.execute {
|
|
169
|
+
try {
|
|
170
|
+
val provider = HeatmapTileProvider.Builder()
|
|
171
|
+
.data(pointsSnapshot)
|
|
172
|
+
.radius(radiusValue)
|
|
173
|
+
.build()
|
|
174
|
+
|
|
175
|
+
post {
|
|
176
|
+
if (token != updateToken) {
|
|
177
|
+
return@post
|
|
178
|
+
}
|
|
179
|
+
if (aMap !== map) {
|
|
180
|
+
return@post
|
|
181
|
+
}
|
|
182
|
+
if (!visible) {
|
|
183
|
+
return@post
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
heatmapOverlay?.remove()
|
|
187
|
+
|
|
188
|
+
val options = TileOverlayOptions().tileProvider(provider)
|
|
189
|
+
|
|
190
|
+
runCatching {
|
|
191
|
+
options.javaClass.getMethod("zIndex", Float::class.javaPrimitiveType)
|
|
192
|
+
.invoke(options, 1f)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
runCatching {
|
|
196
|
+
options.javaClass.getMethod("transparency", Float::class.javaPrimitiveType)
|
|
197
|
+
.invoke(options, (1.0 - opacityValue).toFloat())
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
heatmapOverlay = map.addTileOverlay(options)
|
|
201
|
+
needsRebuild = false
|
|
202
|
+
runCatching { heatmapOverlay?.clearTileCache() }
|
|
203
|
+
applyOverlayVisibility()
|
|
204
|
+
applyOverlayOpacity()
|
|
205
|
+
forceRefresh()
|
|
206
|
+
}
|
|
207
|
+
} catch (t: Throwable) {
|
|
208
|
+
Log.e("HeatMapView", "Failed to build heatmap", t)
|
|
82
209
|
}
|
|
83
210
|
}
|
|
84
211
|
}
|
|
85
212
|
|
|
213
|
+
private fun forceRefresh() {
|
|
214
|
+
runCatching { aMap?.moveCamera(CameraUpdateFactory.zoomBy(0f)) }
|
|
215
|
+
}
|
|
216
|
+
|
|
86
217
|
/**
|
|
87
218
|
* 移除热力图
|
|
88
219
|
*/
|
|
89
220
|
fun removeHeatMap() {
|
|
221
|
+
updateToken += 1
|
|
222
|
+
removeCallbacks(applyUpdateRunnable)
|
|
90
223
|
heatmapOverlay?.remove()
|
|
91
224
|
heatmapOverlay = null
|
|
92
225
|
dataList.clear()
|
|
226
|
+
needsRebuild = true
|
|
93
227
|
}
|
|
94
228
|
|
|
95
229
|
override fun onDetachedFromWindow() {
|
|
@@ -99,6 +233,7 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
|
|
|
99
233
|
if (parent == null) {
|
|
100
234
|
removeHeatMap()
|
|
101
235
|
aMap = null
|
|
236
|
+
runCatching { executor.shutdownNow() }
|
|
102
237
|
}
|
|
103
238
|
}
|
|
104
239
|
}
|
|
@@ -14,6 +14,10 @@ class HeatMapViewModule : Module() {
|
|
|
14
14
|
Prop<List<Map<String, Any>>>("data") { view: HeatMapView, data ->
|
|
15
15
|
view.setData(data)
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
Prop<Boolean>("visible") { view: HeatMapView, visible ->
|
|
19
|
+
view.setVisible(visible)
|
|
20
|
+
}
|
|
17
21
|
|
|
18
22
|
Prop<Int>("radius") { view: HeatMapView, radius ->
|
|
19
23
|
view.setRadius(radius)
|
|
@@ -22,6 +26,14 @@ class HeatMapViewModule : Module() {
|
|
|
22
26
|
Prop<Double>("opacity") { view: HeatMapView, opacity ->
|
|
23
27
|
view.setOpacity(opacity)
|
|
24
28
|
}
|
|
29
|
+
|
|
30
|
+
Prop<Map<String, Any>?>("gradient") { view: HeatMapView, gradient ->
|
|
31
|
+
// view.setGradient(gradient) // TODO: Implement gradient on Android
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Prop<Boolean>("allowRetinaAdapting") { view: HeatMapView, allow ->
|
|
35
|
+
// iOS only, ignore on Android
|
|
36
|
+
}
|
|
25
37
|
}
|
|
26
38
|
}
|
|
27
|
-
}
|
|
39
|
+
}
|
|
@@ -1,24 +1,47 @@
|
|
|
1
1
|
package expo.modules.gaodemap.overlays
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.BitmapFactory
|
|
6
|
+
import android.os.Handler
|
|
7
|
+
import android.os.Looper
|
|
8
|
+
import androidx.core.graphics.scale
|
|
4
9
|
|
|
5
10
|
import com.amap.api.maps.AMap
|
|
11
|
+
import com.amap.api.maps.model.BitmapDescriptor
|
|
6
12
|
import com.amap.api.maps.model.BitmapDescriptorFactory
|
|
7
13
|
import com.amap.api.maps.model.LatLng
|
|
8
14
|
import com.amap.api.maps.model.MultiPointItem
|
|
9
15
|
import com.amap.api.maps.model.MultiPointOverlay
|
|
10
16
|
import com.amap.api.maps.model.MultiPointOverlayOptions
|
|
17
|
+
import expo.modules.gaodemap.companion.BitmapDescriptorCache
|
|
18
|
+
import expo.modules.gaodemap.companion.IconBitmapCache
|
|
11
19
|
import expo.modules.kotlin.AppContext
|
|
20
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
12
21
|
|
|
13
22
|
import expo.modules.kotlin.views.ExpoView
|
|
23
|
+
import java.io.InputStream
|
|
24
|
+
import java.net.HttpURLConnection
|
|
25
|
+
import java.net.URL
|
|
26
|
+
import kotlin.concurrent.thread
|
|
14
27
|
|
|
15
28
|
class MultiPointView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
private val onMultiPointPress by EventDispatcher()
|
|
18
31
|
private var multiPointOverlay: MultiPointOverlay? = null
|
|
19
32
|
private var aMap: AMap? = null
|
|
20
33
|
private var points: MutableList<MultiPointItem> = mutableListOf()
|
|
21
34
|
|
|
35
|
+
private var pendingIconUri: String? = null
|
|
36
|
+
private var currentIconDescriptor: BitmapDescriptor? = null
|
|
37
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
38
|
+
|
|
39
|
+
private var anchorX: Float = 0.5f
|
|
40
|
+
private var anchorY: Float = 0.5f
|
|
41
|
+
|
|
42
|
+
private var iconWidth: Int? = null
|
|
43
|
+
private var iconHeight: Int? = null
|
|
44
|
+
|
|
22
45
|
/**
|
|
23
46
|
* 设置地图实例
|
|
24
47
|
*/
|
|
@@ -35,7 +58,7 @@ class MultiPointView(context: Context, appContext: AppContext) : ExpoView(contex
|
|
|
35
58
|
pointsList.forEach { point ->
|
|
36
59
|
val lat = (point["latitude"] as? Number)?.toDouble()
|
|
37
60
|
val lng = (point["longitude"] as? Number)?.toDouble()
|
|
38
|
-
val id = point["id"] as? String ?: ""
|
|
61
|
+
val id = point["customerId"] as? String ?: point["id"] as? String ?: ""
|
|
39
62
|
|
|
40
63
|
// 坐标验证
|
|
41
64
|
if (lat != null && lng != null && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
|
|
@@ -51,18 +74,32 @@ class MultiPointView(context: Context, appContext: AppContext) : ExpoView(contex
|
|
|
51
74
|
* 设置图标
|
|
52
75
|
*/
|
|
53
76
|
fun setIcon(iconUri: String?) {
|
|
77
|
+
pendingIconUri = iconUri
|
|
78
|
+
if (iconUri != null) {
|
|
79
|
+
loadAndSetIcon(iconUri)
|
|
80
|
+
} else {
|
|
81
|
+
currentIconDescriptor = null
|
|
82
|
+
createOrUpdateMultiPoint()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
54
85
|
|
|
55
|
-
|
|
56
|
-
|
|
86
|
+
fun setIconWidth(width: Int?) {
|
|
87
|
+
iconWidth = width
|
|
88
|
+
pendingIconUri?.let { loadAndSetIcon(it) }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fun setIconHeight(height: Int?) {
|
|
92
|
+
iconHeight = height
|
|
93
|
+
pendingIconUri?.let { loadAndSetIcon(it) }
|
|
57
94
|
}
|
|
58
95
|
|
|
59
96
|
/**
|
|
60
97
|
* 设置锚点
|
|
61
98
|
*/
|
|
62
99
|
fun setAnchor(anchor: Map<String, Float>) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
multiPointOverlay?.setAnchor(
|
|
100
|
+
anchorX = anchor["x"] ?: 0.5f
|
|
101
|
+
anchorY = anchor["y"] ?: 0.5f
|
|
102
|
+
multiPointOverlay?.setAnchor(anchorX, anchorY)
|
|
66
103
|
}
|
|
67
104
|
|
|
68
105
|
/**
|
|
@@ -76,18 +113,133 @@ class MultiPointView(context: Context, appContext: AppContext) : ExpoView(contex
|
|
|
76
113
|
|
|
77
114
|
// 创建海量点选项
|
|
78
115
|
val overlayOptions = MultiPointOverlayOptions()
|
|
79
|
-
|
|
80
|
-
|
|
116
|
+
// 使用加载的图标或默认图标
|
|
117
|
+
val icon = currentIconDescriptor ?: BitmapDescriptorFactory.defaultMarker()
|
|
118
|
+
overlayOptions.icon(icon)
|
|
119
|
+
overlayOptions.anchor(anchorX, anchorY)
|
|
81
120
|
|
|
82
121
|
// 创建海量点覆盖物
|
|
83
122
|
multiPointOverlay = map.addMultiPointOverlay(overlayOptions)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// 注意:MultiPointOverlay 在高德地图 Android SDK 中不直接支持点击事件
|
|
87
|
-
// 如果需要点击事件,需要使用 Marker 或其他方式实现
|
|
123
|
+
multiPointOverlay?.items = points
|
|
88
124
|
}
|
|
89
125
|
}
|
|
90
126
|
}
|
|
127
|
+
|
|
128
|
+
fun handleMultiPointClick(item: MultiPointItem): Boolean {
|
|
129
|
+
val index = points.indexOfFirst { it.customerId == item.customerId }
|
|
130
|
+
if (index != -1) {
|
|
131
|
+
onMultiPointPress(mapOf(
|
|
132
|
+
"id" to item.customerId, // 兼容旧版
|
|
133
|
+
"customerId" to item.customerId,
|
|
134
|
+
"index" to index, // 添加 index 字段
|
|
135
|
+
"latitude" to item.latLng.latitude,
|
|
136
|
+
"longitude" to item.latLng.longitude
|
|
137
|
+
))
|
|
138
|
+
return true
|
|
139
|
+
}
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private fun loadAndSetIcon(iconUri: String) {
|
|
144
|
+
val w = iconWidth ?: 0
|
|
145
|
+
val h = iconHeight ?: 0
|
|
146
|
+
val cacheKey = "multipoint|$iconUri|$w|$h"
|
|
147
|
+
|
|
148
|
+
// 尝试从缓存获取
|
|
149
|
+
BitmapDescriptorCache.get(cacheKey)?.let {
|
|
150
|
+
currentIconDescriptor = it
|
|
151
|
+
createOrUpdateMultiPoint()
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
when {
|
|
156
|
+
iconUri.startsWith("http") -> {
|
|
157
|
+
loadImageFromUrl(iconUri) { bitmap ->
|
|
158
|
+
processBitmap(bitmap, cacheKey)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
iconUri.startsWith("file://") -> {
|
|
162
|
+
val path = iconUri.substring(7)
|
|
163
|
+
val bitmap = BitmapFactory.decodeFile(path)
|
|
164
|
+
processBitmap(bitmap, cacheKey)
|
|
165
|
+
}
|
|
166
|
+
else -> {
|
|
167
|
+
val resId = context.resources.getIdentifier(iconUri, "drawable", context.packageName)
|
|
168
|
+
if (resId != 0) {
|
|
169
|
+
val bitmap = BitmapFactory.decodeResource(context.resources, resId)
|
|
170
|
+
processBitmap(bitmap, cacheKey)
|
|
171
|
+
} else {
|
|
172
|
+
currentIconDescriptor = null
|
|
173
|
+
mainHandler.post { createOrUpdateMultiPoint() }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private fun processBitmap(bitmap: Bitmap?, cacheKey: String) {
|
|
180
|
+
if (bitmap != null) {
|
|
181
|
+
var finalBitmap = bitmap
|
|
182
|
+
if (iconWidth != null || iconHeight != null) {
|
|
183
|
+
var w = iconWidth?.let { dpToPx(it) } ?: 0
|
|
184
|
+
var h = iconHeight?.let { dpToPx(it) } ?: 0
|
|
185
|
+
|
|
186
|
+
if (w > 0 && h == 0) {
|
|
187
|
+
h = (bitmap.height * (w.toFloat() / bitmap.width)).toInt()
|
|
188
|
+
} else if (h > 0 && w == 0) {
|
|
189
|
+
w = (bitmap.width * (h.toFloat() / bitmap.height)).toInt()
|
|
190
|
+
} else if (w == 0 && h == 0) {
|
|
191
|
+
w = bitmap.width
|
|
192
|
+
h = bitmap.height
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
finalBitmap = Bitmap.createScaledBitmap(bitmap, w, h, true)
|
|
197
|
+
} catch (e: Exception) {
|
|
198
|
+
e.printStackTrace()
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
val descriptor = BitmapDescriptorFactory.fromBitmap(finalBitmap)
|
|
203
|
+
BitmapDescriptorCache.putDescriptor(cacheKey, descriptor)
|
|
204
|
+
currentIconDescriptor = descriptor
|
|
205
|
+
} else {
|
|
206
|
+
currentIconDescriptor = null
|
|
207
|
+
}
|
|
208
|
+
mainHandler.post { createOrUpdateMultiPoint() }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private fun dpToPx(dp: Int): Int {
|
|
212
|
+
val density = context.resources.displayMetrics.density
|
|
213
|
+
return (dp * density).toInt()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private fun loadImageFromUrl(url: String, callback: (Bitmap?) -> Unit) {
|
|
217
|
+
thread {
|
|
218
|
+
var connection: HttpURLConnection? = null
|
|
219
|
+
var inputStream: InputStream? = null
|
|
220
|
+
try {
|
|
221
|
+
val urlConnection = URL(url)
|
|
222
|
+
connection = urlConnection.openConnection() as HttpURLConnection
|
|
223
|
+
connection.connectTimeout = 10000
|
|
224
|
+
connection.readTimeout = 10000
|
|
225
|
+
connection.doInput = true
|
|
226
|
+
connection.connect()
|
|
227
|
+
|
|
228
|
+
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
|
|
229
|
+
inputStream = connection.inputStream
|
|
230
|
+
val bitmap = BitmapFactory.decodeStream(inputStream)
|
|
231
|
+
callback(bitmap)
|
|
232
|
+
} else {
|
|
233
|
+
callback(null)
|
|
234
|
+
}
|
|
235
|
+
} catch (_: Exception) {
|
|
236
|
+
callback(null)
|
|
237
|
+
} finally {
|
|
238
|
+
inputStream?.close()
|
|
239
|
+
connection?.disconnect()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
91
243
|
|
|
92
244
|
/**
|
|
93
245
|
* 移除海量点
|
|
@@ -11,7 +11,7 @@ class MultiPointViewModule : Module() {
|
|
|
11
11
|
Name("MultiPointView")
|
|
12
12
|
|
|
13
13
|
View(MultiPointView::class) {
|
|
14
|
-
Events("
|
|
14
|
+
Events("onMultiPointPress")
|
|
15
15
|
|
|
16
16
|
Prop<List<Map<String, Any>>>("points") { view: MultiPointView, points ->
|
|
17
17
|
view.setPoints(points)
|
|
@@ -21,6 +21,14 @@ class MultiPointViewModule : Module() {
|
|
|
21
21
|
view.setIcon(icon)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
Prop<Int?>("iconWidth") { view: MultiPointView, width: Int? ->
|
|
25
|
+
view.setIconWidth(width)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Prop<Int?>("iconHeight") { view: MultiPointView, height: Int? ->
|
|
29
|
+
view.setIconHeight(height)
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
Prop<Map<String, Float>>("anchor"){ view: MultiPointView, anchor ->
|
|
25
33
|
view.setAnchor(anchor)
|
|
26
34
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package expo.modules.gaodemap.utils
|
|
2
|
+
|
|
3
|
+
import com.amap.api.maps.model.BitmapDescriptor
|
|
4
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
5
|
+
|
|
6
|
+
object BitmapDescriptorCache {
|
|
7
|
+
private val cache = ConcurrentHashMap<String, BitmapDescriptor>()
|
|
8
|
+
|
|
9
|
+
fun get(key: String): BitmapDescriptor? {
|
|
10
|
+
return cache[key]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fun put(key: String, descriptor: BitmapDescriptor) {
|
|
14
|
+
cache[key] = descriptor
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fun clear() {
|
|
18
|
+
cache.clear()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Cluster.d.ts","sourceRoot":"","sources":["../../../src/components/overlays/Cluster.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD;;;;;GAKG;AACH,iBAAS,OAAO,CAAC,KAAK,EAAE,YAAY,qBAEnC;;
|
|
1
|
+
{"version":3,"file":"Cluster.d.ts","sourceRoot":"","sources":["../../../src/components/overlays/Cluster.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD;;;;;GAKG;AACH,iBAAS,OAAO,CAAC,KAAK,EAAE,YAAY,qBAEnC;;AAqCD,wBAAkD"}
|
|
@@ -27,8 +27,12 @@ function arePropsEqual(prevProps, nextProps) {
|
|
|
27
27
|
if (prevProps.minClusterSize !== nextProps.minClusterSize) {
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
30
|
-
// 比较
|
|
31
|
-
if (prevProps.
|
|
30
|
+
// 比较 clusterBuckets
|
|
31
|
+
if (prevProps.clusterBuckets !== nextProps.clusterBuckets) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// 比较 onClusterPress 回调
|
|
35
|
+
if (prevProps.onClusterPress !== nextProps.onClusterPress) {
|
|
32
36
|
return false;
|
|
33
37
|
}
|
|
34
38
|
// 其他属性相同,不重新渲染
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Cluster.js","sourceRoot":"","sources":["../../../src/components/overlays/Cluster.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,MAAM,aAAa,GAAG,wBAAwB,CAAC,aAAa,CAAC,CAAC;AAE9D;;;;;GAKG;AACH,SAAS,OAAO,CAAC,KAAmB;IAClC,OAAO,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,EAAG,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,SAAS,aAAa,CAAC,SAAuB,EAAE,SAAuB;IACrE,uBAAuB;IACvB,IAAI,SAAS,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM,EAAE,CAAC;QAC1C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,YAAY;IACZ,IAAI,SAAS,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM,EAAE,CAAC;QAC1C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,oBAAoB;IACpB,IAAI,SAAS,CAAC,cAAc,KAAK,SAAS,CAAC,cAAc,EAAE,CAAC;QAC1D,OAAO,KAAK,CAAC;IACf,CAAC;IAED,
|
|
1
|
+
{"version":3,"file":"Cluster.js","sourceRoot":"","sources":["../../../src/components/overlays/Cluster.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,MAAM,aAAa,GAAG,wBAAwB,CAAC,aAAa,CAAC,CAAC;AAE9D;;;;;GAKG;AACH,SAAS,OAAO,CAAC,KAAmB;IAClC,OAAO,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,EAAG,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,SAAS,aAAa,CAAC,SAAuB,EAAE,SAAuB;IACrE,uBAAuB;IACvB,IAAI,SAAS,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM,EAAE,CAAC;QAC1C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,YAAY;IACZ,IAAI,SAAS,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM,EAAE,CAAC;QAC1C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,oBAAoB;IACpB,IAAI,SAAS,CAAC,cAAc,KAAK,SAAS,CAAC,cAAc,EAAE,CAAC;QAC1D,OAAO,KAAK,CAAC;IACf,CAAC;IAED,oBAAoB;IACpB,IAAI,SAAS,CAAC,cAAc,KAAK,SAAS,CAAC,cAAc,EAAE,CAAC;QAC1D,OAAO,KAAK,CAAC;IACf,CAAC;IAED,uBAAuB;IACvB,IAAI,SAAS,CAAC,cAAc,KAAK,SAAS,CAAC,cAAc,EAAE,CAAC;QAC1D,OAAO,KAAK,CAAC;IACf,CAAC;IAED,eAAe;IACf,OAAO,IAAI,CAAC;AACd,CAAC;AAED,WAAW;AACX,eAAe,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC","sourcesContent":["import { requireNativeViewManager } from 'expo-modules-core';\nimport * as React from 'react';\nimport type { ClusterProps } from '../../types';\n\nconst NativeCluster = requireNativeViewManager('ClusterView');\n\n/**\n * 高德地图点聚合组件\n *\n * @param props 点聚合组件的属性配置\n * @returns 渲染原生点聚合组件\n */\nfunction Cluster(props: ClusterProps) {\n return <NativeCluster {...props} />;\n}\n\n/**\n * 🔑 性能优化:浅比较关键属性\n * 只检查最常变化的属性,避免深度比较开销\n */\nfunction arePropsEqual(prevProps: ClusterProps, nextProps: ClusterProps): boolean {\n // 比较 points 数组引用(最常变化)\n if (prevProps.points !== nextProps.points) {\n return false;\n }\n \n // 比较 radius\n if (prevProps.radius !== nextProps.radius) {\n return false;\n }\n \n // 比较 minClusterSize\n if (prevProps.minClusterSize !== nextProps.minClusterSize) {\n return false;\n }\n \n // 比较 clusterBuckets\n if (prevProps.clusterBuckets !== nextProps.clusterBuckets) {\n return false;\n }\n \n // 比较 onClusterPress 回调\n if (prevProps.onClusterPress !== nextProps.onClusterPress) {\n return false;\n }\n \n // 其他属性相同,不重新渲染\n return true;\n}\n\n// 导出优化后的组件\nexport default React.memo(Cluster, arePropsEqual);\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HeatMap.d.ts","sourceRoot":"","sources":["../../../src/components/overlays/HeatMap.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"HeatMap.d.ts","sourceRoot":"","sources":["../../../src/components/overlays/HeatMap.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD;;;;;GAKG;AACH,iBAAS,OAAO,CAAC,KAAK,EAAE,YAAY,qBASnC;;AAiCD,wBAAkD"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { requireNativeViewManager } from 'expo-modules-core';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
+
import { StyleSheet } from 'react-native';
|
|
3
4
|
const NativeHeatMap = requireNativeViewManager('HeatMapView');
|
|
4
5
|
/**
|
|
5
6
|
* 高德地图热力图组件
|
|
@@ -8,8 +9,15 @@ const NativeHeatMap = requireNativeViewManager('HeatMapView');
|
|
|
8
9
|
* @returns 渲染高德地图原生热力图组件
|
|
9
10
|
*/
|
|
10
11
|
function HeatMap(props) {
|
|
11
|
-
return <NativeHeatMap {...props}
|
|
12
|
+
return (<NativeHeatMap {...props} collapsable={false} pointerEvents="none" style={styles.hidden}/>);
|
|
12
13
|
}
|
|
14
|
+
const styles = StyleSheet.create({
|
|
15
|
+
hidden: {
|
|
16
|
+
width: 0,
|
|
17
|
+
height: 0,
|
|
18
|
+
backgroundColor: 'transparent',
|
|
19
|
+
},
|
|
20
|
+
});
|
|
13
21
|
/**
|
|
14
22
|
* 🔑 性能优化:浅比较关键属性
|
|
15
23
|
*/
|
|
@@ -18,6 +26,9 @@ function arePropsEqual(prevProps, nextProps) {
|
|
|
18
26
|
if (prevProps.data !== nextProps.data) {
|
|
19
27
|
return false;
|
|
20
28
|
}
|
|
29
|
+
if (prevProps.visible !== nextProps.visible) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
21
32
|
// 比较样式属性
|
|
22
33
|
if (prevProps.radius !== nextProps.radius ||
|
|
23
34
|
prevProps.opacity !== nextProps.opacity) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HeatMap.js","sourceRoot":"","sources":["../../../src/components/overlays/HeatMap.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"HeatMap.js","sourceRoot":"","sources":["../../../src/components/overlays/HeatMap.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,MAAM,aAAa,GAAG,wBAAwB,CAAC,aAAa,CAAC,CAAC;AAG9D;;;;;GAKG;AACH,SAAS,OAAO,CAAC,KAAmB;IAClC,OAAO,CACL,CAAC,aAAa,CACZ,IAAI,KAAK,CAAC,CACV,WAAW,CAAC,CAAC,KAAK,CAAC,CACnB,aAAa,CAAC,MAAM,CACpB,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EACrB,CACH,CAAC;AACJ,CAAC;AAED,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IAC/B,MAAM,EAAE;QACN,KAAK,EAAE,CAAC;QACR,MAAM,EAAE,CAAC;QACT,eAAe,EAAE,aAAa;KAC/B;CACF,CAAC,CAAC;AAEH;;GAEG;AACH,SAAS,aAAa,CAAC,SAAuB,EAAE,SAAuB;IACrE,qBAAqB;IACrB,IAAI,SAAS,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;QACtC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,SAAS,CAAC,OAAO,KAAK,SAAS,CAAC,OAAO,EAAE,CAAC;QAC5C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,SAAS;IACT,IAAI,SAAS,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM;QACrC,SAAS,CAAC,OAAO,KAAK,SAAS,CAAC,OAAO,EAAE,CAAC;QAC5C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,WAAW;AACX,eAAe,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC","sourcesContent":["import { requireNativeViewManager } from 'expo-modules-core';\nimport * as React from 'react';\nimport { StyleSheet } from 'react-native';\nimport type { HeatMapProps } from '../../types';\n\nconst NativeHeatMap = requireNativeViewManager('HeatMapView');\n\n\n/**\n * 高德地图热力图组件\n *\n * @param props - 热力图配置属性,继承自NativeHeatMap组件的属性\n * @returns 渲染高德地图原生热力图组件\n */\nfunction HeatMap(props: HeatMapProps) {\n return (\n <NativeHeatMap\n {...props}\n collapsable={false}\n pointerEvents=\"none\"\n style={styles.hidden}\n />\n );\n}\n\nconst styles = StyleSheet.create({\n hidden: {\n width: 0,\n height: 0,\n backgroundColor: 'transparent',\n },\n});\n\n/**\n * 🔑 性能优化:浅比较关键属性\n */\nfunction arePropsEqual(prevProps: HeatMapProps, nextProps: HeatMapProps): boolean {\n // 比较 data 数组引用(最常变化)\n if (prevProps.data !== nextProps.data) {\n return false;\n }\n\n if (prevProps.visible !== nextProps.visible) {\n return false;\n }\n \n // 比较样式属性\n if (prevProps.radius !== nextProps.radius ||\n prevProps.opacity !== nextProps.opacity) {\n return false;\n }\n \n return true;\n}\n\n// 导出优化后的组件\nexport default React.memo(HeatMap, arePropsEqual);\n"]}
|
package/build/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export * from './types';
|
|
2
2
|
export { default as ExpoGaodeMapModule } from './ExpoGaodeMapModule';
|
|
3
3
|
export { default as MapView } from './ExpoGaodeMapView';
|
|
4
|
-
export type { MapViewRef } from './ExpoGaodeMapView';
|
|
5
4
|
export { useMap } from './components/MapContext';
|
|
6
5
|
export { MapUI } from './components/MapUI';
|
|
7
6
|
export { Marker, Polyline, Polygon, Circle, HeatMap, MultiPoint, Cluster, } from './components/overlays';
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,cAAc,SAAS,CAAC;AAExB,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAGrE,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,oBAAoB,CAAC;AACxD,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,cAAc,SAAS,CAAC;AAExB,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAGrE,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAG3C,OAAO,EACL,MAAM,EACN,QAAQ,EACR,OAAO,EACP,MAAM,EACN,OAAO,EACP,UAAU,EACV,OAAO,GACR,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,eAAe,EACf,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,YAAY,EACZ,WAAW,EACX,aAAa,EACb,SAAS,GACV,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGzD,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,SAAS,EACT,eAAe,EACf,WAAW,EACX,QAAQ,EACR,UAAU,EACV,MAAM,GACP,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAG1E,OAAO,EACL,eAAe,EACf,iBAAiB,EAAE,UAAU;AAC7B,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,eAAe,EACf,cAAc,GACf,MAAM,8BAA8B,CAAC;AACtC,YAAY,EACV,oBAAoB,EACpB,cAAc,GACf,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,OAAO,IAAI,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAEjF,YAAY,EACV,cAAc,EACd,gBAAgB,EAChB,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,wBAAwB,EACxB,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,uBAAuB,CAAC;AAM/B;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,wOAGjC,CAAA;AAGF,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAG/D,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC"}
|