expo-arcgis 0.1.1 → 0.1.3

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/README.md +3 -3
  2. package/android/src/main/java/expo/modules/arcgis/DynamicEntityLayerRef.kt +30 -0
  3. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisExtrasModule.kt +102 -0
  4. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisGeometryModule.kt +40 -0
  5. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisMapView.kt +16 -0
  6. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisModule.kt +21 -34
  7. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisSceneView.kt +36 -0
  8. package/android/src/main/java/expo/modules/arcgis/GeocoderFunctions.kt +13 -3
  9. package/android/src/main/java/expo/modules/arcgis/GeometryEngineFunctions.kt +103 -0
  10. package/android/src/main/java/expo/modules/arcgis/GeoprocessingFunctions.kt +25 -0
  11. package/android/src/main/java/expo/modules/arcgis/GraphicsOverlayRef.kt +32 -0
  12. package/android/src/main/java/expo/modules/arcgis/LayerRef.kt +189 -7
  13. package/android/src/main/java/expo/modules/arcgis/OfflineFunctions.kt +19 -0
  14. package/android/src/main/java/expo/modules/arcgis/QueryCodec.kt +21 -0
  15. package/android/src/main/java/expo/modules/arcgis/RouterFunctions.kt +61 -0
  16. package/android/src/main/java/expo/modules/arcgis/UtilityNetworkRef.kt +24 -0
  17. package/build/DynamicEntityLayer.d.ts +4 -1
  18. package/build/DynamicEntityLayer.d.ts.map +1 -1
  19. package/build/DynamicEntityLayer.js +8 -1
  20. package/build/DynamicEntityLayer.js.map +1 -1
  21. package/build/ExpoArcgis.types.d.ts +364 -8
  22. package/build/ExpoArcgis.types.d.ts.map +1 -1
  23. package/build/ExpoArcgis.types.js.map +1 -1
  24. package/build/ExpoArcgisExtrasModule.d.ts +22 -0
  25. package/build/ExpoArcgisExtrasModule.d.ts.map +1 -0
  26. package/build/ExpoArcgisExtrasModule.js +3 -0
  27. package/build/ExpoArcgisExtrasModule.js.map +1 -0
  28. package/build/ExpoArcgisGeometryModule.d.ts +13 -1
  29. package/build/ExpoArcgisGeometryModule.d.ts.map +1 -1
  30. package/build/ExpoArcgisGeometryModule.js.map +1 -1
  31. package/build/ExpoArcgisModule.d.ts +19 -4
  32. package/build/ExpoArcgisModule.d.ts.map +1 -1
  33. package/build/ExpoArcgisModule.js.map +1 -1
  34. package/build/FeatureLayer.d.ts.map +1 -1
  35. package/build/FeatureLayer.js +2 -2
  36. package/build/FeatureLayer.js.map +1 -1
  37. package/build/auth.d.ts +39 -0
  38. package/build/auth.d.ts.map +1 -1
  39. package/build/auth.js +47 -0
  40. package/build/auth.js.map +1 -1
  41. package/build/geometryEngine.d.ts +25 -1
  42. package/build/geometryEngine.d.ts.map +1 -1
  43. package/build/geometryEngine.js +24 -0
  44. package/build/geometryEngine.js.map +1 -1
  45. package/build/index.d.ts +3 -1
  46. package/build/index.d.ts.map +1 -1
  47. package/build/index.js +3 -1
  48. package/build/index.js.map +1 -1
  49. package/build/layers.d.ts +9 -1
  50. package/build/layers.d.ts.map +1 -1
  51. package/build/layers.js +8 -0
  52. package/build/layers.js.map +1 -1
  53. package/build/offline.d.ts +7 -1
  54. package/build/offline.d.ts.map +1 -1
  55. package/build/offline.js +7 -0
  56. package/build/offline.js.map +1 -1
  57. package/build/router.d.ts +6 -1
  58. package/build/router.d.ts.map +1 -1
  59. package/build/router.js +6 -0
  60. package/build/router.js.map +1 -1
  61. package/expo-module.config.json +6 -2
  62. package/ios/DynamicEntityLayerRef.swift +46 -0
  63. package/ios/ExpoArcgisExtrasModule.swift +104 -0
  64. package/ios/ExpoArcgisGeometryModule.swift +41 -0
  65. package/ios/ExpoArcgisMapView.swift +15 -0
  66. package/ios/ExpoArcgisModule.swift +18 -34
  67. package/ios/ExpoArcgisSceneView.swift +39 -0
  68. package/ios/GeocoderFunctions.swift +9 -1
  69. package/ios/GeometryEngineFunctions.swift +122 -0
  70. package/ios/GeoprocessingFunctions.swift +20 -0
  71. package/ios/GraphicsOverlayRef.swift +24 -0
  72. package/ios/LayerRef.swift +201 -7
  73. package/ios/OfflineFunctions.swift +14 -0
  74. package/ios/QueryCodec.swift +21 -1
  75. package/ios/RouterFunctions.swift +57 -0
  76. package/ios/UtilityNetworkRef.swift +21 -0
  77. package/package.json +1 -1
  78. package/src/DynamicEntityLayer.tsx +12 -1
  79. package/src/ExpoArcgis.types.ts +379 -9
  80. package/src/ExpoArcgisExtrasModule.ts +39 -0
  81. package/src/ExpoArcgisGeometryModule.ts +19 -0
  82. package/src/ExpoArcgisModule.ts +25 -3
  83. package/src/FeatureLayer.tsx +3 -2
  84. package/src/auth.ts +55 -0
  85. package/src/geometryEngine.ts +40 -0
  86. package/src/index.ts +5 -0
  87. package/src/layers.tsx +16 -0
  88. package/src/offline.ts +14 -0
  89. package/src/router.ts +16 -1
