expo-arcgis 0.1.2 → 0.1.4

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 (93) 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 +53 -0
  4. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisExtrasModule.kt +15 -0
  5. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisGeometryModule.kt +12 -2
  6. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisModule.kt +44 -6
  7. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisSceneView.kt +20 -0
  8. package/android/src/main/java/expo/modules/arcgis/GeocoderFunctions.kt +45 -3
  9. package/android/src/main/java/expo/modules/arcgis/GeometryEngineFunctions.kt +79 -0
  10. package/android/src/main/java/expo/modules/arcgis/GeoprocessingFunctions.kt +41 -0
  11. package/android/src/main/java/expo/modules/arcgis/GraphicsOverlayRef.kt +184 -25
  12. package/android/src/main/java/expo/modules/arcgis/LayerRef.kt +94 -1
  13. package/android/src/main/java/expo/modules/arcgis/MapRef.kt +61 -3
  14. package/android/src/main/java/expo/modules/arcgis/OfflineFunctions.kt +76 -2
  15. package/android/src/main/java/expo/modules/arcgis/QueryCodec.kt +25 -2
  16. package/android/src/main/java/expo/modules/arcgis/SceneRef.kt +29 -3
  17. package/android/src/main/java/expo/modules/arcgis/UtilityNetworkRef.kt +13 -0
  18. package/build/DynamicEntityLayer.d.ts +6 -2
  19. package/build/DynamicEntityLayer.d.ts.map +1 -1
  20. package/build/DynamicEntityLayer.js +10 -3
  21. package/build/DynamicEntityLayer.js.map +1 -1
  22. package/build/ExpoArcgis.types.d.ts +495 -3
  23. package/build/ExpoArcgis.types.d.ts.map +1 -1
  24. package/build/ExpoArcgis.types.js.map +1 -1
  25. package/build/ExpoArcgisExtrasModule.d.ts +7 -1
  26. package/build/ExpoArcgisExtrasModule.d.ts.map +1 -1
  27. package/build/ExpoArcgisExtrasModule.js.map +1 -1
  28. package/build/ExpoArcgisGeometryModule.d.ts +8 -2
  29. package/build/ExpoArcgisGeometryModule.d.ts.map +1 -1
  30. package/build/ExpoArcgisGeometryModule.js.map +1 -1
  31. package/build/ExpoArcgisModule.d.ts +22 -3
  32. package/build/ExpoArcgisModule.d.ts.map +1 -1
  33. package/build/ExpoArcgisModule.js.map +1 -1
  34. package/build/FeatureLayer.d.ts +11 -0
  35. package/build/FeatureLayer.d.ts.map +1 -1
  36. package/build/LineOfSight.d.ts +17 -5
  37. package/build/LineOfSight.d.ts.map +1 -1
  38. package/build/LineOfSight.js +36 -13
  39. package/build/LineOfSight.js.map +1 -1
  40. package/build/Viewshed.d.ts +14 -4
  41. package/build/Viewshed.d.ts.map +1 -1
  42. package/build/Viewshed.js +27 -8
  43. package/build/Viewshed.js.map +1 -1
  44. package/build/auth.d.ts +17 -1
  45. package/build/auth.d.ts.map +1 -1
  46. package/build/auth.js +21 -2
  47. package/build/auth.js.map +1 -1
  48. package/build/geocoder.d.ts +12 -1
  49. package/build/geocoder.d.ts.map +1 -1
  50. package/build/geocoder.js +10 -1
  51. package/build/geocoder.js.map +1 -1
  52. package/build/geometryEngine.d.ts +31 -1
  53. package/build/geometryEngine.d.ts.map +1 -1
  54. package/build/geometryEngine.js +30 -0
  55. package/build/geometryEngine.js.map +1 -1
  56. package/build/index.d.ts +2 -2
  57. package/build/index.d.ts.map +1 -1
  58. package/build/index.js +1 -1
  59. package/build/index.js.map +1 -1
  60. package/build/offline.d.ts +14 -2
  61. package/build/offline.d.ts.map +1 -1
  62. package/build/offline.js +14 -1
  63. package/build/offline.js.map +1 -1
  64. package/ios/AnalysisOverlayRef.swift +74 -0
  65. package/ios/AuthChallengeHandler.swift +7 -2
  66. package/ios/DynamicEntityLayerRef.swift +68 -0
  67. package/ios/ExpoArcgisExtrasModule.swift +19 -0
  68. package/ios/ExpoArcgisGeometryModule.swift +12 -2
  69. package/ios/ExpoArcgisModule.swift +41 -4
  70. package/ios/ExpoArcgisSceneView.swift +24 -0
  71. package/ios/GeocoderFunctions.swift +63 -2
  72. package/ios/GeometryEngineFunctions.swift +100 -0
  73. package/ios/GeoprocessingFunctions.swift +34 -0
  74. package/ios/GraphicsOverlayRef.swift +107 -5
  75. package/ios/LayerRef.swift +89 -1
  76. package/ios/MapRef.swift +70 -4
  77. package/ios/OfflineFunctions.swift +85 -2
  78. package/ios/QueryCodec.swift +25 -0
  79. package/ios/SceneRef.swift +27 -4
  80. package/ios/UtilityNetworkRef.swift +13 -0
  81. package/package.json +1 -1
  82. package/src/DynamicEntityLayer.tsx +14 -3
  83. package/src/ExpoArcgis.types.ts +507 -5
  84. package/src/ExpoArcgisExtrasModule.ts +17 -0
  85. package/src/ExpoArcgisGeometryModule.ts +14 -1
  86. package/src/ExpoArcgisModule.ts +25 -2
  87. package/src/LineOfSight.tsx +53 -17
  88. package/src/Viewshed.tsx +36 -11
  89. package/src/auth.ts +31 -2
  90. package/src/geocoder.ts +12 -1
  91. package/src/geometryEngine.ts +42 -0
  92. package/src/index.ts +2 -0
  93. package/src/offline.ts +24 -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
  }
