expo-arcgis 0.1.3 → 0.1.5
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/src/main/java/expo/modules/arcgis/AnalysisOverlayRef.kt +66 -0
- package/android/src/main/java/expo/modules/arcgis/AuthChallengeHandler.kt +9 -3
- package/android/src/main/java/expo/modules/arcgis/DynamicEntityLayerRef.kt +23 -0
- package/android/src/main/java/expo/modules/arcgis/ExpoArcgisGeometryModule.kt +10 -2
- package/android/src/main/java/expo/modules/arcgis/ExpoArcgisModule.kt +37 -4
- package/android/src/main/java/expo/modules/arcgis/GeocoderFunctions.kt +45 -3
- package/android/src/main/java/expo/modules/arcgis/GeometryEngineFunctions.kt +11 -0
- package/android/src/main/java/expo/modules/arcgis/GeoprocessingFunctions.kt +18 -2
- package/android/src/main/java/expo/modules/arcgis/GraphicsOverlayRef.kt +177 -25
- package/android/src/main/java/expo/modules/arcgis/LayerRef.kt +94 -1
- package/android/src/main/java/expo/modules/arcgis/MapRef.kt +61 -3
- package/android/src/main/java/expo/modules/arcgis/OfflineFunctions.kt +57 -2
- package/android/src/main/java/expo/modules/arcgis/QueryCodec.kt +25 -2
- package/android/src/main/java/expo/modules/arcgis/SceneRef.kt +29 -3
- package/android/src/main/java/expo/modules/arcgis/UtilityNetworkRef.kt +13 -0
- package/build/DynamicEntityLayer.d.ts +2 -1
- package/build/DynamicEntityLayer.d.ts.map +1 -1
- package/build/DynamicEntityLayer.js +2 -2
- package/build/DynamicEntityLayer.js.map +1 -1
- package/build/ExpoArcgis.types.d.ts +363 -3
- package/build/ExpoArcgis.types.d.ts.map +1 -1
- package/build/ExpoArcgis.types.js.map +1 -1
- package/build/ExpoArcgisGeometryModule.d.ts +6 -2
- package/build/ExpoArcgisGeometryModule.d.ts.map +1 -1
- package/build/ExpoArcgisGeometryModule.js.map +1 -1
- package/build/ExpoArcgisModule.d.ts +21 -3
- package/build/ExpoArcgisModule.d.ts.map +1 -1
- package/build/ExpoArcgisModule.js.map +1 -1
- package/build/FeatureLayer.d.ts +11 -0
- package/build/FeatureLayer.d.ts.map +1 -1
- package/build/Graphic.d.ts +7 -2
- package/build/Graphic.d.ts.map +1 -1
- package/build/Graphic.js +9 -4
- package/build/Graphic.js.map +1 -1
- package/build/LineOfSight.d.ts +17 -5
- package/build/LineOfSight.d.ts.map +1 -1
- package/build/LineOfSight.js +36 -13
- package/build/LineOfSight.js.map +1 -1
- package/build/Viewshed.d.ts +14 -4
- package/build/Viewshed.d.ts.map +1 -1
- package/build/Viewshed.js +27 -8
- package/build/Viewshed.js.map +1 -1
- package/build/auth.d.ts +3 -1
- package/build/auth.d.ts.map +1 -1
- package/build/auth.js +4 -2
- package/build/auth.js.map +1 -1
- package/build/geocoder.d.ts +12 -1
- package/build/geocoder.d.ts.map +1 -1
- package/build/geocoder.js +10 -1
- package/build/geocoder.js.map +1 -1
- package/build/geometryEngine.d.ts +18 -0
- package/build/geometryEngine.d.ts.map +1 -1
- package/build/geometryEngine.js +18 -0
- package/build/geometryEngine.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/offline.d.ts +8 -2
- package/build/offline.d.ts.map +1 -1
- package/build/offline.js +7 -1
- package/build/offline.js.map +1 -1
- package/ios/AnalysisOverlayRef.swift +74 -0
- package/ios/AuthChallengeHandler.swift +7 -2
- package/ios/DynamicEntityLayerRef.swift +22 -0
- package/ios/ExpoArcgisGeometryModule.swift +10 -2
- package/ios/ExpoArcgisModule.swift +37 -4
- package/ios/GeocoderFunctions.swift +63 -2
- package/ios/GeometryEngineFunctions.swift +17 -0
- package/ios/GeoprocessingFunctions.swift +14 -0
- package/ios/GraphicsOverlayRef.swift +103 -5
- package/ios/LayerRef.swift +89 -1
- package/ios/MapRef.swift +70 -4
- package/ios/OfflineFunctions.swift +71 -2
- package/ios/QueryCodec.swift +25 -0
- package/ios/SceneRef.swift +27 -4
- package/ios/UtilityNetworkRef.swift +13 -0
- package/package.json +1 -1
- package/src/DynamicEntityLayer.tsx +2 -2
- package/src/ExpoArcgis.types.ts +373 -5
- package/src/ExpoArcgisGeometryModule.ts +10 -1
- package/src/ExpoArcgisModule.ts +23 -2
- package/src/Graphic.tsx +9 -4
- package/src/LineOfSight.tsx +53 -17
- package/src/Viewshed.tsx +36 -11
- package/src/auth.ts +8 -2
- package/src/geocoder.ts +12 -1
- package/src/geometryEngine.ts +24 -0
- package/src/index.ts +1 -0
- package/src/offline.ts +10 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
package expo.modules.arcgis
|
|
2
2
|
|
|
3
3
|
import com.arcgismaps.analysis.interactive.Analysis
|
|
4
|
+
import com.arcgismaps.analysis.interactive.ExploratoryGeoElementLineOfSight
|
|
5
|
+
import com.arcgismaps.analysis.interactive.ExploratoryGeoElementViewshed
|
|
4
6
|
import com.arcgismaps.analysis.interactive.ExploratoryLineOfSightTargetVisibility
|
|
5
7
|
import com.arcgismaps.analysis.interactive.ExploratoryLocationDistanceMeasurement
|
|
6
8
|
import com.arcgismaps.analysis.interactive.ExploratoryLocationLineOfSight
|
|
@@ -75,6 +77,42 @@ class ViewshedRef(appContext: AppContext, props: Map<String, Any?>) : AnalysisRe
|
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
/**
|
|
81
|
+
* SharedObject wrapping an [ExploratoryGeoElementViewshed] — a viewshed whose observer tracks a
|
|
82
|
+
* [GraphicRef]'s native [Graphic] (a [GeoElement]) as it moves.
|
|
83
|
+
*/
|
|
84
|
+
class GeoElementViewshedRef(appContext: AppContext, graphic: GraphicRef, props: Map<String, Any?>) : AnalysisRef(appContext) {
|
|
85
|
+
private val viewshed = ExploratoryGeoElementViewshed(
|
|
86
|
+
graphic.graphic,
|
|
87
|
+
numOr(props["horizontalAngle"], 45.0),
|
|
88
|
+
numOr(props["verticalAngle"], 45.0),
|
|
89
|
+
numOr(props["headingOffset"], 0.0),
|
|
90
|
+
numOr(props["pitchOffset"], 0.0),
|
|
91
|
+
(props["minDistance"] as? Number)?.toDouble(),
|
|
92
|
+
(props["maxDistance"] as? Number)?.toDouble(),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
override val analysis: Analysis get() = viewshed
|
|
96
|
+
|
|
97
|
+
init {
|
|
98
|
+
applyProps(props)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fun applyProps(changed: Map<String, Any?>) {
|
|
102
|
+
changed.forEach { (key, value) ->
|
|
103
|
+
when (key) {
|
|
104
|
+
"headingOffset" -> (value as? Number)?.toDouble()?.let { viewshed.headingOffset = it }
|
|
105
|
+
"pitchOffset" -> (value as? Number)?.toDouble()?.let { viewshed.pitchOffset = it }
|
|
106
|
+
"horizontalAngle" -> (value as? Number)?.toDouble()?.let { viewshed.horizontalAngle = it }
|
|
107
|
+
"verticalAngle" -> (value as? Number)?.toDouble()?.let { viewshed.verticalAngle = it }
|
|
108
|
+
"minDistance" -> viewshed.minDistance = (value as? Number)?.toDouble()
|
|
109
|
+
"maxDistance" -> viewshed.maxDistance = (value as? Number)?.toDouble()
|
|
110
|
+
"frustumOutlineVisible" -> (value as? Boolean)?.let { viewshed.frustumOutlineVisible = it }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
78
116
|
/** SharedObject wrapping an [ExploratoryLocationLineOfSight] — streams target visibility to JS. */
|
|
79
117
|
class LineOfSightRef(appContext: AppContext, props: Map<String, Any?>) : AnalysisRef(appContext) {
|
|
80
118
|
private val lineOfSight = ExploratoryLocationLineOfSight(
|
|
@@ -151,6 +189,34 @@ class DistanceMeasurementRef(appContext: AppContext, props: Map<String, Any?>) :
|
|
|
151
189
|
}
|
|
152
190
|
}
|
|
153
191
|
|
|
192
|
+
/**
|
|
193
|
+
* SharedObject wrapping an [ExploratoryGeoElementLineOfSight] — a line of sight whose observer
|
|
194
|
+
* and target both track a [GraphicRef]'s native [Graphic] (a [GeoElement]) as it moves.
|
|
195
|
+
* Streams the target's visibility back to JS via `onTargetVisibilityChange`.
|
|
196
|
+
*/
|
|
197
|
+
class GeoElementLineOfSightRef(appContext: AppContext, observer: GraphicRef, target: GraphicRef) : AnalysisRef(appContext) {
|
|
198
|
+
private val lineOfSight = ExploratoryGeoElementLineOfSight(
|
|
199
|
+
observer.graphic,
|
|
200
|
+
target.graphic,
|
|
201
|
+
)
|
|
202
|
+
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
203
|
+
|
|
204
|
+
override val analysis: Analysis get() = lineOfSight
|
|
205
|
+
|
|
206
|
+
init {
|
|
207
|
+
scope.launch {
|
|
208
|
+
lineOfSight.targetVisibility.collect { visibility ->
|
|
209
|
+
emit("onTargetVisibilityChange", mapOf("visibility" to visibilityString(visibility)))
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
override fun deallocate() {
|
|
215
|
+
scope.cancel()
|
|
216
|
+
super.deallocate()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
154
220
|
/** Decodes a JS point dict into a [Point] (returns null if the value is not a point geometry). */
|
|
155
221
|
internal fun analysisPoint(value: Any?): Point? =
|
|
156
222
|
(value as? Map<*, *>)?.let { geometryFromDict(it) } as? Point
|
|
@@ -22,11 +22,17 @@ import kotlinx.coroutines.launch
|
|
|
22
22
|
object AuthChallengeHandler : ArcGISAuthenticationChallengeHandler {
|
|
23
23
|
private var username: String? = null
|
|
24
24
|
private var password: String? = null
|
|
25
|
+
private var tokenExpirationMinutes: Int? = null
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Stores (or clears, when null) the login the handler uses to mint token credentials on demand.
|
|
29
|
+
* [tokenExpirationMinutes] is forwarded to [TokenCredential.createWithChallenge]; pass null to
|
|
30
|
+
* use the server's default expiry.
|
|
31
|
+
*/
|
|
32
|
+
fun setCredentials(username: String?, password: String?, tokenExpirationMinutes: Int? = null) {
|
|
28
33
|
this.username = username
|
|
29
34
|
this.password = password
|
|
35
|
+
this.tokenExpirationMinutes = tokenExpirationMinutes
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
override suspend fun handleArcGISAuthenticationChallenge(
|
|
@@ -34,7 +40,7 @@ object AuthChallengeHandler : ArcGISAuthenticationChallengeHandler {
|
|
|
34
40
|
): ArcGISAuthenticationChallengeResponse {
|
|
35
41
|
val user = username ?: return ArcGISAuthenticationChallengeResponse.ContinueAndFail
|
|
36
42
|
val pass = password ?: return ArcGISAuthenticationChallengeResponse.ContinueAndFail
|
|
37
|
-
return TokenCredential.
|
|
43
|
+
return TokenCredential.createWithChallenge(challenge, user, pass, tokenExpirationMinutes)
|
|
38
44
|
.map { ArcGISAuthenticationChallengeResponse.ContinueWithCredential(it) }
|
|
39
45
|
.getOrElse { ArcGISAuthenticationChallengeResponse.ContinueAndFail }
|
|
40
46
|
}
|
|
@@ -60,6 +60,24 @@ class DynamicEntityLayerRef(appContext: AppContext, props: Map<String, Any?>) :
|
|
|
60
60
|
flow.tryEmit(CustomDynamicEntityDataSource.FeedEvent.NewObservation(geom, attributes))
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Returns the observation history for the entity with the given track id, newest first,
|
|
65
|
+
* capped at [max] entries (default 100). Each observation carries its own `attributes` and
|
|
66
|
+
* `geometry` snapshot at the time it was received.
|
|
67
|
+
*/
|
|
68
|
+
suspend fun queryObservations(entityId: String, max: Int = 100): List<Map<String, Any?>> {
|
|
69
|
+
val result = dataSource.queryDynamicEntities(listOf(entityId)).getOrThrow()
|
|
70
|
+
val entity = result.toList().firstOrNull()
|
|
71
|
+
?: return emptyList()
|
|
72
|
+
return entity.getObservations(max).map { obs ->
|
|
73
|
+
val entry = mutableMapOf<String, Any?>(
|
|
74
|
+
"attributes" to obs.attributes.toMap(),
|
|
75
|
+
)
|
|
76
|
+
obs.geometry?.let { entry["geometry"] = dictFromGeometry(it) }
|
|
77
|
+
entry
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
63
81
|
/** Returns the data source's currently-tracked dynamic entities (attributes + geometry). */
|
|
64
82
|
suspend fun queryDynamicEntities(): Map<String, Any?> {
|
|
65
83
|
val result = dataSource.queryDynamicEntities(DynamicEntityQueryParameters()).getOrThrow()
|
|
@@ -93,6 +111,11 @@ class DynamicEntityLayerRef(appContext: AppContext, props: Map<String, Any?>) :
|
|
|
93
111
|
}
|
|
94
112
|
(dataSource as? ArcGISStreamService)?.filter = filter
|
|
95
113
|
}
|
|
114
|
+
(changed["purgeOptions"] as? Map<*, *>)?.let { purgeDict ->
|
|
115
|
+
val opts = dataSource.purgeOptions
|
|
116
|
+
(purgeDict["maximumObservations"] as? Number)?.let { opts.maximumObservations = it.toLong() }
|
|
117
|
+
(purgeDict["maximumDuration"] as? Number)?.let { opts.maximumDuration = it.toDouble() }
|
|
118
|
+
}
|
|
96
119
|
}
|
|
97
120
|
|
|
98
121
|
override fun deallocate() {
|
|
@@ -61,6 +61,9 @@ class ExpoArcgisGeometryModule : Module() {
|
|
|
61
61
|
Function("geScale", ::geScale)
|
|
62
62
|
Function("geEllipseGeodesic", ::geEllipseGeodesic)
|
|
63
63
|
Function("geSectorGeodesic", ::geSectorGeodesic)
|
|
64
|
+
Function("geWithZ", ::geWithZ)
|
|
65
|
+
Function("geWithM", ::geWithM)
|
|
66
|
+
Function("geWithZAndM", ::geWithZAndM)
|
|
64
67
|
|
|
65
68
|
// CoordinateFormatter — point <-> notation strings, exposed as the JS `coordinateFormatter` namespace.
|
|
66
69
|
Function("cfToLatLong", ::cfToLatLong)
|
|
@@ -82,6 +85,11 @@ class ExpoArcgisGeometryModule : Module() {
|
|
|
82
85
|
AsyncFunction("suggest") Coroutine { searchText: String, params: Map<String, Any?> ->
|
|
83
86
|
suggest(searchText, params)
|
|
84
87
|
}
|
|
88
|
+
// FLAG: ExpoArcgisGeometry is approaching 64 KB; if the module's definition() grows further,
|
|
89
|
+
// relocate geocodeSuggestion (and the other geocoder AsyncFunctions) to ExpoArcgisExtrasModule.
|
|
90
|
+
AsyncFunction("geocodeSuggestion") Coroutine { suggestionId: Int, params: Map<String, Any?> ->
|
|
91
|
+
geocodeSuggestion(suggestionId, params)
|
|
92
|
+
}
|
|
85
93
|
|
|
86
94
|
// Routing — solve a route between stops, exposed as the JS `router` namespace.
|
|
87
95
|
AsyncFunction("solveRoute") Coroutine { stops: List<Map<String, Any?>>, params: Map<String, Any?> ->
|
|
@@ -174,8 +182,8 @@ class ExpoArcgisGeometryModule : Module() {
|
|
|
174
182
|
}
|
|
175
183
|
|
|
176
184
|
// Offline — take maps/data offline, exposed as the JS `offline` namespace.
|
|
177
|
-
AsyncFunction("generateOfflineMap") Coroutine { portalItemId: String, areaOfInterest: Map<String, Any?>, downloadName: String ->
|
|
178
|
-
generateOfflineMap(appContext, appContext.reactContext?.filesDir, portalItemId, areaOfInterest, downloadName)
|
|
185
|
+
AsyncFunction("generateOfflineMap") Coroutine { portalItemId: String, areaOfInterest: Map<String, Any?>, downloadName: String, overrides: Map<String, Any?>? ->
|
|
186
|
+
generateOfflineMap(appContext, appContext.reactContext?.filesDir, portalItemId, areaOfInterest, downloadName, overrides)
|
|
179
187
|
}
|
|
180
188
|
AsyncFunction("syncOfflineMap") Coroutine { mobileMapPackagePath: String ->
|
|
181
189
|
syncOfflineMap(appContext, mobileMapPackagePath)
|
|
@@ -4,6 +4,7 @@ import android.content.Context
|
|
|
4
4
|
import com.arcgismaps.ApiKey
|
|
5
5
|
import com.arcgismaps.ArcGISEnvironment
|
|
6
6
|
import com.arcgismaps.httpcore.authentication.OAuthApplicationCredential
|
|
7
|
+
import com.arcgismaps.httpcore.authentication.OAuthUserCredential
|
|
7
8
|
import com.arcgismaps.httpcore.authentication.TokenCredential
|
|
8
9
|
import expo.modules.kotlin.Promise
|
|
9
10
|
import expo.modules.kotlin.functions.Coroutine
|
|
@@ -34,14 +35,21 @@ class ExpoArcgisModule : Module() {
|
|
|
34
35
|
|
|
35
36
|
// Token auth for secured services (e.g. utility-network feature services) — store the login;
|
|
36
37
|
// the challenge handler mints a TokenCredential for the exact challenged resource on demand.
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
// `tokenExpirationMinutes` is optional; the server's default expiry is used when omitted.
|
|
39
|
+
Function("setTokenCredential") { username: String, password: String, tokenExpirationMinutes: Int? ->
|
|
40
|
+
AuthChallengeHandler.setCredentials(username, password, tokenExpirationMinutes)
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
//
|
|
43
|
+
// Revokes any OAuth user credentials on the server, then clears all cached credentials.
|
|
42
44
|
AsyncFunction("signOut") Coroutine { ->
|
|
45
|
+
val store = ArcGISEnvironment.authenticationManager.arcGISCredentialStore
|
|
46
|
+
for (credential in store.getCredentials()) {
|
|
47
|
+
if (credential is OAuthUserCredential) {
|
|
48
|
+
credential.revokeToken()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
43
51
|
AuthChallengeHandler.setCredentials(null, null)
|
|
44
|
-
|
|
52
|
+
store.removeAll()
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
// OAuth user sign-in (Android): JS opens the browser between these two steps.
|
|
@@ -247,6 +255,9 @@ class ExpoArcgisModule : Module() {
|
|
|
247
255
|
AsyncFunction("queryDynamicEntities") Coroutine { ref: DynamicEntityLayerRef ->
|
|
248
256
|
ref.queryDynamicEntities()
|
|
249
257
|
}
|
|
258
|
+
AsyncFunction("queryObservations") Coroutine { ref: DynamicEntityLayerRef, entityId: String, max: Int ->
|
|
259
|
+
ref.queryObservations(entityId, max)
|
|
260
|
+
}
|
|
250
261
|
Function("pushObservation") { ref: DynamicEntityLayerRef, attributes: Map<String, Any?>, geometry: Map<String, Any?> ->
|
|
251
262
|
ref.pushObservation(attributes, geometry)
|
|
252
263
|
}
|
|
@@ -292,6 +303,16 @@ class ExpoArcgisModule : Module() {
|
|
|
292
303
|
Function("applyProps") { ref: ViewshedRef, changed: Map<String, Any?> -> ref.applyProps(changed) }
|
|
293
304
|
}
|
|
294
305
|
|
|
306
|
+
// GeoElement-anchored viewshed — observer tracks a Graphic as it moves.
|
|
307
|
+
Class(GeoElementViewshedRef::class) {
|
|
308
|
+
Constructor { graphic: GraphicRef, props: Map<String, Any?> ->
|
|
309
|
+
GeoElementViewshedRef(appContext, graphic, props)
|
|
310
|
+
}
|
|
311
|
+
Function("applyProps") { ref: GeoElementViewshedRef, changed: Map<String, Any?> ->
|
|
312
|
+
ref.applyProps(changed)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
295
316
|
// Line of sight — emits onTargetVisibilityChange as the target's visibility changes.
|
|
296
317
|
Class(LineOfSightRef::class) {
|
|
297
318
|
Constructor { props: Map<String, Any?> -> LineOfSightRef(appContext, props) }
|
|
@@ -299,6 +320,15 @@ class ExpoArcgisModule : Module() {
|
|
|
299
320
|
Function("applyProps") { ref: LineOfSightRef, changed: Map<String, Any?> -> ref.applyProps(changed) }
|
|
300
321
|
}
|
|
301
322
|
|
|
323
|
+
// GeoElement-anchored line of sight — observer and target each track a Graphic as it moves.
|
|
324
|
+
// FLAG: added to the main module — integrator may need to relocate for the 64 KB budget.
|
|
325
|
+
Class(GeoElementLineOfSightRef::class) {
|
|
326
|
+
Constructor { observer: GraphicRef, target: GraphicRef ->
|
|
327
|
+
GeoElementLineOfSightRef(appContext, observer, target)
|
|
328
|
+
}
|
|
329
|
+
Events("onTargetVisibilityChange")
|
|
330
|
+
}
|
|
331
|
+
|
|
302
332
|
Class(DistanceMeasurementRef::class) {
|
|
303
333
|
Constructor { props: Map<String, Any?> -> DistanceMeasurementRef(appContext, props) }
|
|
304
334
|
Events("onMeasurementChange")
|
|
@@ -329,6 +359,9 @@ class ExpoArcgisModule : Module() {
|
|
|
329
359
|
AsyncFunction("associations") Coroutine { ref: UtilityNetworkRef, tableName: String, whereClause: String ->
|
|
330
360
|
ref.associations(tableName, whereClause)
|
|
331
361
|
}
|
|
362
|
+
Function("getTerminalConfigurations") { ref: UtilityNetworkRef ->
|
|
363
|
+
ref.getTerminalConfigurations()
|
|
364
|
+
}
|
|
332
365
|
AsyncFunction("getState") Coroutine { ref: UtilityNetworkRef -> ref.getState() }
|
|
333
366
|
Function("validateNetworkTopology") { ref: UtilityNetworkRef, extent: Map<String, Any?> ->
|
|
334
367
|
ref.validateNetworkTopology(extent)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package expo.modules.arcgis
|
|
2
2
|
|
|
3
3
|
import com.arcgismaps.geometry.Point
|
|
4
|
+
import com.arcgismaps.geometry.SpatialReference
|
|
4
5
|
import com.arcgismaps.tasks.geocode.GeocodeParameters
|
|
5
6
|
import com.arcgismaps.tasks.geocode.GeocodeResult
|
|
6
7
|
import com.arcgismaps.tasks.geocode.LocatorTask
|
|
@@ -12,8 +13,31 @@ import java.util.concurrent.ConcurrentHashMap
|
|
|
12
13
|
/**
|
|
13
14
|
* Free functions backing the JS `geocoder` namespace — forward / reverse geocoding via a
|
|
14
15
|
* [LocatorTask]. Registered as `AsyncFunction`s in `ExpoArcgisGeometryModule`.
|
|
16
|
+
*
|
|
17
|
+
* SuggestResult round-trip
|
|
18
|
+
* ─────────────────────────
|
|
19
|
+
* [suggest] attaches a `suggestionId` (Int) to each returned item and stashes the native
|
|
20
|
+
* [SuggestResult] (and its locator URL) in [SuggestRegistry]. The registry is REPLACED on each
|
|
21
|
+
* new [suggest] call (ids restart from 0) so memory never grows unboundedly.
|
|
22
|
+
* [geocodeSuggestion] looks up the held result and calls `LocatorTask.geocode(suggestResult)` —
|
|
23
|
+
* the SDK's precise overload that avoids a text re-search.
|
|
15
24
|
*/
|
|
16
25
|
|
|
26
|
+
/** Holds the native [SuggestResult]s from the most recent [suggest] call. */
|
|
27
|
+
private object SuggestRegistry {
|
|
28
|
+
@Volatile private var locatorUrl: String = ""
|
|
29
|
+
@Volatile private var results: Map<Int, SuggestResult> = emptyMap()
|
|
30
|
+
|
|
31
|
+
/** Replaces the registry with a fresh set of results (called at the start of each [suggest]). */
|
|
32
|
+
@Synchronized fun reset(url: String, items: List<SuggestResult>) {
|
|
33
|
+
locatorUrl = url
|
|
34
|
+
results = items.mapIndexed { id, r -> id to r }.toMap()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Synchronized fun lookup(id: Int): Pair<SuggestResult, String>? =
|
|
38
|
+
results[id]?.let { it to locatorUrl }
|
|
39
|
+
}
|
|
40
|
+
|
|
17
41
|
private const val WORLD_GEOCODER =
|
|
18
42
|
"https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer"
|
|
19
43
|
|
|
@@ -49,6 +73,8 @@ private fun buildGeocodeParameters(params: Map<String, Any?>): GeocodeParameters
|
|
|
49
73
|
(params["maxResults"] as? Number)?.toInt()?.let { maxResults = it }
|
|
50
74
|
(params["countryCode"] as? String)?.let { countryCode = it }
|
|
51
75
|
(params["categories"] as? List<*>)?.filterIsInstance<String>()?.let { categories.addAll(it) }
|
|
76
|
+
(params["resultAttributeNames"] as? List<*>)?.filterIsInstance<String>()?.let { resultAttributeNames.addAll(it) }
|
|
77
|
+
(params["outputSpatialReference"] as? Number)?.toInt()?.let { outputSpatialReference = SpatialReference(it) }
|
|
52
78
|
((params["preferredSearchLocation"] as? Map<*, *>)?.let { geometryFromDict(it) } as? Point)
|
|
53
79
|
?.let { preferredSearchLocation = it }
|
|
54
80
|
}
|
|
@@ -59,9 +85,25 @@ private fun buildReverseGeocodeParameters(params: Map<String, Any?>): ReverseGeo
|
|
|
59
85
|
(params["maxDistance"] as? Number)?.toDouble()?.let { maxDistance = it }
|
|
60
86
|
}
|
|
61
87
|
|
|
62
|
-
internal suspend fun suggest(searchText: String, params: Map<String, Any?>): List<Map<String, Any?>>
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
internal suspend fun suggest(searchText: String, params: Map<String, Any?>): List<Map<String, Any?>> {
|
|
89
|
+
val url = params["locatorUrl"] as? String ?: WORLD_GEOCODER
|
|
90
|
+
val results = locatorTask(params).suggest(searchText, buildSuggestParameters(params)).getOrThrow()
|
|
91
|
+
// Stash all results in the registry (replaces any prior batch; ids restart from 0).
|
|
92
|
+
SuggestRegistry.reset(url, results)
|
|
93
|
+
return results.mapIndexed { id, r ->
|
|
94
|
+
serializeSuggestResult(r) + mapOf("suggestionId" to id)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
internal suspend fun geocodeSuggestion(suggestionId: Int, params: Map<String, Any?>): List<Map<String, Any?>> {
|
|
99
|
+
val (suggestResult, storedUrl) = SuggestRegistry.lookup(suggestionId)
|
|
100
|
+
?: error("suggestionId $suggestionId not found — call suggest() first")
|
|
101
|
+
// Allow the caller to override the locator URL, but fall back to the one stored by suggest().
|
|
102
|
+
val url = params["locatorUrl"] as? String ?: storedUrl
|
|
103
|
+
return locators.getOrPut(url) { LocatorTask(url) }
|
|
104
|
+
.geocode(suggestResult).getOrThrow()
|
|
105
|
+
.map { serializeGeocodeResult(it) }
|
|
106
|
+
}
|
|
65
107
|
|
|
66
108
|
private fun buildSuggestParameters(params: Map<String, Any?>): SuggestParameters = SuggestParameters().apply {
|
|
67
109
|
(params["maxResults"] as? Number)?.toInt()?.let { maxResults = it }
|
|
@@ -259,6 +259,17 @@ internal fun geScale(
|
|
|
259
259
|
)
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
// region Z / M builders
|
|
263
|
+
|
|
264
|
+
internal fun geWithZ(g: Map<String, Any?>, z: Double): Map<String, Any?>? =
|
|
265
|
+
parseGeo(g)?.let { encode(GeometryEngine.createWithZOrNull(it, z)) }
|
|
266
|
+
|
|
267
|
+
internal fun geWithM(g: Map<String, Any?>, m: Double): Map<String, Any?>? =
|
|
268
|
+
parseGeo(g)?.let { encode(GeometryEngine.createWithMOrNull(it, m)) }
|
|
269
|
+
|
|
270
|
+
internal fun geWithZAndM(g: Map<String, Any?>, z: Double, m: Double): Map<String, Any?>? =
|
|
271
|
+
parseGeo(g)?.let { encode(GeometryEngine.createWithZAndMOrNull(it, z, m)) }
|
|
272
|
+
|
|
262
273
|
// region Geodesic construction
|
|
263
274
|
|
|
264
275
|
internal fun geEllipseGeodesic(params: Map<String, Any?>): Map<String, Any?>? {
|
|
@@ -6,6 +6,7 @@ import com.arcgismaps.tasks.geoprocessing.GeoprocessingTask
|
|
|
6
6
|
import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingBoolean
|
|
7
7
|
import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingDataFile
|
|
8
8
|
import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingDate
|
|
9
|
+
import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingRaster
|
|
9
10
|
import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingDouble
|
|
10
11
|
import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingFeatures
|
|
11
12
|
import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingLinearUnit
|
|
@@ -82,8 +83,16 @@ private fun buildGeoprocessingParameter(d: Map<*, *>): GeoprocessingParameter? =
|
|
|
82
83
|
GeoprocessingMultiValue(paramType, elements)
|
|
83
84
|
}
|
|
84
85
|
"dataFile" -> {
|
|
85
|
-
val
|
|
86
|
-
|
|
86
|
+
val filePath = d["filePath"] as? String
|
|
87
|
+
if (filePath != null) {
|
|
88
|
+
// Local-file path — use GeoprocessingDataFile.inputFilePath (a settable property).
|
|
89
|
+
val df = GeoprocessingDataFile.Companion.create()
|
|
90
|
+
df.inputFilePath = filePath
|
|
91
|
+
df
|
|
92
|
+
} else {
|
|
93
|
+
val url = d["url"] as? String ?: return null
|
|
94
|
+
GeoprocessingDataFile.Companion.createWithUrl(url)
|
|
95
|
+
}
|
|
87
96
|
}
|
|
88
97
|
else -> null
|
|
89
98
|
}
|
|
@@ -105,5 +114,12 @@ private suspend fun serializeGeoprocessingParameter(param: GeoprocessingParamete
|
|
|
105
114
|
if (param.canFetchOutputFeatures) param.fetchOutputFeatures().getOrThrow()
|
|
106
115
|
param.features?.map { serializeFeature(it) } ?: emptyList<Map<String, Any?>>()
|
|
107
116
|
}
|
|
117
|
+
// GeoprocessingRaster extends GeoprocessingDataFile — must come before any GeoprocessingDataFile branch.
|
|
118
|
+
is GeoprocessingRaster -> {
|
|
119
|
+
val result = mutableMapOf<String, Any?>("type" to "raster")
|
|
120
|
+
param.url?.let { result["url"] = it }
|
|
121
|
+
param.inputFilePath?.let { result["filePath"] = it }
|
|
122
|
+
result
|
|
123
|
+
}
|
|
108
124
|
else -> null
|
|
109
125
|
}
|
|
@@ -58,36 +58,188 @@ class GraphicsOverlayRef(appContext: AppContext) : SharedObject(appContext) {
|
|
|
58
58
|
/**
|
|
59
59
|
* Builds a [Renderer] (simple / unique-value / class-breaks) from a JS renderer dict.
|
|
60
60
|
* Shared by [GraphicsOverlayRef.setRenderer] and [FeatureLayerRef.applyProps].
|
|
61
|
+
*
|
|
62
|
+
* When `visualVariables` is present, round-trips through [Renderer.toJson] /
|
|
63
|
+
* [Renderer.Companion.fromJsonOrNull] so the native C++ engine applies data-driven
|
|
64
|
+
* size/color/rotation/opacity — typed Kotlin wrappers do not expose `VisualVariable` classes
|
|
65
|
+
* in SDK 300.0.0.
|
|
61
66
|
*/
|
|
62
|
-
internal fun buildRenderer(r: Map<*, *>): Renderer?
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
(
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
internal fun buildRenderer(r: Map<*, *>): Renderer? {
|
|
68
|
+
val typed: Renderer? = when (r["type"]) {
|
|
69
|
+
"simple" -> (r["symbol"] as? Map<*, *>)?.let(::buildSymbol)?.let { SimpleRenderer(it) }
|
|
70
|
+
"unique-value" -> {
|
|
71
|
+
val fields = (r["fields"] as? List<*>)?.filterIsInstance<String>() ?: emptyList()
|
|
72
|
+
val values = (r["uniqueValues"] as? List<*>)?.mapNotNull { uv ->
|
|
73
|
+
(uv as? Map<*, *>)?.let {
|
|
74
|
+
val symbol = (it["symbol"] as? Map<*, *>)?.let(::buildSymbol) ?: return@mapNotNull null
|
|
75
|
+
UniqueValue(it["label"] as? String ?: "", "", symbol, rendererValues(it["values"]))
|
|
76
|
+
}
|
|
77
|
+
} ?: emptyList()
|
|
78
|
+
UniqueValueRenderer(
|
|
79
|
+
fields,
|
|
80
|
+
values,
|
|
81
|
+
r["defaultLabel"] as? String ?: "",
|
|
82
|
+
(r["defaultSymbol"] as? Map<*, *>)?.let(::buildSymbol) ?: transparentMarker(),
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
"class-breaks" -> {
|
|
86
|
+
val breaks = (r["classBreaks"] as? List<*>)?.mapNotNull { cb ->
|
|
87
|
+
(cb as? Map<*, *>)?.let {
|
|
88
|
+
val symbol = (it["symbol"] as? Map<*, *>)?.let(::buildSymbol) ?: return@mapNotNull null
|
|
89
|
+
ClassBreak(it["label"] as? String ?: "", "", num(it["min"]), num(it["max"]), symbol)
|
|
90
|
+
}
|
|
91
|
+
} ?: emptyList()
|
|
92
|
+
ClassBreaksRenderer(r["field"] as? String ?: "", breaks).apply {
|
|
93
|
+
(r["defaultSymbol"] as? Map<*, *>)?.let(::buildSymbol)?.let { defaultSymbol = it }
|
|
70
94
|
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
fields,
|
|
74
|
-
values,
|
|
75
|
-
r["defaultLabel"] as? String ?: "",
|
|
76
|
-
(r["defaultSymbol"] as? Map<*, *>)?.let(::buildSymbol) ?: transparentMarker(),
|
|
77
|
-
)
|
|
95
|
+
}
|
|
96
|
+
else -> null
|
|
78
97
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
// If visualVariables are present, inject them into the renderer JSON and reconstruct.
|
|
99
|
+
val vvRaw = (r["visualVariables"] as? List<*>)?.mapNotNull { it as? Map<*, *> }
|
|
100
|
+
if (vvRaw.isNullOrEmpty() || typed == null) return typed
|
|
101
|
+
return applyVisualVariables(vvRaw, typed) ?: typed
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Builds the ArcGIS REST JSON representation of [vvRaw], injects it into [renderer]'s own JSON,
|
|
106
|
+
* and reconstructs the renderer so the native C++ engine applies data-driven symbology.
|
|
107
|
+
* Returns `null` if JSON manipulation fails; the caller falls back to [renderer].
|
|
108
|
+
*/
|
|
109
|
+
private fun applyVisualVariables(vvRaw: List<Map<*, *>>, renderer: Renderer): Renderer? {
|
|
110
|
+
val vvJson = vvRaw.mapNotNull(::buildVisualVariableJson)
|
|
111
|
+
if (vvJson.isEmpty()) return null
|
|
112
|
+
return try {
|
|
113
|
+
val rendererJson = renderer.toJson()
|
|
114
|
+
val rendererMap = jsonObjToMap(org.json.JSONObject(rendererJson))
|
|
115
|
+
rendererMap["visualVariables"] = vvJson
|
|
116
|
+
val modifiedJson = mapToJsonObject(rendererMap).toString()
|
|
117
|
+
Renderer.fromJsonOrNull(modifiedJson)
|
|
118
|
+
} catch (e: Exception) {
|
|
119
|
+
null
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Converts an [org.json.JSONObject] to a mutable [Map] recursively (for round-trip JSON mutation). */
|
|
124
|
+
private fun jsonObjToMap(obj: org.json.JSONObject): MutableMap<String, Any?> {
|
|
125
|
+
val map = mutableMapOf<String, Any?>()
|
|
126
|
+
for (key in obj.keys()) {
|
|
127
|
+
map[key] = when (val v = obj.get(key)) {
|
|
128
|
+
is org.json.JSONObject -> jsonObjToMap(v)
|
|
129
|
+
is org.json.JSONArray -> jsonArrToList(v)
|
|
130
|
+
org.json.JSONObject.NULL -> null
|
|
131
|
+
else -> v
|
|
88
132
|
}
|
|
89
133
|
}
|
|
90
|
-
|
|
134
|
+
return map
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private fun jsonArrToList(arr: org.json.JSONArray): List<Any?> =
|
|
138
|
+
(0 until arr.length()).map { i ->
|
|
139
|
+
when (val v = arr.get(i)) {
|
|
140
|
+
is org.json.JSONObject -> jsonObjToMap(v)
|
|
141
|
+
is org.json.JSONArray -> jsonArrToList(v)
|
|
142
|
+
org.json.JSONObject.NULL -> null
|
|
143
|
+
else -> v
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Recursively converts a [Map] to a [org.json.JSONObject]. */
|
|
148
|
+
private fun mapToJsonObject(map: Map<*, *>): org.json.JSONObject {
|
|
149
|
+
val obj = org.json.JSONObject()
|
|
150
|
+
for ((k, v) in map) {
|
|
151
|
+
val key = k?.toString() ?: continue
|
|
152
|
+
when (v) {
|
|
153
|
+
null -> obj.put(key, org.json.JSONObject.NULL)
|
|
154
|
+
is Map<*, *> -> obj.put(key, mapToJsonObject(v))
|
|
155
|
+
is List<*> -> obj.put(key, listToJsonArray(v))
|
|
156
|
+
else -> obj.put(key, v)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return obj
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Recursively converts a [List] to a [org.json.JSONArray]. */
|
|
163
|
+
private fun listToJsonArray(list: List<*>): org.json.JSONArray {
|
|
164
|
+
val arr = org.json.JSONArray()
|
|
165
|
+
for (v in list) {
|
|
166
|
+
when (v) {
|
|
167
|
+
null -> arr.put(org.json.JSONObject.NULL)
|
|
168
|
+
is Map<*, *> -> arr.put(mapToJsonObject(v))
|
|
169
|
+
is List<*> -> arr.put(listToJsonArray(v))
|
|
170
|
+
else -> arr.put(v)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return arr
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Converts one JS `VisualVariable` map to the ArcGIS REST JSON map understood by the native
|
|
178
|
+
* renderer engine. Hex color strings (`#RRGGBB`/`#RRGGBBAA`, alpha-last) are converted to
|
|
179
|
+
* `[R, G, B, A]` integer lists as required by the REST spec.
|
|
180
|
+
*/
|
|
181
|
+
private fun buildVisualVariableJson(vv: Map<*, *>): Map<String, Any?>? = when (vv["type"]) {
|
|
182
|
+
"size" -> buildMap {
|
|
183
|
+
put("type", "sizeInfo")
|
|
184
|
+
(vv["field"] as? String)?.let { put("field", it) }
|
|
185
|
+
(vv["valueExpression"] as? String)?.let { put("valueExpression", it) }
|
|
186
|
+
(vv["minDataValue"] as? Number)?.toDouble()?.let { put("minDataValue", it) }
|
|
187
|
+
(vv["maxDataValue"] as? Number)?.toDouble()?.let { put("maxDataValue", it) }
|
|
188
|
+
(vv["minSize"] as? Number)?.toDouble()?.let { put("minSize", it) }
|
|
189
|
+
(vv["maxSize"] as? Number)?.toDouble()?.let { put("maxSize", it) }
|
|
190
|
+
(vv["stops"] as? List<*>)?.mapNotNull { s ->
|
|
191
|
+
(s as? Map<*, *>)?.let {
|
|
192
|
+
val value = (it["value"] as? Number)?.toDouble() ?: return@mapNotNull null
|
|
193
|
+
val size = (it["size"] as? Number)?.toDouble() ?: return@mapNotNull null
|
|
194
|
+
mapOf("value" to value, "size" to size)
|
|
195
|
+
}
|
|
196
|
+
}?.takeIf { it.isNotEmpty() }?.let { put("stops", it) }
|
|
197
|
+
}
|
|
198
|
+
"color" -> buildMap {
|
|
199
|
+
put("type", "colorInfo")
|
|
200
|
+
(vv["field"] as? String)?.let { put("field", it) }
|
|
201
|
+
(vv["valueExpression"] as? String)?.let { put("valueExpression", it) }
|
|
202
|
+
(vv["stops"] as? List<*>)?.mapNotNull { s ->
|
|
203
|
+
(s as? Map<*, *>)?.let {
|
|
204
|
+
val value = (it["value"] as? Number)?.toDouble() ?: return@mapNotNull null
|
|
205
|
+
val colorArr = restColor(it["color"]) ?: return@mapNotNull null
|
|
206
|
+
mapOf("value" to value, "color" to colorArr)
|
|
207
|
+
}
|
|
208
|
+
}?.takeIf { it.isNotEmpty() }?.let { put("stops", it) }
|
|
209
|
+
}
|
|
210
|
+
"rotation" -> buildMap {
|
|
211
|
+
put("type", "rotationInfo")
|
|
212
|
+
(vv["field"] as? String)?.let { put("field", it) }
|
|
213
|
+
(vv["valueExpression"] as? String)?.let { put("valueExpression", it) }
|
|
214
|
+
(vv["rotationType"] as? String)?.let { put("rotationType", it) }
|
|
215
|
+
}
|
|
216
|
+
"opacity" -> buildMap {
|
|
217
|
+
put("type", "opacityInfo")
|
|
218
|
+
(vv["field"] as? String)?.let { put("field", it) }
|
|
219
|
+
(vv["valueExpression"] as? String)?.let { put("valueExpression", it) }
|
|
220
|
+
(vv["stops"] as? List<*>)?.mapNotNull { s ->
|
|
221
|
+
(s as? Map<*, *>)?.let {
|
|
222
|
+
val value = (it["value"] as? Number)?.toDouble() ?: return@mapNotNull null
|
|
223
|
+
val opacity = (it["opacity"] as? Number)?.toDouble() ?: return@mapNotNull null
|
|
224
|
+
mapOf("value" to value, "opacity" to opacity)
|
|
225
|
+
}
|
|
226
|
+
}?.takeIf { it.isNotEmpty() }?.let { put("stops", it) }
|
|
227
|
+
}
|
|
228
|
+
else -> null // skip unknown visual variable types gracefully
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Converts a hex color string (`#RRGGBB` / `#RRGGBBAA`, alpha-last) to an
|
|
233
|
+
* `[R, G, B, A]` integer list as required by the ArcGIS REST renderer JSON spec.
|
|
234
|
+
*/
|
|
235
|
+
private fun restColor(value: Any?): List<Int>? {
|
|
236
|
+
val hex = (value as? String)?.trim()?.removePrefix("#") ?: return null
|
|
237
|
+
val v = hex.toLongOrNull(16) ?: return null
|
|
238
|
+
return when (hex.length) {
|
|
239
|
+
6 -> listOf(((v shr 16) and 0xff).toInt(), ((v shr 8) and 0xff).toInt(), (v and 0xff).toInt(), 255)
|
|
240
|
+
8 -> listOf(((v shr 24) and 0xff).toInt(), ((v shr 16) and 0xff).toInt(), ((v shr 8) and 0xff).toInt(), (v and 0xff).toInt())
|
|
241
|
+
else -> null
|
|
242
|
+
}
|
|
91
243
|
}
|
|
92
244
|
|
|
93
245
|
/** Converts JS unique values to comparable scalars (whole numbers → Int, else Double/String). */
|