package/README.md CHANGED
@@ -18,14 +18,14 @@ authentication.
18
18
  | Area | What's covered |
19
19
  |---|---|
20
20
  | **2D / 3D** | `<MapView>` + `<Map>`; `<SceneView>` + `<Scene>` (surface, camera, web scenes, light/shadows) |
21
- | **Layers** | Feature, Tile, MapImage, Vector-tile, Raster, WMS, WMTS, KML, Scene, IntegratedMesh, PointCloud, OGC 3D Tiles, WebTiled, OpenStreetMap, WFS, OGC API Features, DynamicEntity (stream), Annotation, Dimension, BuildingScene (3D), OrientedImagery, SubtypeFeature, **Group** (container) |
21
+ | **Layers** | Feature, Tile, MapImage, Vector-tile, Raster, WMS, WMTS, KML, Scene, IntegratedMesh, PointCloud, OGC 3D Tiles, WebTiled, OpenStreetMap, WFS, OGC API Features, DynamicEntity (stream), Annotation, Dimension, BuildingScene (3D), OrientedImagery, SubtypeFeature, **Group** (container), **FeatureCollection** (in-memory), **GeoPackage** (local `.gpkg`) |
22
22
  | **Graphics** | `<GraphicsOverlay>` + `<Graphic>`, symbols (simple marker/line/fill, text, 3D scene symbol, picture-marker), renderers (simple / unique-value / class-breaks), labels, clustering |
23
23
  | **Geometry** | `geometryEngine` (buffer, project, distance, intersect, …), `coordinateFormatter`, codec for all geometry types |
24
24
  | **Query** | feature query / count / extent / statistics on a `<FeatureLayer>` ref; `identify` on a view ref |
25
25
  | **Editing** | add / update / delete features, `<GeometryEditor>` (tools), feature templates |
26
26
  | **Location** | device location, simulated location data source, `onLocationChange` |
27
27
  | **Geocoding** | `geocoder.geocode` / `reverseGeocode` / `suggest`, offline `.loc` locators |
28
- | **Routing** | `router.solveRoute` / directions, travel modes, point barriers, curb approach |
28
+ | **Routing** | `router.solveRoute` / directions, travel modes, point barriers, curb approach; `router.createRouteTracker` turn-by-turn navigation |
29
29
  | **Analysis (3D)** | `<AnalysisOverlay>` + `<Viewshed>` / `<LineOfSight>` / `<DistanceMeasurement>` |
30
30
  | **Geoprocessing** | `geoprocessor.execute` → `JobRef` (progress + cancel), typed parameters |
31
31
  | **Utility network** | `<UtilityNetwork>` load + trace, named configs, associations, `describeNetwork` |
@@ -126,7 +126,7 @@ const [hit] = await geocoder.geocode('Los Angeles');
126
126
  `IntegratedMeshLayer`, `PointCloudLayer`, `Ogc3DTilesLayer`, `WebTiledLayer`, `OpenStreetMapLayer`,
127
127
  `WmsLayer`, `WmtsLayer`, `RasterLayer`, `KmlLayer`, `WfsLayer`, `OgcFeatureLayer`, `DynamicEntityLayer`,
128
128
  `AnnotationLayer`, `DimensionLayer`, `BuildingSceneLayer`, `OrientedImageryLayer`, `SubtypeFeatureLayer`,
129
- `GroupLayer`
129
+ `GroupLayer`, `FeatureCollectionLayer`, `GeoPackageLayer`
130
130
  - **Graphics & analysis** — `GraphicsOverlay`, `Graphic`, `AnalysisOverlay`, `Viewshed`, `LineOfSight`,
131
131
  `DistanceMeasurement`, `GeometryEditor`, `UtilityNetwork`
132
132
  - **Namespaces** — `geometryEngine`, `coordinateFormatter`, `geocoder`, `router`, `geoprocessor`, `offline`
@@ -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). */
@@ -97,6 +109,24 @@ fun connectionStatusString(status: ConnectionStatus): String = when (status) {
97
109
  is ConnectionStatus.Failed -> "failed"
98
110
  }
99
111
 