@@ -39,6 +39,18 @@ class DynamicEntityLayerRef(appContext: AppContext, props: Map<String, Any?>) :
39
39
  emit("onConnectionStatusChange", mapOf("status" to connectionStatusString(status)))
40
40
  }
41
41
  }
42
+ // Emit entity-received events (new/updated observation arrived for an entity).
43
+ scope.launch {
44
+ dataSource.dynamicEntityReceivedEvent.collect { info ->
45
+ emit("onDynamicEntityChange", dynamicEntityPayload("received", info.dynamicEntity))
46
+ }
47
+ }
48
+ // Emit entity-purged events (entity evicted by purge rules).
49
+ scope.launch {
50
+ dataSource.dynamicEntityPurgedEvent.collect { info ->
51
+ emit("onDynamicEntityChange", dynamicEntityPayload("purged", info.dynamicEntity))
52
+ }
53
+ }
42
54
  }
43
55
 
44
56
  /** Pushes an observation into a custom data source (no-op for a stream service). */
@@ -48,6 +60,24 @@ class DynamicEntityLayerRef(appContext: AppContext, props: Map<String, Any?>) :
48
60
  flow.tryEmit(CustomDynamicEntityDataSource.FeedEvent.NewObservation(geom, attributes))
49
61
  }
50
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
+
51
81
  /** Returns the data source's currently-tracked dynamic entities (attributes + geometry). */
52
82
  suspend fun queryDynamicEntities(): Map<String, Any?> {
53
83
  val result = dataSource.queryDynamicEntities(DynamicEntityQueryParameters()).getOrThrow()
@@ -81,6 +111,11 @@ class DynamicEntityLayerRef(appContext: AppContext, props: Map<String, Any?>) :
81
111
  }
82
112
  (dataSource as? ArcGISStreamService)?.filter = filter
83
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
+ }
84
119
  }
85
120
 
