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.
Files changed (89) hide show
  1. package/android/src/main/java/expo/modules/arcgis/AnalysisOverlayRef.kt +66 -0
  2. package/android/src/main/java/expo/modules/arcgis/AuthChallengeHandler.kt +9 -3
  3. package/android/src/main/java/expo/modules/arcgis/DynamicEntityLayerRef.kt +23 -0
  4. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisGeometryModule.kt +10 -2
  5. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisModule.kt +37 -4
  6. package/android/src/main/java/expo/modules/arcgis/GeocoderFunctions.kt +45 -3
  7. package/android/src/main/java/expo/modules/arcgis/GeometryEngineFunctions.kt +11 -0
  8. package/android/src/main/java/expo/modules/arcgis/GeoprocessingFunctions.kt +18 -2
  9. package/android/src/main/java/expo/modules/arcgis/GraphicsOverlayRef.kt +177 -25
  10. package/android/src/main/java/expo/modules/arcgis/LayerRef.kt +94 -1
  11. package/android/src/main/java/expo/modules/arcgis/MapRef.kt +61 -3
  12. package/android/src/main/java/expo/modules/arcgis/OfflineFunctions.kt +57 -2
  13. package/android/src/main/java/expo/modules/arcgis/QueryCodec.kt +25 -2
  14. package/android/src/main/java/expo/modules/arcgis/SceneRef.kt +29 -3
  15. package/android/src/main/java/expo/modules/arcgis/UtilityNetworkRef.kt +13 -0
  16. package/build/DynamicEntityLayer.d.ts +2 -1
  17. package/build/DynamicEntityLayer.d.ts.map +1 -1
  18. package/build/DynamicEntityLayer.js +2 -2
  19. package/build/DynamicEntityLayer.js.map +1 -1
  20. package/build/ExpoArcgis.types.d.ts +363 -3
  21. package/build/ExpoArcgis.types.d.ts.map +1 -1
  22. package/build/ExpoArcgis.types.js.map +1 -1
  23. package/build/ExpoArcgisGeometryModule.d.ts +6 -2
  24. package/build/ExpoArcgisGeometryModule.d.ts.map +1 -1
  25. package/build/ExpoArcgisGeometryModule.js.map +1 -1
  26. package/build/ExpoArcgisModule.d.ts +21 -3
  27. package/build/ExpoArcgisModule.d.ts.map +1 -1
  28. package/build/ExpoArcgisModule.js.map +1 -1
  29. package/build/FeatureLayer.d.ts +11 -0
  30. package/build/FeatureLayer.d.ts.map +1 -1
  31. package/build/Graphic.d.ts +7 -2
  32. package/build/Graphic.d.ts.map +1 -1
  33. package/build/Graphic.js +9 -4
  34. package/build/Graphic.js.map +1 -1
  35. package/build/LineOfSight.d.ts +17 -5
  36. package/build/LineOfSight.d.ts.map +1 -1
  37. package/build/LineOfSight.js +36 -13
  38. package/build/LineOfSight.js.map +1 -1
  39. package/build/Viewshed.d.ts +14 -4
  40. package/build/Viewshed.d.ts.map +1 -1
  41. package/build/Viewshed.js +27 -8
  42. package/build/Viewshed.js.map +1 -1
  43. package/build/auth.d.ts +3 -1
  44. package/build/auth.d.ts.map +1 -1
  45. package/build/auth.js +4 -2
  46. package/build/auth.js.map +1 -1
  47. package/build/geocoder.d.ts +12 -1
  48. package/build/geocoder.d.ts.map +1 -1
  49. package/build/geocoder.js +10 -1
  50. package/build/geocoder.js.map +1 -1
  51. package/build/geometryEngine.d.ts +18 -0
  52. package/build/geometryEngine.d.ts.map +1 -1
  53. package/build/geometryEngine.js +18 -0
  54. package/build/geometryEngine.js.map +1 -1
  55. package/build/index.d.ts +1 -1
  56. package/build/index.d.ts.map +1 -1
  57. package/build/index.js.map +1 -1
  58. package/build/offline.d.ts +8 -2
  59. package/build/offline.d.ts.map +1 -1
  60. package/build/offline.js +7 -1
  61. package/build/offline.js.map +1 -1
  62. package/ios/AnalysisOverlayRef.swift +74 -0
  63. package/ios/AuthChallengeHandler.swift +7 -2
  64. package/ios/DynamicEntityLayerRef.swift +22 -0
  65. package/ios/ExpoArcgisGeometryModule.swift +10 -2
  66. package/ios/ExpoArcgisModule.swift +37 -4
  67. package/ios/GeocoderFunctions.swift +63 -2
  68. package/ios/GeometryEngineFunctions.swift +17 -0
  69. package/ios/GeoprocessingFunctions.swift +14 -0
  70. package/ios/GraphicsOverlayRef.swift +103 -5
  71. package/ios/LayerRef.swift +89 -1
  72. package/ios/MapRef.swift +70 -4
  73. package/ios/OfflineFunctions.swift +71 -2
  74. package/ios/QueryCodec.swift +25 -0
  75. package/ios/SceneRef.swift +27 -4
  76. package/ios/UtilityNetworkRef.swift +13 -0
  77. package/package.json +1 -1
  78. package/src/DynamicEntityLayer.tsx +2 -2
  79. package/src/ExpoArcgis.types.ts +373 -5
  80. package/src/ExpoArcgisGeometryModule.ts +10 -1
  81. package/src/ExpoArcgisModule.ts +23 -2
  82. package/src/Graphic.tsx +9 -4
  83. package/src/LineOfSight.tsx +53 -17
  84. package/src/Viewshed.tsx +36 -11
  85. package/src/auth.ts +8 -2
  86. package/src/geocoder.ts +12 -1
  87. package/src/geometryEngine.ts +24 -0
  88. package/src/index.ts +1 -0
  89. 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