112
+ /**
113
+ * Builds a compact payload for the `onDynamicEntityChange` event.
114
+ * Only `received` and `purged` change types are emitted (observation-only updates are
115
+ * captured within `dynamicEntityReceivedEvent` which fires per-entity arrival, not per
116
+ * observation, so the event rate is bounded to entity lifecycle changes).
117
+ * Geometry is serialized only when present; attributes are passed as-is (the map returned
118
+ * by the SDK is a shallow snapshot of the current entity attributes).
119
+ */
120
+ private fun dynamicEntityPayload(changeType: String, entity: com.arcgismaps.realtime.DynamicEntity): Map<String, Any?> {
121
+ val payload = mutableMapOf<String, Any?>(
122
+ "changeType" to changeType,
123
+ "entityId" to entity.id,
124
+ "attributes" to entity.attributes.toMap(),
125
+ )
126
+ entity.geometry?.let { payload["geometry"] = dictFromGeometry(it) }
127
+ return payload
128
+ }
129
+
100
130
  /** Builds the real-time data source: a custom feed (push from JS) or a stream service. */
101
131
  private fun buildDataSource(
102
132
  props: Map<String, Any?>,
@@ -0,0 +1,102 @@
1
+ package expo.modules.arcgis
2
+
3
+ import com.arcgismaps.ArcGISEnvironment
4
+ import com.arcgismaps.httpcore.authentication.TokenCredential
5
+ import expo.modules.kotlin.functions.Coroutine
6
+ import expo.modules.kotlin.modules.Module
7
+ import expo.modules.kotlin.modules.ModuleDefinition
8
+
9
+ /**
10
+ * Third native module, hosting the heavier operational-layer SharedObject classes (currently
11
+ * [FeatureLayerRef]), exposed to JS as `ExpoArcgisExtras`. Split out of [ExpoArcgisGeometryModule]
12
+ * so that no module's `definition()` exceeds the JVM 64 KB method-size limit. SharedObjects are
13
+ * global, so a ref constructed here attaches to a `<MapView>` from the main module fine.
14
+ */
15
+ class ExpoArcgisExtrasModule : Module() {
16
+ override fun definition() = ModuleDefinition {
17
+ Name("ExpoArcgisExtras")
18
+
19
+ Class(FeatureLayerRef::class) {
20
+ Constructor { props: Map<String, Any?> ->
21
+ FeatureLayerRef(appContext, props).also { it.applyProps(props) }
22
+ }
23
+ Function("applyProps") { ref: FeatureLayerRef, changed: Map<String, Any?> ->
24
+ ref.applyProps(changed)
25
+ }
26
+ AsyncFunction("queryFeatures") Coroutine { ref: FeatureLayerRef, query: Map<String, Any?>? ->
27
+ ref.queryFeatures(query)
28
+ }
29
+ AsyncFunction("queryFeatureCount") Coroutine { ref: FeatureLayerRef, query: Map<String, Any?>? ->
30
+ ref.queryFeatureCount(query)
31
+ }
32
+ AsyncFunction("queryExtent") Coroutine { ref: FeatureLayerRef, query: Map<String, Any?>? ->
33
+ ref.queryExtent(query)
34
+ }
35
+ AsyncFunction("queryStatistics") Coroutine { ref: FeatureLayerRef, query: Map<String, Any?> ->
36
+ ref.queryStatistics(query)
37
+ }
38
+ AsyncFunction("queryFeatureTemplates") Coroutine { ref: FeatureLayerRef ->
39
+ ref.queryFeatureTemplates()
40
+ }
41
+ AsyncFunction("addFeature") Coroutine { ref: FeatureLayerRef, attributes: Map<String, Any?>, geometry: Map<String, Any?>?, apply: Boolean? ->
42
+ ref.addFeature(attributes, geometry, apply)
43
+ }
44
+ AsyncFunction("updateFeature") Coroutine { ref: FeatureLayerRef, objectId: Long, changes: Map<String, Any?>, apply: Boolean? ->
45
+ ref.updateFeature(objectId, changes, apply)
46
+ }
47
+ AsyncFunction("deleteFeature") Coroutine { ref: FeatureLayerRef, objectId: Long, apply: Boolean? ->
48
+ ref.deleteFeature(objectId, apply)
49
+ }
50
+ AsyncFunction("applyEdits") Coroutine { ref: FeatureLayerRef ->
51
+ ref.applyEdits()
52
+ }
53
+ AsyncFunction("undoLocalEdits") Coroutine { ref: FeatureLayerRef ->
54
+ ref.undoLocalEdits()
55
+ }
56
+ AsyncFunction("queryRelatedFeatures") Coroutine { ref: FeatureLayerRef, objectId: Long ->
57
+ ref.queryRelatedFeatures(objectId)
58
+ }
59
+ AsyncFunction("queryAttachments") Coroutine { ref: FeatureLayerRef, objectId: Long ->
60
+ ref.queryAttachments(objectId)
61
+ }
62
+ AsyncFunction("addAttachment") Coroutine { ref: FeatureLayerRef, objectId: Long, name: String, contentType: String, dataBase64: String ->
63
+ ref.addAttachment(objectId, name, contentType, dataBase64)
64
+ }
65
+ AsyncFunction("fetchAttachment") Coroutine { ref: FeatureLayerRef, objectId: Long, attachmentId: Long ->
66
+ ref.fetchAttachment(objectId, attachmentId)
67
+ }
68
+ AsyncFunction("deleteAttachment") Coroutine { ref: FeatureLayerRef, objectId: Long, attachmentId: Long ->
69
+ ref.deleteAttachment(objectId, attachmentId)
70
+ }
71
+ AsyncFunction("updateAttachment") Coroutine { ref: FeatureLayerRef, objectId: Long, attachmentId: Long, name: String, contentType: String, dataBase64: String ->
72
+ ref.updateAttachment(objectId, attachmentId, name, contentType, dataBase64)
73
+ }
74
+ }
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
+
89
+ // Turn-by-turn navigation — solve a route and track device locations against it.
90
+ AsyncFunction("createRouteTracker") Coroutine { stops: List<Map<String, Any?>>, params: Map<String, Any?> ->
91
+ createRouteTracker(appContext, stops, params)
92
+ }
93
+ Class(RouteTrackerRef::class) {
94
+ AsyncFunction("trackLocation") Coroutine { ref: RouteTrackerRef, location: Map<String, Any?> ->
95
+ ref.trackLocation(location)
96
+ }
97
+ AsyncFunction("switchToNextDestination") Coroutine { ref: RouteTrackerRef ->
98
+ ref.switchToNextDestination()
99
+ }
100
+ }
101
+ }
102
+ }
@@ -1,5 +1,7 @@
1
1
  package expo.modules.arcgis
2
2
 
3
+ import com.arcgismaps.ArcGISEnvironment
4
+ import com.arcgismaps.httpcore.authentication.ArcGISCredentialStore
3
5
  import expo.modules.kotlin.functions.Coroutine
4
6
  import expo.modules.kotlin.modules.Module
5
7
  import expo.modules.kotlin.modules.ModuleDefinition
@@ -30,6 +32,12 @@ class ExpoArcgisGeometryModule : Module() {
30
32
  Function("geClip", ::geClip)
31
33
  Function("geCut", ::geCut)
32
34
  Function("geConvexHull", ::geConvexHull)
35
+ Function("geLabelPoint", ::geLabelPoint)
36
+ Function("geNormalizeCentralMeridian", ::geNormalizeCentralMeridian)
37
+ Function("geReshape", ::geReshape)
38
+ Function("geIntersections", ::geIntersections)
39
+ Function("geExtend", ::geExtend)
40
+ Function("geAutoComplete", ::geAutoComplete)
33
41
  Function("geBoundary", ::geBoundary)
34
42
  Function("geSimplify", ::geSimplify)
35
43
  Function("geDensify", ::geDensify)
@@ -51,6 +59,8 @@ class ExpoArcgisGeometryModule : Module() {
51
59
  Function("geMove", ::geMove)
52
60
  Function("geRotate", ::geRotate)
53
61
  Function("geScale", ::geScale)
62
+ Function("geEllipseGeodesic", ::geEllipseGeodesic)
63
+ Function("geSectorGeodesic", ::geSectorGeodesic)
54
64
 
55
65
  // CoordinateFormatter — point <-> notation strings, exposed as the JS `coordinateFormatter` namespace.
56
66
  Function("cfToLatLong", ::cfToLatLong)
@@ -132,6 +142,36 @@ class ExpoArcgisGeometryModule : Module() {
132
142
  Function("addLayer") { ref: GroupLayerRef, layer: LayerRef -> ref.addLayer(layer) }
133
143
  Function("removeLayer") { ref: GroupLayerRef, layer: LayerRef -> ref.removeLayer(layer) }
134
144
  }
145
+ // FeatureLayerRef moved to the third module (ExpoArcgisExtras) to keep this module's
146
+ // definition() under the 64 KB limit. SharedObjects are global, so it stays cross-module.
147
+ // In-memory FeatureCollectionLayer — built from a client-side schema + features (no service).
148
+ Class(FeatureCollectionLayerRef::class) {
149
+ Constructor { props: Map<String, Any?> ->
150
+ FeatureCollectionLayerRef(appContext, props).also { it.applyProps(props) }
151
+ }
152
+ Function("applyProps") { ref: FeatureCollectionLayerRef, changed: Map<String, Any?> -> ref.applyProps(changed) }
153
+ }
154
+ // GeoPackage layer — async-loads a local .gpkg, picks the feature table, wraps it in a FeatureLayer.
155
+ Class(GeoPackageLayerRef::class) {
156
+ Constructor { props: Map<String, Any?> ->
157
+ GeoPackageLayerRef(
158
+ appContext,
159
+ props["path"] as? String ?: "",
160
+ props["tableName"] as? String,
161
+ ).also { it.applyProps(props) }
162
+ }
163
+ Function("applyProps") { ref: GeoPackageLayerRef, changed: Map<String, Any?> -> ref.applyProps(changed) }
164
+ }
165
+
166
+ // Auth — persistent credential store (survives app restarts via Android encrypted storage).
167
+ // Registered here (not in the main module) because the main module is at the JVM 64 KB limit.
168
+ AsyncFunction("enablePersistentCredentialStore") Coroutine { ->
169
+ val store = ArcGISCredentialStore.createWithPersistence().getOrThrow()
170
+ ArcGISEnvironment.authenticationManager.arcGISCredentialStore = store
171
+ }
172
+ AsyncFunction("clearCredentialStore") Coroutine { ->
173
+ ArcGISEnvironment.authenticationManager.arcGISCredentialStore.removeAll()
174
+ }
135
175
 
136
176
  // Offline — take maps/data offline, exposed as the JS `offline` namespace.
137
177
  AsyncFunction("generateOfflineMap") Coroutine { portalItemId: String, areaOfInterest: Map<String, Any?>, downloadName: String ->
@@ -196,6 +196,22 @@ class ExpoArcgisMapView(context: Context, appContext: AppContext) : ExpoView(con
196
196
  }
197
197
  }
198
198
 
199
+ /** Identifies popups under a screen point — evaluates each and returns `{ title, fields }`. */
200
+ fun identifyPopups(screenPoint: Map<String, Any?>, options: Map<String, Any?>?, promise: Promise) {
201
+ val x = (screenPoint["x"] as? Number)?.toDouble() ?: 0.0
202
+ val y = (screenPoint["y"] as? Number)?.toDouble() ?: 0.0
203
+ val tolerance = (options?.get("tolerance") as? Number)?.toDouble() ?: 12.0
204
+ val maxResults = (options?.get("maxResults") as? Number)?.toInt() ?: 1
205
+ scope.launch {
206
+ try {
207
+ val results = mapView.identifyLayers(ScreenCoordinate(x, y), tolerance, false, maxResults).getOrThrow()
208
+ promise.resolve(serializePopups(results))
209
+ } catch (e: Exception) {
210
+ promise.reject("IDENTIFY_ERROR", e.message ?: "Identify failed", e)
211
+ }
212
+ }
213
+ }
214
+
199
215
  /** Retries loading the map (Loadable pattern) — useful after a network outage. Re-emits the result. */
200
216
  fun retryLoad(promise: Promise) {
201
217
  val map = mapView.map ?: run { promise.resolve(null); return }
@@ -96,38 +96,8 @@ class ExpoArcgisModule : Module() {
96
96
  }
97
97
 
98
98
  // Operational layers — SharedObjects the JS <FeatureLayer>/<TileLayer> construct.
99
- Class(FeatureLayerRef::class) {
100
- Constructor { props: Map<String, Any?> ->
101
- FeatureLayerRef(appContext, props).also { it.applyProps(props) }
102
- }
103
- Function("applyProps") { ref: FeatureLayerRef, changed: Map<String, Any?> ->
104
- ref.applyProps(changed)
105
- }
106
- AsyncFunction("queryFeatures") Coroutine { ref: FeatureLayerRef, query: Map<String, Any?>? ->
107
- ref.queryFeatures(query)
108
- }
109
- AsyncFunction("queryFeatureCount") Coroutine { ref: FeatureLayerRef, query: Map<String, Any?>? ->
110
- ref.queryFeatureCount(query)
111
- }
112
- AsyncFunction("queryExtent") Coroutine { ref: FeatureLayerRef, query: Map<String, Any?>? ->
113
- ref.queryExtent(query)
114
- }
115
- AsyncFunction("queryStatistics") Coroutine { ref: FeatureLayerRef, query: Map<String, Any?> ->
116
- ref.queryStatistics(query)
117
- }
118
- AsyncFunction("queryFeatureTemplates") Coroutine { ref: FeatureLayerRef ->
119
- ref.queryFeatureTemplates()
120
- }
121
- AsyncFunction("addFeature") Coroutine { ref: FeatureLayerRef, attributes: Map<String, Any?>, geometry: Map<String, Any?>? ->
122
- ref.addFeature(attributes, geometry)
123
- }
124
- AsyncFunction("updateFeature") Coroutine { ref: FeatureLayerRef, objectId: Long, changes: Map<String, Any?> ->
125
- ref.updateFeature(objectId, changes)
126
- }
127
- AsyncFunction("deleteFeature") Coroutine { ref: FeatureLayerRef, objectId: Long ->
128
- ref.deleteFeature(objectId)
129
- }
130
- }
99
+ // FeatureLayerRef is registered on the ExpoArcgisGeometry module to keep this module's
100
+ // definition() under the Android JVM 64 KB method-size limit (SharedObjects cross modules).
131
101
 
132
102
  Class(TiledLayerRef::class) {
133
103
  Constructor { props: Map<String, Any?> ->
@@ -264,12 +234,13 @@ class ExpoArcgisModule : Module() {
264
234
  Function("applyProps") { ref: OgcFeatureLayerRef, changed: Map<String, Any?> -> ref.applyProps(changed) }
265
235
  }
266
236
 
267
- // Real-time DynamicEntityLayer (stream service) — emits onConnectionStatusChange.
237
+ // Real-time DynamicEntityLayer (stream service) — emits onConnectionStatusChange +
238
+ // onDynamicEntityChange (received/purged entity events).
268
239
  Class(DynamicEntityLayerRef::class) {
269
240
  Constructor { props: Map<String, Any?> ->
270
241
  DynamicEntityLayerRef(appContext, props).also { it.applyProps(props) }
271
242
  }
272
- Events("onConnectionStatusChange")
243
+ Events("onConnectionStatusChange", "onDynamicEntityChange")
273
244
  Function("applyProps") { ref: DynamicEntityLayerRef, changed: Map<String, Any?> ->
274
245
  ref.applyProps(changed)
275
246
  }
@@ -358,6 +329,10 @@ class ExpoArcgisModule : Module() {
358
329
  AsyncFunction("associations") Coroutine { ref: UtilityNetworkRef, tableName: String, whereClause: String ->
359
330
  ref.associations(tableName, whereClause)
360
331
  }
332
+ AsyncFunction("getState") Coroutine { ref: UtilityNetworkRef -> ref.getState() }
333
+ Function("validateNetworkTopology") { ref: UtilityNetworkRef, extent: Map<String, Any?> ->
334
+ ref.validateNetworkTopology(extent)
335
+ }
361
336
  }
362
337
 
363
338
  // Interactive GeometryEditor — bound to a <MapView> for sketching; emits onGeometryChange.
@@ -401,6 +376,10 @@ class ExpoArcgisModule : Module() {
401
376
  view.identify(screenPoint, options, promise)
402
377
  }
403
378
 
379
+ AsyncFunction("identifyPopups") { view: ExpoArcgisMapView, screenPoint: Map<String, Any?>, options: Map<String, Any?>?, promise: Promise ->
380
+ view.identifyPopups(screenPoint, options, promise)
381
+ }
382
+
404
383
  AsyncFunction("retryLoad") { view: ExpoArcgisMapView, promise: Promise ->
405
384
  view.retryLoad(promise)
406
385
  }
@@ -427,6 +406,10 @@ class ExpoArcgisModule : Module() {
427
406
  view.setCamera(camera)
428
407
  }
429
408
 
409
+ Prop("cameraController") { view: ExpoArcgisSceneView, value: Map<String, Any?>? ->
410
+ view.setCameraController(value)
411
+ }
412
+
430
413
  Prop("sunLighting") { view: ExpoArcgisSceneView, value: String? ->
431
414
  view.setSunLighting(value)
432
415
  }
@@ -446,6 +429,10 @@ class ExpoArcgisModule : Module() {
446
429
  AsyncFunction("identify") { view: ExpoArcgisSceneView, screenPoint: Map<String, Any?>, options: Map<String, Any?>?, promise: Promise ->
447
430
  view.identify(screenPoint, options, promise)
448
431
  }
432
+
433
+ AsyncFunction("identifyPopups") { view: ExpoArcgisSceneView, screenPoint: Map<String, Any?>, options: Map<String, Any?>?, promise: Promise ->
434
+ view.identifyPopups(screenPoint, options, promise)
435
+ }
449
436
  }
450
437
  }
451
438
  }
@@ -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
@@ -101,6 +103,22 @@ class ExpoArcgisSceneView(context: Context, appContext: AppContext) : ExpoView(c
101
103
  }
102
104
  }
103
105
 
106
+ /** Identifies popups under a screen point — evaluates each and returns `{ title, fields }`. */
107
+ fun identifyPopups(screenPoint: Map<String, Any?>, options: Map<String, Any?>?, promise: Promise) {
108
+ val x = (screenPoint["x"] as? Number)?.toDouble() ?: 0.0
109
+ val y = (screenPoint["y"] as? Number)?.toDouble() ?: 0.0
110
+ val tolerance = (options?.get("tolerance") as? Number)?.toDouble() ?: 12.0
111
+ val maxResults = (options?.get("maxResults") as? Number)?.toInt() ?: 1
112
+ scope.launch {
113
+ try {
114
+ val results = sceneView.identifyLayers(ScreenCoordinate(x, y), tolerance, false, maxResults).getOrThrow()
115
+ promise.resolve(serializePopups(results))
116
+ } catch (e: Exception) {
117
+ promise.reject("IDENTIFY_ERROR", e.message ?: "Identify failed", e)
118
+ }
119
+ }
120
+ }
121
+
104
122
  /** Retries loading the scene (Loadable pattern) — useful after a network outage. Re-emits the result. */
105
123
  fun retryLoad(promise: Promise) {
106
124
  val scene = sceneView.scene ?: run { promise.resolve(null); return }
@@ -131,6 +149,24 @@ class ExpoArcgisSceneView(context: Context, appContext: AppContext) : ExpoView(c
131
149
  scope.launch { sceneView.setViewpointCameraAnimated(camera, 0.5f) }
132
150
  }
133
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
+
134
170
  /** Sun lighting mode (shadows). */
135
171
  fun setSunLighting(s: String?) {
136
172
  sceneView.sunLighting = when (s) {
@@ -25,9 +25,19 @@ private fun locatorTask(params: Map<String, Any?>): LocatorTask {
25
25
  return locators.getOrPut(url) { LocatorTask(url) }
26
26
  }
27
27
 
28
- internal suspend fun geocode(searchText: String, params: Map<String, Any?>): List<Map<String, Any?>> =
29
- locatorTask(params).geocode(searchText, buildGeocodeParameters(params)).getOrThrow()
30
- .map { serializeGeocodeResult(it) }
28
+ internal suspend fun geocode(searchText: String, params: Map<String, Any?>): List<Map<String, Any?>> {
29
+ val locator = locatorTask(params)
30
+ val parameters = buildGeocodeParameters(params)
31
+ @Suppress("UNCHECKED_CAST")
32
+ val searchValues = (params["searchValues"] as? Map<*, *>)
33
+ ?.mapNotNull { (k, v) -> if (k is String && v is String) k to v else null }
34
+ ?.toMap()
35
+ return if (!searchValues.isNullOrEmpty()) {
36
+ locator.geocode(searchValues, parameters).getOrThrow()
37
+ } else {
38
+ locator.geocode(searchText, parameters).getOrThrow()
39
+ }.map { serializeGeocodeResult(it) }
40
+ }
31
41
 
32
42
  internal suspend fun reverseGeocode(point: Map<String, Any?>, params: Map<String, Any?>): List<Map<String, Any?>> {
33
43
  val location = geometryFromDict(point) as? Point ?: return emptyList()
@@ -1,9 +1,13 @@
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
8
+ import com.arcgismaps.geometry.Multipart
6
9
  import com.arcgismaps.geometry.Point
10
+ import com.arcgismaps.geometry.Polygon
7
11
  import com.arcgismaps.geometry.Polyline
8
12
 
9
13
  /**
@@ -104,6 +108,39 @@ internal fun geCut(g: Map<String, Any?>, cutter: Map<String, Any?>): List<Map<St
104
108
  internal fun geConvexHull(g: Map<String, Any?>): Map<String, Any?>? =
105
109
  parseGeo(g)?.let { encode(GeometryEngine.convexHullOrNull(it)) }
106
110
 
111
+ internal fun geLabelPoint(g: Map<String, Any?>): Map<String, Any?>? =
112
+ (parseGeo(g) as? Polygon)?.let { encode(GeometryEngine.labelPointOrNull(it)) }
113
+
114
+ internal fun geNormalizeCentralMeridian(g: Map<String, Any?>): Map<String, Any?>? =
115
+ parseGeo(g)?.let { encode(GeometryEngine.normalizeCentralMeridian(it)) }
116
+
117
+ internal fun geReshape(g: Map<String, Any?>, reshaper: Map<String, Any?>): Map<String, Any?>? {
118
+ val multipart = parseGeo(g) as? Multipart ?: return null
119
+ val line = parseGeo(reshaper) as? Polyline ?: return null
120
+ return encode(GeometryEngine.reshape(multipart, line))
121
+ }
122
+
123
+ internal fun geIntersections(a: Map<String, Any?>, b: Map<String, Any?>): List<Map<String, Any?>> {
124
+ val g1 = parseGeo(a) ?: return emptyList()
125
+ val g2 = parseGeo(b) ?: return emptyList()
126
+ return GeometryEngine.tryIntersections(g1, g2).mapNotNull { encode(it) }
127
+ }
128
+
129
+ internal fun geExtend(p: Map<String, Any?>, extender: Map<String, Any?>): Map<String, Any?>? {
130
+ val polyline = parseGeo(p) as? Polyline ?: return null
131
+ val ext = parseGeo(extender) as? Polyline ?: return null
132
+ return encode(GeometryEngine.extend(polyline, ext, emptySet()))
133
+ }
134
+
135
+ internal fun geAutoComplete(
136
+ existing: List<Map<String, Any?>>,
137
+ boundaries: List<Map<String, Any?>>,
138
+ ): List<Map<String, Any?>> {
139
+ val polygons = existing.mapNotNull { parseGeo(it) as? Polygon }
140
+ val lines = boundaries.mapNotNull { parseGeo(it) as? Polyline }
141
+ return GeometryEngine.tryAutoComplete(polygons, lines).mapNotNull { encode(it) }
142
+ }
143
+
107
144
  internal fun geBoundary(g: Map<String, Any?>): Map<String, Any?>? =
108
145
  parseGeo(g)?.let { encode(GeometryEngine.boundaryOrNull(it)) }
109
146
 
@@ -221,3 +258,69 @@ internal fun geScale(
221
258
  else GeometryEngine.scale(geometry, factorX, factorY),
222
259
  )
223
260
  }
261
+
262
+ // region Geodesic construction
263
+
264
+ internal fun geEllipseGeodesic(params: Map<String, Any?>): Map<String, Any?>? {
265
+ val centerDict = params["center"] as? Map<String, Any?> ?: return null
266
+ val center = parsePoint(centerDict) ?: return null
267
+
268
+ val semiAxis1Length = (params["semiAxis1Length"] as? Number)?.toDouble() ?: 0.0
269
+ val semiAxis2Length = (params["semiAxis2Length"] as? Number)?.toDouble() ?: 0.0
270
+ val axisDirection = (params["axisDirection"] as? Number)?.toDouble() ?: 0.0
271
+ val angUnit = angularUnit(params["angularUnit"] as? String)
272
+ val linUnit = linearUnit(params["linearUnit"] as? String)
273
+ val maxSegLen = (params["maxSegmentLength"] as? Number)?.toDouble() ?: 0.0
274
+ val maxPtCount = (params["maxPointCount"] as? Number)?.toLong() ?: 10L
275
+ val geoType = params["geometryType"] as? String
276
+
277
+ val p: GeodesicEllipseParameters = when (geoType) {
278
+ "polyline" -> GeodesicEllipseParameters.Companion.createForPolyline()
279
+ "multipoint" -> GeodesicEllipseParameters.Companion.createForMultipoint()
280
+ else -> GeodesicEllipseParameters.Companion.createForPolygon()
281
+ }
282
+ p.center = center
283
+ p.semiAxis1Length = semiAxis1Length
284
+ p.semiAxis2Length = semiAxis2Length
285
+ p.axisDirection = axisDirection
286
+ p.angularUnit = angUnit
287
+ p.linearUnit = linUnit
288
+ p.maxSegmentLength = maxSegLen
289
+ p.maxPointCount = maxPtCount
290
+
291
+ return encode(GeometryEngine.ellipseGeodesicOrNull(p))
292
+ }
293
+
294
+ internal fun geSectorGeodesic(params: Map<String, Any?>): Map<String, Any?>? {
295
+ val centerDict = params["center"] as? Map<String, Any?> ?: return null
296
+ val center = parsePoint(centerDict) ?: return null
297
+
298
+ val semiAxis1Length = (params["semiAxis1Length"] as? Number)?.toDouble() ?: 0.0
299
+ val semiAxis2Length = (params["semiAxis2Length"] as? Number)?.toDouble() ?: 0.0
300
+ val axisDirection = (params["axisDirection"] as? Number)?.toDouble() ?: 0.0
301
+ val sectorAngle = (params["sectorAngle"] as? Number)?.toDouble() ?: 0.0
302
+ val startDirection = (params["startDirection"] as? Number)?.toDouble() ?: 0.0
303
+ val angUnit = angularUnit(params["angularUnit"] as? String)
304
+ val linUnit = linearUnit(params["linearUnit"] as? String)
305
+ val maxSegLen = (params["maxSegmentLength"] as? Number)?.toDouble() ?: 0.0
306
+ val maxPtCount = (params["maxPointCount"] as? Number)?.toLong() ?: 10L
307
+ val geoType = params["geometryType"] as? String
308
+
309
+ val p: GeodesicSectorParameters = when (geoType) {
310
+ "polyline" -> GeodesicSectorParameters.Companion.createForPolyline()
311
+ "multipoint" -> GeodesicSectorParameters.Companion.createForMultipoint()
312
+ else -> GeodesicSectorParameters.Companion.createForPolygon()
313
+ }
314
+ p.center = center
315
+ p.semiAxis1Length = semiAxis1Length
316
+ p.semiAxis2Length = semiAxis2Length
317
+ p.axisDirection = axisDirection
318
+ p.sectorAngle = sectorAngle
319
+ p.startDirection = startDirection
320
+ p.angularUnit = angUnit
321
+ p.linearUnit = linUnit
322
+ p.maxSegmentLength = maxSegLen
323
+ p.maxPointCount = maxPtCount
324
+
325
+ return encode(GeometryEngine.sectorGeodesicOrNull(p))
326
+ }
@@ -4,12 +4,15 @@ 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
8
9
  import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingDouble
9
10
  import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingFeatures
10
11
  import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingLinearUnit
11
12
  import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingLong
13
+ import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingMultiValue
12
14
  import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingParameter
15
+ import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingParameterType
13
16
  import com.arcgismaps.tasks.geoprocessing.geoprocessingparameters.GeoprocessingString
14
17
  import expo.modules.kotlin.AppContext
15
18
  import java.time.Instant
@@ -60,6 +63,28 @@ private fun buildGeoprocessingParameter(d: Map<*, *>): GeoprocessingParameter? =
60
63
  ?.map { Graphic().apply { geometry = it } } ?: emptyList()
61
64
  GeoprocessingFeatures(FeatureCollectionTable(graphics, emptyList()))
62
65
  }
66
+ "multiValue" -> {
67
+ // JS numbers → GeoprocessingDouble, JS strings → GeoprocessingString.
68
+ val rawValues = d["values"] as? List<*> ?: emptyList<Any>()
69
+ val elements: List<GeoprocessingParameter> = rawValues.mapNotNull { v ->
70
+ when (v) {
71
+ is Number -> GeoprocessingDouble(v.toDouble())
72
+ is String -> GeoprocessingString(v)
73
+ else -> null
74
+ }
75
+ }
76
+ // Use the element type of the first item to determine the multiValue's parameterType;
77
+ // fall back to GeoprocessingParameterType.GeoprocessingString when the list is empty.
78
+ val paramType = if (elements.firstOrNull() is GeoprocessingDouble)
79
+ GeoprocessingParameterType.GeoprocessingDouble
80
+ else
81
+ GeoprocessingParameterType.GeoprocessingString
82
+ GeoprocessingMultiValue(paramType, elements)
83
+ }
84
+ "dataFile" -> {
85
+ val url = d["url"] as? String ?: return null
86
+ GeoprocessingDataFile.Companion.createWithUrl(url)
87
+ }
63
88
  else -> null
64
89
  }
65
90