86
121
  override fun deallocate() {
@@ -97,6 +132,24 @@ fun connectionStatusString(status: ConnectionStatus): String = when (status) {
97
132
  is ConnectionStatus.Failed -> "failed"
98
133
  }
99
134
 
135
+ /**
136
+ * Builds a compact payload for the `onDynamicEntityChange` event.
137
+ * Only `received` and `purged` change types are emitted (observation-only updates are
138
+ * captured within `dynamicEntityReceivedEvent` which fires per-entity arrival, not per
139
+ * observation, so the event rate is bounded to entity lifecycle changes).
140
+ * Geometry is serialized only when present; attributes are passed as-is (the map returned
141
+ * by the SDK is a shallow snapshot of the current entity attributes).
142
+ */
143
+ private fun dynamicEntityPayload(changeType: String, entity: com.arcgismaps.realtime.DynamicEntity): Map<String, Any?> {
144
+ val payload = mutableMapOf<String, Any?>(
145
+ "changeType" to changeType,
146
+ "entityId" to entity.id,
147
+ "attributes" to entity.attributes.toMap(),
148
+ )
149
+ entity.geometry?.let { payload["geometry"] = dictFromGeometry(it) }
150
+ return payload
151
+ }
152
+
100
153
  /** Builds the real-time data source: a custom feed (push from JS) or a stream service. */
101
154
  private fun buildDataSource(
102
155
  props: Map<String, Any?>,
@@ -1,5 +1,7 @@
1
1
  package expo.modules.arcgis
2
2
 
3
+ import com.arcgismaps.ArcGISEnvironment
4
+ import com.arcgismaps.httpcore.authentication.TokenCredential
3
5
  import expo.modules.kotlin.functions.Coroutine
4
6
  import expo.modules.kotlin.modules.Module
5
7
  import expo.modules.kotlin.modules.ModuleDefinition
@@ -71,6 +73,19 @@ class ExpoArcgisExtrasModule : Module() {
71
73
  }
72
74
  }
73
75
 
76
+ // Per-service token credential — mint a token for a specific URL and add it to the store.
77
+ AsyncFunction("setServiceCredential") Coroutine { serviceUrl: String, username: String, password: String, tokenExpirationMinutes: Int? ->
78
+ val credential = TokenCredential.create(serviceUrl, username, password, tokenExpirationMinutes)
79
+ .getOrThrow()
80
+ ArcGISEnvironment.authenticationManager.arcGISCredentialStore.add(credential, serviceUrl)
81
+ .getOrThrow()
82
+ }
83
+
84
+ // Tile-cache size estimation — quick estimate before committing to a download.
85
+ AsyncFunction("estimateTileCacheSize") Coroutine { tileServiceUrl: String, areaOfInterest: Map<String, Any?>, options: Map<String, Any?>? ->
86
+ estimateTileCacheSize(tileServiceUrl, areaOfInterest, options)
87
+ }
88
+
74
89
  // Turn-by-turn navigation — solve a route and track device locations against it.
75
90
  AsyncFunction("createRouteTracker") Coroutine { stops: List<Map<String, Any?>>, params: Map<String, Any?> ->
76
91
  createRouteTracker(appContext, stops, params)
@@ -59,6 +59,11 @@ class ExpoArcgisGeometryModule : Module() {
59
59
  Function("geMove", ::geMove)
60
60
  Function("geRotate", ::geRotate)
61
61
  Function("geScale", ::geScale)
62
+ Function("geEllipseGeodesic", ::geEllipseGeodesic)
63
+ Function("geSectorGeodesic", ::geSectorGeodesic)
64
+ Function("geWithZ", ::geWithZ)
65
+ Function("geWithM", ::geWithM)
66
+ Function("geWithZAndM", ::geWithZAndM)
62
67
 
63
68
  // CoordinateFormatter — point <-> notation strings, exposed as the JS `coordinateFormatter` namespace.
64
69
  Function("cfToLatLong", ::cfToLatLong)
@@ -80,6 +85,11 @@ class ExpoArcgisGeometryModule : Module() {
80
85
  AsyncFunction("suggest") Coroutine { searchText: String, params: Map<String, Any?> ->
81
86
  suggest(searchText, params)
82
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
+ }
83
93
 
84
94
  // Routing — solve a route between stops, exposed as the JS `router` namespace.
85
95
  AsyncFunction("solveRoute") Coroutine { stops: List<Map<String, Any?>>, params: Map<String, Any?> ->
@@ -172,8 +182,8 @@ class ExpoArcgisGeometryModule : Module() {
172
182
  }
173
183
 
174
184
  // Offline — take maps/data offline, exposed as the JS `offline` namespace.
175
- AsyncFunction("generateOfflineMap") Coroutine { portalItemId: String, areaOfInterest: Map<String, Any?>, downloadName: String ->
176
- 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)
177
187
  }
178
188
  AsyncFunction("syncOfflineMap") Coroutine { mobileMapPackagePath: String ->
179
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.
@@ -234,18 +242,22 @@ class ExpoArcgisModule : Module() {
234
242
  Function("applyProps") { ref: OgcFeatureLayerRef, changed: Map<String, Any?> -> ref.applyProps(changed) }
235
243
  }
236
244
 
237
- // Real-time DynamicEntityLayer (stream service) — emits onConnectionStatusChange.
245
+ // Real-time DynamicEntityLayer (stream service) — emits onConnectionStatusChange +
246
+ // onDynamicEntityChange (received/purged entity events).
238
247
  Class(DynamicEntityLayerRef::class) {
239
248
  Constructor { props: Map<String, Any?> ->
240
249
  DynamicEntityLayerRef(appContext, props).also { it.applyProps(props) }
241
250
  }
242
- Events("onConnectionStatusChange")
251
+ Events("onConnectionStatusChange", "onDynamicEntityChange")
243
252
  Function("applyProps") { ref: DynamicEntityLayerRef, changed: Map<String, Any?> ->
244
253
  ref.applyProps(changed)
245
254
  }
246
255
  AsyncFunction("queryDynamicEntities") Coroutine { ref: DynamicEntityLayerRef ->
247
256
  ref.queryDynamicEntities()
248
257
  }
258
+ AsyncFunction("queryObservations") Coroutine { ref: DynamicEntityLayerRef, entityId: String, max: Int ->
259
+ ref.queryObservations(entityId, max)
260
+ }
249
261
  Function("pushObservation") { ref: DynamicEntityLayerRef, attributes: Map<String, Any?>, geometry: Map<String, Any?> ->
250
262
  ref.pushObservation(attributes, geometry)
251
263
  }
@@ -291,6 +303,16 @@ class ExpoArcgisModule : Module() {
291
303
  Function("applyProps") { ref: ViewshedRef, changed: Map<String, Any?> -> ref.applyProps(changed) }
292
304
  }
293
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
+
294
316
  // Line of sight — emits onTargetVisibilityChange as the target's visibility changes.
295
317
  Class(LineOfSightRef::class) {
296
318
  Constructor { props: Map<String, Any?> -> LineOfSightRef(appContext, props) }
@@ -298,6 +320,15 @@ class ExpoArcgisModule : Module() {
298
320
  Function("applyProps") { ref: LineOfSightRef, changed: Map<String, Any?> -> ref.applyProps(changed) }
299
321
  }
300
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
+
301
332
  Class(DistanceMeasurementRef::class) {
302
333
  Constructor { props: Map<String, Any?> -> DistanceMeasurementRef(appContext, props) }
303
334
  Events("onMeasurementChange")
@@ -328,6 +359,9 @@ class ExpoArcgisModule : Module() {
328
359
  AsyncFunction("associations") Coroutine { ref: UtilityNetworkRef, tableName: String, whereClause: String ->
329
360
  ref.associations(tableName, whereClause)
330
361
  }
362
+ Function("getTerminalConfigurations") { ref: UtilityNetworkRef ->
363
+ ref.getTerminalConfigurations()
364
+ }
331
365
  AsyncFunction("getState") Coroutine { ref: UtilityNetworkRef -> ref.getState() }
332
366
  Function("validateNetworkTopology") { ref: UtilityNetworkRef, extent: Map<String, Any?> ->
333
367
  ref.validateNetworkTopology(extent)
@@ -405,6 +439,10 @@ class ExpoArcgisModule : Module() {
405
439
  view.setCamera(camera)
406
440
  }
407
441
 
442
+ Prop("cameraController") { view: ExpoArcgisSceneView, value: Map<String, Any?>? ->
443
+ view.setCameraController(value)
444
+ }
445
+
408
446
  Prop("sunLighting") { view: ExpoArcgisSceneView, value: String? ->
409
447
  view.setSunLighting(value)
410
448
  }
@@ -9,7 +9,9 @@ import com.arcgismaps.geometry.Point
9
9
  import com.arcgismaps.geometry.SpatialReference
10
10
  import com.arcgismaps.mapping.view.AtmosphereEffect
11
11
  import com.arcgismaps.mapping.view.Camera
12
+ import com.arcgismaps.mapping.view.GlobeCameraController
12
13
  import com.arcgismaps.mapping.view.LightingMode
14
+ import com.arcgismaps.mapping.view.OrbitLocationCameraController
13
15
  import com.arcgismaps.mapping.view.SceneView
14
16
  import com.arcgismaps.mapping.view.ScreenCoordinate
15
17
  import expo.modules.kotlin.AppContext
@@ -147,6 +149,24 @@ class ExpoArcgisSceneView(context: Context, appContext: AppContext) : ExpoView(c
147
149
  scope.launch { sceneView.setViewpointCameraAnimated(camera, 0.5f) }
148
150
  }
149
151
 
152
+ /** Sets or clears the scene's camera controller (orbit/globe). `null` restores the SDK default. */
153
+ fun setCameraController(c: Map<String, Any?>?) {
154
+ sceneView.cameraController = when (c?.get("type") as? String) {
155
+ "orbitLocation" -> {
156
+ val target = c["target"] as? Map<*, *>
157
+ val x = (target?.get("x") as? Number)?.toDouble() ?: 0.0
158
+ val y = (target?.get("y") as? Number)?.toDouble() ?: 0.0
159
+ val z = (target?.get("z") as? Number)?.toDouble()
160
+ val point = if (z != null) Point(x, y, z, SpatialReference.wgs84())
161
+ else Point(x, y, SpatialReference.wgs84())
162
+ val distance = (c["distance"] as? Number)?.toDouble() ?: 1500.0
163
+ OrbitLocationCameraController(point, distance)
164
+ }
165
+ "globe" -> GlobeCameraController()
166
+ else -> GlobeCameraController() // null/absent → restore SDK default (GlobeCameraController)
167
+ }
168
+ }
169
+
150
170
  /** Sun lighting mode (shadows). */
151
171
  fun setSunLighting(s: String?) {
152
172
  sceneView.sunLighting = when (s) {
@@ -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 }
@@ -1,6 +1,8 @@
1
1
  package expo.modules.arcgis
2
2
 
3
3
  import com.arcgismaps.geometry.Envelope
4
+ import com.arcgismaps.geometry.GeodesicEllipseParameters
5
+ import com.arcgismaps.geometry.GeodesicSectorParameters
4
6
  import com.arcgismaps.geometry.Geometry
5
7
  import com.arcgismaps.geometry.GeometryEngine
6
8
  import com.arcgismaps.geometry.Multipart
@@ -256,3 +258,80 @@ internal fun geScale(
256
258
  else GeometryEngine.scale(geometry, factorX, factorY),
257
259
  )
258
260
  }
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
+
273
+ // region Geodesic construction
274
+
275
+ internal fun geEllipseGeodesic(params: Map<String, Any?>): Map<String, Any?>? {
276
+ val centerDict = params["center"] as? Map<String, Any?> ?: return null
277
+ val center = parsePoint(centerDict) ?: return null
278
+
279
+ val semiAxis1Length = (params["semiAxis1Length"] as? Number)?.toDouble() ?: 0.0
280
+ val semiAxis2Length = (params["semiAxis2Length"] as? Number)?.toDouble() ?: 0.0
281
+ val axisDirection = (params["axisDirection"] as? Number)?.toDouble() ?: 0.0
282
+ val angUnit = angularUnit(params["angularUnit"] as? String)
283
+ val linUnit = linearUnit(params["linearUnit"] as? String)
284
+ val maxSegLen = (params["maxSegmentLength"] as? Number)?.toDouble() ?: 0.0
285
+ val maxPtCount = (params["maxPointCount"] as? Number)?.toLong() ?: 10L
286
+ val geoType = params["geometryType"] as? String
287
+
288
+ val p: GeodesicEllipseParameters = when (geoType) {
289
+ "polyline" -> GeodesicEllipseParameters.Companion.createForPolyline()
290
+ "multipoint" -> GeodesicEllipseParameters.Companion.createForMultipoint()
291
+ else -> GeodesicEllipseParameters.Companion.createForPolygon()
292
+ }
293
+ p.center = center
294
+ p.semiAxis1Length = semiAxis1Length
295
+ p.semiAxis2Length = semiAxis2Length
296
+ p.axisDirection = axisDirection
297
+ p.angularUnit = angUnit
298
+ p.linearUnit = linUnit
299
+ p.maxSegmentLength = maxSegLen
300
+ p.maxPointCount = maxPtCount
301
+
302
+ return encode(GeometryEngine.ellipseGeodesicOrNull(p))
303
+ }
304
+
305
+ internal fun geSectorGeodesic(params: Map<String, Any?>): Map<String, Any?>? {
306
+ val centerDict = params["center"] as? Map<String, Any?> ?: return null
307
+ val center = parsePoint(centerDict) ?: return null
308
+
309
+ val semiAxis1Length = (params["semiAxis1Length"] as? Number)?.toDouble() ?: 0.0
310
+ val semiAxis2Length = (params["semiAxis2Length"] as? Number)?.toDouble() ?: 0.0
311
+ val axisDirection = (params["axisDirection"] as? Number)?.toDouble() ?: 0.0
312
+ val sectorAngle = (params["sectorAngle"] as? Number)?.toDouble() ?: 0.0
313
+ val startDirection = (params["startDirection"] as? Number)?.toDouble() ?: 0.0
314
+ val angUnit = angularUnit(params["angularUnit"] as? String)
315
+ val linUnit = linearUnit(params["linearUnit"] as? String)
316
+ val maxSegLen = (params["maxSegmentLength"] as? Number)?.toDouble() ?: 0.0
317
+ val maxPtCount = (params["maxPointCount"] as? Number)?.toLong() ?: 10L
318
+ val geoType = params["geometryType"] as? String
319
+
320
+ val p: GeodesicSectorParameters = when (geoType) {
321
+ "polyline" -> GeodesicSectorParameters.Companion.createForPolyline()
322
+ "multipoint" -> GeodesicSectorParameters.Companion.createForMultipoint()
323
+ else -> GeodesicSectorParameters.Companion.createForPolygon()
324
+ }
325
+ p.center = center
326
+ p.semiAxis1Length = semiAxis1Length
327
+ p.semiAxis2Length = semiAxis2Length
328
+ p.axisDirection = axisDirection
329
+ p.sectorAngle = sectorAngle
330
+ p.startDirection = startDirection
331
+ p.angularUnit = angUnit
332
+ p.linearUnit = linUnit
333
+ p.maxSegmentLength = maxSegLen
334
+ p.maxPointCount = maxPtCount
335
+
336
+ return encode(GeometryEngine.sectorGeodesicOrNull(p))
337
+ }
@@ -4,12 +4,16 @@ import com.arcgismaps.data.FeatureCollectionTable
4
4
  import com.arcgismaps.mapping.view.Graphic
5
5
  import com.arcgismaps.tasks.geoprocessing.GeoprocessingTask
6
6
  import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingBoolean
7
+ import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingDataFile
7
8
  import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingDate
9
+ import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingRaster
8
10
  import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingDouble
9
11
  import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingFeatures
10
12
  import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingLinearUnit
11
13
  import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingLong
14
+ import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingMultiValue
12
15
  import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingParameter
16
+ import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingParameterType
13
17
  import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingString
14
18
  import expo.modules.kotlin.AppContext
15
19
  import java.time.Instant
@@ -60,6 +64,36 @@ private fun buildGeoprocessingParameter(d: Map<*, *>): GeoprocessingParameter? =
60
64
  ?.map { Graphic().apply { geometry = it } } ?: emptyList()
61
65
  GeoprocessingFeatures(FeatureCollectionTable(graphics, emptyList()))
62
66
  }
67
+ "multiValue" -> {
68
+ // JS numbers → GeoprocessingDouble, JS strings → GeoprocessingString.
69
+ val rawValues = d["values"] as? List<*> ?: emptyList<Any>()
70
+ val elements: List<GeoprocessingParameter> = rawValues.mapNotNull { v ->
71
+ when (v) {
72
+ is Number -> GeoprocessingDouble(v.toDouble())
73
+ is String -> GeoprocessingString(v)
74
+ else -> null
75
+ }
76
+ }
77
+ // Use the element type of the first item to determine the multiValue's parameterType;
78
+ // fall back to GeoprocessingParameterType.GeoprocessingString when the list is empty.
79
+ val paramType = if (elements.firstOrNull() is GeoprocessingDouble)
80
+ GeoprocessingParameterType.GeoprocessingDouble
81
+ else
82
+ GeoprocessingParameterType.GeoprocessingString
83
+ GeoprocessingMultiValue(paramType, elements)
84
+ }
85
+ "dataFile" -> {
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
+ }
96
+ }
63
97
  else -> null
64
98
  }
65
99
 
@@ -80,5 +114,12 @@ private suspend fun serializeGeoprocessingParameter(param: GeoprocessingParamete
80
114
  if (param.canFetchOutputFeatures) param.fetchOutputFeatures().getOrThrow()
81
115
  param.features?.map { serializeFeature(it) } ?: emptyList<Map<String, Any?>>()
82
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
+ }
83
124
  else -> null
84
125
  }