- /** Stores (or clears, when null) the login the handler uses to mint token credentials on demand. */
27
- fun setCredentials(username: String?, password: String?) {
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.create(challenge.requestUrl, user, pass)
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
- Function("setTokenCredential") { username: String, password: String ->
38
- AuthChallengeHandler.setCredentials(username, password)
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
- // Clears the stored login and all cached credentials (token + OAuth).
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
- ArcGISEnvironment.authenticationManager.arcGISCredentialStore.removeAll()
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
- locatorTask(params).suggest(searchText, buildSuggestParameters(params)).getOrThrow()
64
- .map { serializeSuggestResult(it) }
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 url = d["url"] as? String ?: return null
86
- GeoprocessingDataFile.Companion.createWithUrl(url)
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? = when (r["type"]) {
63
- "simple" -> (r["symbol"] as? Map<*, *>)?.let(::buildSymbol)?.let { SimpleRenderer(it) }
64
- "unique-value" -> {
65
- val fields = (r["fields"] as? List<*>)?.filterIsInstance<String>() ?: emptyList()
66
- val values = (r["uniqueValues"] as? List<*>)?.mapNotNull { uv ->
67
- (uv as? Map<*, *>)?.let {
68
- val symbol = (it["symbol"] as? Map<*, *>)?.let(::buildSymbol) ?: return@mapNotNull null
69
- UniqueValue(it["label"] as? String ?: "", "", symbol, rendererValues(it["values"]))
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
- } ?: emptyList()
72
- UniqueValueRenderer(
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
- "class-breaks" -> {
80
- val breaks = (r["classBreaks"] as? List<*>)?.mapNotNull { cb ->
81
- (cb as? Map<*, *>)?.let {
82
- val symbol = (it["symbol"] as? Map<*, *>)?.let(::buildSymbol) ?: return@mapNotNull null
83
- ClassBreak(it["label"] as? String ?: "", "", num(it["min"]), num(it["max"]), symbol)
84
- }
85
- } ?: emptyList()
86
- ClassBreaksRenderer(r["field"] as? String ?: "", breaks).apply {
87
- (r["defaultSymbol"] as? Map<*, *>)?.let(::buildSymbol)?.let { defaultSymbol = it }
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
- else -> null
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). */