expo-arcgis 0.1.1 → 0.1.2

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 (74) hide show
  1. package/README.md +3 -3
  2. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisExtrasModule.kt +87 -0
  3. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisGeometryModule.kt +38 -0
  4. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisMapView.kt +16 -0
  5. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisModule.kt +14 -32
  6. package/android/src/main/java/expo/modules/arcgis/ExpoArcgisSceneView.kt +16 -0
  7. package/android/src/main/java/expo/modules/arcgis/GeocoderFunctions.kt +13 -3
  8. package/android/src/main/java/expo/modules/arcgis/GeometryEngineFunctions.kt +35 -0
  9. package/android/src/main/java/expo/modules/arcgis/GraphicsOverlayRef.kt +25 -0
  10. package/android/src/main/java/expo/modules/arcgis/LayerRef.kt +189 -7
  11. package/android/src/main/java/expo/modules/arcgis/QueryCodec.kt +21 -0
  12. package/android/src/main/java/expo/modules/arcgis/RouterFunctions.kt +61 -0
  13. package/android/src/main/java/expo/modules/arcgis/UtilityNetworkRef.kt +24 -0
  14. package/build/DynamicEntityLayer.d.ts.map +1 -1
  15. package/build/ExpoArcgis.types.d.ts +232 -8
  16. package/build/ExpoArcgis.types.d.ts.map +1 -1
  17. package/build/ExpoArcgis.types.js.map +1 -1
  18. package/build/ExpoArcgisExtrasModule.d.ts +16 -0
  19. package/build/ExpoArcgisExtrasModule.d.ts.map +1 -0
  20. package/build/ExpoArcgisExtrasModule.js +3 -0
  21. package/build/ExpoArcgisExtrasModule.js.map +1 -0
  22. package/build/ExpoArcgisGeometryModule.d.ts +11 -1
  23. package/build/ExpoArcgisGeometryModule.d.ts.map +1 -1
  24. package/build/ExpoArcgisGeometryModule.js.map +1 -1
  25. package/build/ExpoArcgisModule.d.ts +18 -4
  26. package/build/ExpoArcgisModule.d.ts.map +1 -1
  27. package/build/ExpoArcgisModule.js.map +1 -1
  28. package/build/FeatureLayer.d.ts.map +1 -1
  29. package/build/FeatureLayer.js +2 -2
  30. package/build/FeatureLayer.js.map +1 -1
  31. package/build/auth.d.ts +25 -0
  32. package/build/auth.d.ts.map +1 -1
  33. package/build/auth.js +30 -0
  34. package/build/auth.js.map +1 -1
  35. package/build/geometryEngine.d.ts +12 -0
  36. package/build/geometryEngine.d.ts.map +1 -1
  37. package/build/geometryEngine.js +12 -0
  38. package/build/geometryEngine.js.map +1 -1
  39. package/build/index.d.ts +3 -1
  40. package/build/index.d.ts.map +1 -1
  41. package/build/index.js +3 -1
  42. package/build/index.js.map +1 -1
  43. package/build/layers.d.ts +9 -1
  44. package/build/layers.d.ts.map +1 -1
  45. package/build/layers.js +8 -0
  46. package/build/layers.js.map +1 -1
  47. package/build/router.d.ts +6 -1
  48. package/build/router.d.ts.map +1 -1
  49. package/build/router.js +6 -0
  50. package/build/router.js.map +1 -1
  51. package/expo-module.config.json +6 -2
  52. package/ios/ExpoArcgisExtrasModule.swift +85 -0
  53. package/ios/ExpoArcgisGeometryModule.swift +39 -0
  54. package/ios/ExpoArcgisMapView.swift +15 -0
  55. package/ios/ExpoArcgisModule.swift +14 -34
  56. package/ios/ExpoArcgisSceneView.swift +15 -0
  57. package/ios/GeocoderFunctions.swift +9 -1
  58. package/ios/GeometryEngineFunctions.swift +39 -0
  59. package/ios/GraphicsOverlayRef.swift +20 -0
  60. package/ios/LayerRef.swift +201 -7
  61. package/ios/QueryCodec.swift +21 -1
  62. package/ios/RouterFunctions.swift +57 -0
  63. package/ios/UtilityNetworkRef.swift +21 -0
  64. package/package.json +1 -1
  65. package/src/ExpoArcgis.types.ts +244 -8
  66. package/src/ExpoArcgisExtrasModule.ts +22 -0
  67. package/src/ExpoArcgisGeometryModule.ts +15 -0
  68. package/src/ExpoArcgisModule.ts +23 -3
  69. package/src/FeatureLayer.tsx +3 -2
  70. package/src/auth.ts +32 -0
  71. package/src/geometryEngine.ts +22 -0
  72. package/src/index.ts +4 -0
  73. package/src/layers.tsx +16 -0
  74. 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`
@@ -0,0 +1,87 @@
1
+ package expo.modules.arcgis
2
+
3
+ import expo.modules.kotlin.functions.Coroutine
4
+ import expo.modules.kotlin.modules.Module
5
+ import expo.modules.kotlin.modules.ModuleDefinition
6
+
7
+ /**
8
+ * Third native module, hosting the heavier operational-layer SharedObject classes (currently
9
+ * [FeatureLayerRef]), exposed to JS as `ExpoArcgisExtras`. Split out of [ExpoArcgisGeometryModule]
10
+ * so that no module's `definition()` exceeds the JVM 64 KB method-size limit. SharedObjects are
11
+ * global, so a ref constructed here attaches to a `<MapView>` from the main module fine.
12
+ */
13
+ class ExpoArcgisExtrasModule : Module() {
14
+ override fun definition() = ModuleDefinition {
15
+ Name("ExpoArcgisExtras")
16
+
17
+ Class(FeatureLayerRef::class) {
18
+ Constructor { props: Map<String, Any?> ->
19
+ FeatureLayerRef(appContext, props).also { it.applyProps(props) }
20
+ }
21
+ Function("applyProps") { ref: FeatureLayerRef, changed: Map<String, Any?> ->
22
+ ref.applyProps(changed)
23
+ }
24
+ AsyncFunction("queryFeatures") Coroutine { ref: FeatureLayerRef, query: Map<String, Any?>? ->
25
+ ref.queryFeatures(query)
26
+ }
27
+ AsyncFunction("queryFeatureCount") Coroutine { ref: FeatureLayerRef, query: Map<String, Any?>? ->
28
+ ref.queryFeatureCount(query)
29
+ }
30
+ AsyncFunction("queryExtent") Coroutine { ref: FeatureLayerRef, query: Map<String, Any?>? ->
31
+ ref.queryExtent(query)
32
+ }
33
+ AsyncFunction("queryStatistics") Coroutine { ref: FeatureLayerRef, query: Map<String, Any?> ->
34
+ ref.queryStatistics(query)
35
+ }
36
+ AsyncFunction("queryFeatureTemplates") Coroutine { ref: FeatureLayerRef ->
37
+ ref.queryFeatureTemplates()
38
+ }
39
+ AsyncFunction("addFeature") Coroutine { ref: FeatureLayerRef, attributes: Map<String, Any?>, geometry: Map<String, Any?>?, apply: Boolean? ->
40
+ ref.addFeature(attributes, geometry, apply)
41
+ }
42
+ AsyncFunction("updateFeature") Coroutine { ref: FeatureLayerRef, objectId: Long, changes: Map<String, Any?>, apply: Boolean? ->
43
+ ref.updateFeature(objectId, changes, apply)
44
+ }
45
+ AsyncFunction("deleteFeature") Coroutine { ref: FeatureLayerRef, objectId: Long, apply: Boolean? ->
46
+ ref.deleteFeature(objectId, apply)
47
+ }
48
+ AsyncFunction("applyEdits") Coroutine { ref: FeatureLayerRef ->
49
+ ref.applyEdits()
50
+ }
51
+ AsyncFunction("undoLocalEdits") Coroutine { ref: FeatureLayerRef ->
52
+ ref.undoLocalEdits()
53
+ }
54
+ AsyncFunction("queryRelatedFeatures") Coroutine { ref: FeatureLayerRef, objectId: Long ->
55
+ ref.queryRelatedFeatures(objectId)
56
+ }
57
+ AsyncFunction("queryAttachments") Coroutine { ref: FeatureLayerRef, objectId: Long ->
58
+ ref.queryAttachments(objectId)
59
+ }
60
+ AsyncFunction("addAttachment") Coroutine { ref: FeatureLayerRef, objectId: Long, name: String, contentType: String, dataBase64: String ->
61
+ ref.addAttachment(objectId, name, contentType, dataBase64)
62
+ }
63
+ AsyncFunction("fetchAttachment") Coroutine { ref: FeatureLayerRef, objectId: Long, attachmentId: Long ->
64
+ ref.fetchAttachment(objectId, attachmentId)
65
+ }
66
+ AsyncFunction("deleteAttachment") Coroutine { ref: FeatureLayerRef, objectId: Long, attachmentId: Long ->
67
+ ref.deleteAttachment(objectId, attachmentId)
68
+ }
69
+ AsyncFunction("updateAttachment") Coroutine { ref: FeatureLayerRef, objectId: Long, attachmentId: Long, name: String, contentType: String, dataBase64: String ->
70
+ ref.updateAttachment(objectId, attachmentId, name, contentType, dataBase64)
71
+ }
72
+ }
73
+
74
+ // Turn-by-turn navigation — solve a route and track device locations against it.
75
+ AsyncFunction("createRouteTracker") Coroutine { stops: List<Map<String, Any?>>, params: Map<String, Any?> ->
76
+ createRouteTracker(appContext, stops, params)
77
+ }
78
+ Class(RouteTrackerRef::class) {
79
+ AsyncFunction("trackLocation") Coroutine { ref: RouteTrackerRef, location: Map<String, Any?> ->
80
+ ref.trackLocation(location)
81
+ }
82
+ AsyncFunction("switchToNextDestination") Coroutine { ref: RouteTrackerRef ->
83
+ ref.switchToNextDestination()
84
+ }
85
+ }
86
+ }
87
+ }
@@ -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)
@@ -132,6 +140,36 @@ class ExpoArcgisGeometryModule : Module() {
132
140
  Function("addLayer") { ref: GroupLayerRef, layer: LayerRef -> ref.addLayer(layer) }
133
141
  Function("removeLayer") { ref: GroupLayerRef, layer: LayerRef -> ref.removeLayer(layer) }
134
142
  }
143
+ // FeatureLayerRef moved to the third module (ExpoArcgisExtras) to keep this module's
144
+ // definition() under the 64 KB limit. SharedObjects are global, so it stays cross-module.
145
+ // In-memory FeatureCollectionLayer — built from a client-side schema + features (no service).
146
+ Class(FeatureCollectionLayerRef::class) {
147
+ Constructor { props: Map<String, Any?> ->
148
+ FeatureCollectionLayerRef(appContext, props).also { it.applyProps(props) }
149
+ }
150
+ Function("applyProps") { ref: FeatureCollectionLayerRef, changed: Map<String, Any?> -> ref.applyProps(changed) }
151
+ }
152
+ // GeoPackage layer — async-loads a local .gpkg, picks the feature table, wraps it in a FeatureLayer.
153
+ Class(GeoPackageLayerRef::class) {
154
+ Constructor { props: Map<String, Any?> ->
155
+ GeoPackageLayerRef(
156
+ appContext,
157
+ props["path"] as? String ?: "",
158
+ props["tableName"] as? String,
159
+ ).also { it.applyProps(props) }
160
+ }
161
+ Function("applyProps") { ref: GeoPackageLayerRef, changed: Map<String, Any?> -> ref.applyProps(changed) }
162
+ }
163
+
164
+ // Auth — persistent credential store (survives app restarts via Android encrypted storage).
165
+ // Registered here (not in the main module) because the main module is at the JVM 64 KB limit.
166
+ AsyncFunction("enablePersistentCredentialStore") Coroutine { ->
167
+ val store = ArcGISCredentialStore.createWithPersistence().getOrThrow()
168
+ ArcGISEnvironment.authenticationManager.arcGISCredentialStore = store
169
+ }
170
+ AsyncFunction("clearCredentialStore") Coroutine { ->
171
+ ArcGISEnvironment.authenticationManager.arcGISCredentialStore.removeAll()
172
+ }
135
173
 
136
174
  // Offline — take maps/data offline, exposed as the JS `offline` namespace.
137
175
  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?> ->
@@ -358,6 +328,10 @@ class ExpoArcgisModule : Module() {
358
328
  AsyncFunction("associations") Coroutine { ref: UtilityNetworkRef, tableName: String, whereClause: String ->
359
329
  ref.associations(tableName, whereClause)
360
330
  }
331
+ AsyncFunction("getState") Coroutine { ref: UtilityNetworkRef -> ref.getState() }
332
+ Function("validateNetworkTopology") { ref: UtilityNetworkRef, extent: Map<String, Any?> ->
333
+ ref.validateNetworkTopology(extent)
334
+ }
361
335
  }
362
336
 
363
337
  // Interactive GeometryEditor — bound to a <MapView> for sketching; emits onGeometryChange.
@@ -401,6 +375,10 @@ class ExpoArcgisModule : Module() {
401
375
  view.identify(screenPoint, options, promise)
402
376
  }
403
377
 
378
+ AsyncFunction("identifyPopups") { view: ExpoArcgisMapView, screenPoint: Map<String, Any?>, options: Map<String, Any?>?, promise: Promise ->
379
+ view.identifyPopups(screenPoint, options, promise)
380
+ }
381
+
404
382
  AsyncFunction("retryLoad") { view: ExpoArcgisMapView, promise: Promise ->
405
383
  view.retryLoad(promise)
406
384
  }
@@ -446,6 +424,10 @@ class ExpoArcgisModule : Module() {
446
424
  AsyncFunction("identify") { view: ExpoArcgisSceneView, screenPoint: Map<String, Any?>, options: Map<String, Any?>?, promise: Promise ->
447
425
  view.identify(screenPoint, options, promise)
448
426
  }
427
+
428
+ AsyncFunction("identifyPopups") { view: ExpoArcgisSceneView, screenPoint: Map<String, Any?>, options: Map<String, Any?>?, promise: Promise ->
429
+ view.identifyPopups(screenPoint, options, promise)
430
+ }
449
431
  }
450
432
  }
451
433
  }
@@ -101,6 +101,22 @@ class ExpoArcgisSceneView(context: Context, appContext: AppContext) : ExpoView(c
101
101
  }
102
102
  }
103
103
 
104
+ /** Identifies popups under a screen point — evaluates each and returns `{ title, fields }`. */
105
+ fun identifyPopups(screenPoint: Map<String, Any?>, options: Map<String, Any?>?, promise: Promise) {
106
+ val x = (screenPoint["x"] as? Number)?.toDouble() ?: 0.0
107
+ val y = (screenPoint["y"] as? Number)?.toDouble() ?: 0.0
108
+ val tolerance = (options?.get("tolerance") as? Number)?.toDouble() ?: 12.0
109
+ val maxResults = (options?.get("maxResults") as? Number)?.toInt() ?: 1
110
+ scope.launch {
111
+ try {
112
+ val results = sceneView.identifyLayers(ScreenCoordinate(x, y), tolerance, false, maxResults).getOrThrow()
113
+ promise.resolve(serializePopups(results))
114
+ } catch (e: Exception) {
115
+ promise.reject("IDENTIFY_ERROR", e.message ?: "Identify failed", e)
116
+ }
117
+ }
118
+ }
119
+
104
120
  /** Retries loading the scene (Loadable pattern) — useful after a network outage. Re-emits the result. */
105
121
  fun retryLoad(promise: Promise) {
106
122
  val scene = sceneView.scene ?: run { promise.resolve(null); return }
@@ -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()
@@ -3,7 +3,9 @@ package expo.modules.arcgis
3
3
  import com.arcgismaps.geometry.Envelope
4
4
  import com.arcgismaps.geometry.Geometry
5
5
  import com.arcgismaps.geometry.GeometryEngine
6
+ import com.arcgismaps.geometry.Multipart
6
7
  import com.arcgismaps.geometry.Point
8
+ import com.arcgismaps.geometry.Polygon
7
9
  import com.arcgismaps.geometry.Polyline
8
10
 
9
11
  /**
@@ -104,6 +106,39 @@ internal fun geCut(g: Map<String, Any?>, cutter: Map<String, Any?>): List<Map<St
104
106
  internal fun geConvexHull(g: Map<String, Any?>): Map<String, Any?>? =
105
107
  parseGeo(g)?.let { encode(GeometryEngine.convexHullOrNull(it)) }
106
108
 
109
+ internal fun geLabelPoint(g: Map<String, Any?>): Map<String, Any?>? =
110
+ (parseGeo(g) as? Polygon)?.let { encode(GeometryEngine.labelPointOrNull(it)) }
111
+
112
+ internal fun geNormalizeCentralMeridian(g: Map<String, Any?>): Map<String, Any?>? =
113
+ parseGeo(g)?.let { encode(GeometryEngine.normalizeCentralMeridian(it)) }
114
+
115
+ internal fun geReshape(g: Map<String, Any?>, reshaper: Map<String, Any?>): Map<String, Any?>? {
116
+ val multipart = parseGeo(g) as? Multipart ?: return null
117
+ val line = parseGeo(reshaper) as? Polyline ?: return null
118
+ return encode(GeometryEngine.reshape(multipart, line))
119
+ }
120
+
121
+ internal fun geIntersections(a: Map<String, Any?>, b: Map<String, Any?>): List<Map<String, Any?>> {
122
+ val g1 = parseGeo(a) ?: return emptyList()
123
+ val g2 = parseGeo(b) ?: return emptyList()
124
+ return GeometryEngine.tryIntersections(g1, g2).mapNotNull { encode(it) }
125
+ }
126
+
127
+ internal fun geExtend(p: Map<String, Any?>, extender: Map<String, Any?>): Map<String, Any?>? {
128
+ val polyline = parseGeo(p) as? Polyline ?: return null
129
+ val ext = parseGeo(extender) as? Polyline ?: return null
130
+ return encode(GeometryEngine.extend(polyline, ext, emptySet()))
131
+ }
132
+
133
+ internal fun geAutoComplete(
134
+ existing: List<Map<String, Any?>>,
135
+ boundaries: List<Map<String, Any?>>,
136
+ ): List<Map<String, Any?>> {
137
+ val polygons = existing.mapNotNull { parseGeo(it) as? Polygon }
138
+ val lines = boundaries.mapNotNull { parseGeo(it) as? Polyline }
139
+ return GeometryEngine.tryAutoComplete(polygons, lines).mapNotNull { encode(it) }
140
+ }
141
+
107
142
  internal fun geBoundary(g: Map<String, Any?>): Map<String, Any?>? =
108
143
  parseGeo(g)?.let { encode(GeometryEngine.boundaryOrNull(it)) }
109
144
 
@@ -15,8 +15,11 @@ import com.arcgismaps.mapping.symbology.SimpleFillSymbol
15
15
  import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle
16
16
  import com.arcgismaps.mapping.symbology.SimpleLineSymbol
17
17
  import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle
18
+ import com.arcgismaps.mapping.symbology.DistanceCompositeSceneSymbol
19
+ import com.arcgismaps.mapping.symbology.DistanceSymbolRange
18
20
  import com.arcgismaps.mapping.symbology.SimpleMarkerSceneSymbol
19
21
  import com.arcgismaps.mapping.symbology.SimpleMarkerSceneSymbolStyle
22
+ import com.arcgismaps.mapping.symbology.PictureFillSymbol
20
23
  import com.arcgismaps.mapping.symbology.PictureMarkerSymbol
21
24
  import com.arcgismaps.mapping.symbology.SimpleMarkerSymbol
22
25
  import com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyle
@@ -188,6 +191,28 @@ private fun buildSymbol(s: Map<*, *>): Symbol? = when (s["type"]) {
188
191
  (s["height"] as? Number)?.toFloat()?.let { height = it }
189
192
  }
190
193
  }
194
+ "picture-fill" -> (s["url"] as? String)?.let { url ->
195
+ PictureFillSymbol(url).apply {
196
+ (s["width"] as? Number)?.toFloat()?.let { width = it }
197
+ (s["height"] as? Number)?.toFloat()?.let { height = it }
198
+ outline = outlineOf(s["outline"])
199
+ }
200
+ }
201
+ "distance-composite-scene" -> {
202
+ val composite = DistanceCompositeSceneSymbol()
203
+ val rangeList = s["ranges"] as? List<*> ?: emptyList<Any>()
204
+ for (rd in rangeList) {
205
+ val rdMap = rd as? Map<*, *> ?: continue
206
+ val sym = (rdMap["symbol"] as? Map<*, *>)?.let(::buildSymbol) ?: continue
207
+ val range = DistanceSymbolRange(
208
+ sym,
209
+ (rdMap["minDistance"] as? Number)?.toDouble(),
210
+ (rdMap["maxDistance"] as? Number)?.toDouble(),
211
+ )
212
+ composite.ranges.add(range)
213
+ }
214
+ composite
215
+ }
191
216
  else -> null
192
217
  }
193
218
 
@@ -1,9 +1,16 @@
1
1
  package expo.modules.arcgis
2
2
 
3
+ import android.util.Base64
4
+ import com.arcgismaps.data.ArcGISFeature
3
5
  import com.arcgismaps.data.ArcGISFeatureTable
4
6
  import com.arcgismaps.data.Feature
7
+ import com.arcgismaps.data.FeatureCollection
8
+ import com.arcgismaps.data.FeatureCollectionTable
5
9
  import com.arcgismaps.data.FeatureRequestMode
6
10
  import com.arcgismaps.data.FeatureTable
11
+ import com.arcgismaps.data.Field
12
+ import com.arcgismaps.data.FieldType
13
+ import com.arcgismaps.data.GeoPackage
7
14
  import com.arcgismaps.data.QueryFeatureFields
8
15
  import com.arcgismaps.data.QueryParameters
9
16
  import com.arcgismaps.data.ServiceFeatureTable
@@ -11,12 +18,17 @@ import com.arcgismaps.data.ShapefileFeatureTable
11
18
  import com.arcgismaps.data.OgcFeatureCollectionTable
12
19
  import com.arcgismaps.data.WfsFeatureTable
13
20
  import com.arcgismaps.mapping.layers.AnnotationLayer
21
+ import kotlinx.coroutines.CoroutineScope
22
+ import kotlinx.coroutines.Dispatchers
23
+ import kotlinx.coroutines.SupervisorJob
24
+ import kotlinx.coroutines.launch
14
25
  import com.arcgismaps.mapping.layers.ArcGISMapImageLayer
15
26
  import com.arcgismaps.mapping.layers.ArcGISSceneLayer
16
27
  import com.arcgismaps.mapping.layers.ArcGISTiledLayer
17
28
  import com.arcgismaps.mapping.layers.ArcGISVectorTiledLayer
18
29
  import com.arcgismaps.mapping.layers.BuildingSceneLayer
19
30
  import com.arcgismaps.mapping.layers.DimensionLayer
31
+ import com.arcgismaps.mapping.layers.FeatureCollectionLayer
20
32
  import com.arcgismaps.mapping.layers.FeatureLayer
21
33
  import com.arcgismaps.mapping.layers.GroupLayer
22
34
  import com.arcgismaps.mapping.layers.IntegratedMeshLayer
@@ -32,6 +44,7 @@ import com.arcgismaps.mapping.layers.KmlLayer
32
44
  import com.arcgismaps.mapping.layers.WmsLayer
33
45
  import com.arcgismaps.mapping.layers.WmtsLayer
34
46
  import com.arcgismaps.mapping.kml.KmlDataset
47
+ import com.arcgismaps.mapping.view.Graphic
35
48
  import com.arcgismaps.raster.ImageServiceRaster
36
49
  import com.arcgismaps.raster.Raster
37
50
  import expo.modules.kotlin.AppContext
@@ -87,28 +100,115 @@ class FeatureLayerRef(appContext: AppContext, props: Map<String, Any?>) : LayerR
87
100
  }
88
101
  }
89
102
 
90
- /** Adds a feature, pushes the edit to the service, and returns the new object id. */
91
- suspend fun addFeature(attributes: Map<String, Any?>, geometry: Map<String, Any?>?): Long? {
103
+ /**
104
+ * Adds a feature. When `apply` is not `false`, pushes the edit and returns the new object id;
105
+ * pass `apply = false` to make a local-only edit (batch with `applyEdits`).
106
+ */
107
+ suspend fun addFeature(attributes: Map<String, Any?>, geometry: Map<String, Any?>?, apply: Boolean?): Long? {
92
108
  val feature = table.createFeature()
93
109
  applyAttributes(feature, attributes)
94
110
  geometry?.let { dict -> geometryFromDict(dict)?.let { feature.geometry = it } }
95
111
  table.addFeature(feature).getOrThrow()
112
+ if (apply == false) return null
96
113
  return persistEdits()
97
114
  }
98
115
 
99
- /** Updates the feature with `objectId` (changed attributes and/or geometry) and pushes the edit. */
100
- suspend fun updateFeature(objectId: Long, changes: Map<String, Any?>) {
116
+ /** Updates the feature with `objectId`. Pass `apply = false` for a local-only edit. */
117
+ suspend fun updateFeature(objectId: Long, changes: Map<String, Any?>, apply: Boolean?) {
101
118
  val feature = featureByObjectId(objectId) ?: return
102
119
  (changes["attributes"] as? Map<*, *>)?.let { applyAttributes(feature, it) }
103
120
  (changes["geometry"] as? Map<*, *>)?.let { geometryFromDict(it)?.let { g -> feature.geometry = g } }
104
121
  table.updateFeature(feature).getOrThrow()
105
- persistEdits()
122
+ if (apply != false) persistEdits()
106
123
  }
107
124
 
108
- /** Deletes the feature with `objectId` and pushes the edit. */
109
- suspend fun deleteFeature(objectId: Long) {
125
+ /** Deletes the feature with `objectId`. Pass `apply = false` for a local-only edit. */
126
+ suspend fun deleteFeature(objectId: Long, apply: Boolean?) {
110
127
  val feature = featureByObjectId(objectId) ?: return
111
128
  table.deleteFeature(feature).getOrThrow()
129
+ if (apply != false) persistEdits()
130
+ }
131
+
132
+ /** Pushes all pending local edits to the service in one batch; returns each edit's result. */
133
+ suspend fun applyEdits(): List<Map<String, Any?>> {
134
+ val serviceTable = table as? ServiceFeatureTable ?: return emptyList()
135
+ return serviceTable.applyEdits().getOrThrow().map {
136
+ mapOf("objectId" to it.objectId, "completedWithErrors" to it.completedWithErrors)
137
+ }
138
+ }
139
+
140
+ /** Discards all pending local edits (since the last `applyEdits`). */
141
+ suspend fun undoLocalEdits() {
142
+ (table as? ServiceFeatureTable)?.undoLocalEdits()?.getOrThrow()
143
+ }
144
+
145
+ /** Queries features related to `objectId` (across all relationships); returns groups by relationship. */
146
+ suspend fun queryRelatedFeatures(objectId: Long): List<Map<String, Any?>> {
147
+ val arcgisTable = table as? ArcGISFeatureTable ?: return emptyList()
148
+ val feature = featureByObjectId(objectId) as? ArcGISFeature ?: return emptyList()
149
+ return arcgisTable.queryRelatedFeatures(feature).getOrThrow().map { result ->
150
+ mapOf(
151
+ "relationshipId" to (result.relationshipInfo?.id ?: -1L),
152
+ "features" to result.map { serializeFeature(it) },
153
+ )
154
+ }
155
+ }
156
+
157
+ /** Queries the attachments for the feature with `objectId`; returns `[{id, name, contentType, size}]`. */
158
+ suspend fun queryAttachments(objectId: Long): List<Map<String, Any?>> {
159
+ val feature = featureByObjectId(objectId) as? ArcGISFeature ?: return emptyList()
160
+ return feature.fetchAttachments().getOrThrow().map { attachment ->
161
+ mapOf(
162
+ "id" to attachment.id,
163
+ "name" to attachment.name,
164
+ "contentType" to attachment.contentType,
165
+ "size" to attachment.size,
166
+ )
167
+ }
168
+ }
169
+
170
+ /** Decodes `dataBase64`, adds it as an attachment to the feature, then persists the edit. */
171
+ suspend fun addAttachment(objectId: Long, name: String, contentType: String, dataBase64: String) {
172
+ val feature = featureByObjectId(objectId) as? ArcGISFeature ?: return
173
+ val bytes = Base64.decode(dataBase64, Base64.NO_WRAP)
174
+ feature.addAttachment(name, contentType, bytes).getOrThrow()
175
+ persistEdits()
176
+ }
177
+
178
+ /** Fetches the binary data for the attachment with `attachmentId` and returns it as base64. */
179
+ suspend fun fetchAttachment(objectId: Long, attachmentId: Long): String {
180
+ val feature = featureByObjectId(objectId) as? ArcGISFeature
181
+ ?: error("Feature not found: $objectId")
182
+ val attachment = feature.fetchAttachments().getOrThrow()
183
+ .firstOrNull { it.id == attachmentId }
184
+ ?: error("Attachment not found: $attachmentId")
185
+ val bytes = attachment.fetchData().getOrThrow()
186
+ return Base64.encodeToString(bytes, Base64.NO_WRAP)
187
+ }
188
+
189
+ /** Deletes the attachment with `attachmentId` from the feature with `objectId`, then persists. */
190
+ suspend fun deleteAttachment(objectId: Long, attachmentId: Long) {
191
+ val feature = featureByObjectId(objectId) as? ArcGISFeature
192
+ ?: error("Feature not found: $objectId")
193
+ val attachment = feature.fetchAttachments().getOrThrow()
194
+ .firstOrNull { it.id == attachmentId }
195
+ ?: error("Attachment not found: $attachmentId")
196
+ feature.deleteAttachment(attachment).getOrThrow()
197
+ persistEdits()
198
+ }
199
+
200
+ /** Decodes `dataBase64` and updates the attachment with `attachmentId`, then persists. */
201
+ suspend fun updateAttachment(
202
+ objectId: Long, attachmentId: Long,
203
+ name: String, contentType: String, dataBase64: String,
204
+ ) {
205
+ val feature = featureByObjectId(objectId) as? ArcGISFeature
206
+ ?: error("Feature not found: $objectId")
207
+ val attachment = feature.fetchAttachments().getOrThrow()
208
+ .firstOrNull { it.id == attachmentId }
209
+ ?: error("Attachment not found: $attachmentId")
210
+ val bytes = Base64.decode(dataBase64, Base64.NO_WRAP)
211
+ feature.updateAttachment(attachment, name, contentType, bytes).getOrThrow()
112
212
  persistEdits()
113
213
  }
114
214
 
@@ -314,3 +414,85 @@ class GroupLayerRef(appContext: AppContext) : LayerRef(appContext) {
314
414
 
315
415
  override fun applyProps(changed: Map<String, Any?>) = applyCommonProps(changed)
316
416
  }
417
+
418
+ /**
419
+ * In-memory [FeatureCollectionLayer] — a layer built from a client-side schema (`fields`) and
420
+ * `features` (no service). Features become graphics in a [FeatureCollectionTable].
421
+ */
422
+ class FeatureCollectionLayerRef(appContext: AppContext, props: Map<String, Any?>) : LayerRef(appContext) {
423
+ private val table: FeatureCollectionTable = run {
424
+ val fields = (props["fields"] as? List<*> ?: emptyList<Any?>())
425
+ .mapNotNull { (it as? Map<*, *>)?.let(::makeFeatureCollectionField) }
426
+ val graphics = (props["features"] as? List<*> ?: emptyList<Any?>()).mapNotNull { spec ->
427
+ val s = spec as? Map<*, *> ?: return@mapNotNull null
428
+ Graphic().apply {
429
+ geometry = (s["geometry"] as? Map<*, *>)?.let { geometryFromDict(it) }
430
+ (s["attributes"] as? Map<*, *>)?.forEach { (k, v) -> attributes[k.toString()] = v }
431
+ }
432
+ }
433
+ FeatureCollectionTable(graphics, fields).apply {
434
+ renderer = (props["renderer"] as? Map<*, *>)?.let { buildRenderer(it) }
435
+ }
436
+ }
437
+ override val layer: Layer = FeatureCollectionLayer(FeatureCollection().apply { tables.add(table) })
438
+
439
+ override fun applyProps(changed: Map<String, Any?>) {
440
+ applyCommonProps(changed)
441
+ if (changed.containsKey("renderer")) {
442
+ table.renderer = (changed["renderer"] as? Map<*, *>)?.let { buildRenderer(it) }
443
+ }
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Operational layer loaded from a local GeoPackage (`.gpkg`) file. Opens the GeoPackage
449
+ * asynchronously, picks the feature table by [tableName] (or the first when null), wraps it in a
450
+ * [FeatureLayer], and attaches it to the placeholder [GroupLayer] once ready.
451
+ */
452
+ class GeoPackageLayerRef(appContext: AppContext, path: String, tableName: String?) :
453
+ LayerRef(appContext) {
454
+
455
+ private val group = GroupLayer(emptyList())
456
+ override val layer: Layer = group
457
+ private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
458
+
459
+ init {
460
+ scope.launch {
461
+ val pkg = GeoPackage(path)
462
+ pkg.load().onFailure { return@launch }
463
+ val tables = pkg.geoPackageFeatureTables
464
+ if (tables.isEmpty()) return@launch
465
+ val table = if (tableName != null) {
466
+ tables.firstOrNull { it.tableName == tableName } ?: return@launch
467
+ } else {
468
+ tables[0]
469
+ }
470
+ val featureLayer = FeatureLayer.createWithFeatureTable(table)
471
+ group.layers.add(featureLayer)
472
+ }
473
+ }
474
+
475
+ override fun applyProps(changed: Map<String, Any?>) = applyCommonProps(changed)
476
+ }
477
+
478
+ private fun makeFeatureCollectionField(d: Map<*, *>): Field {
479
+ val name = d["name"] as? String ?: ""
480
+ return Field(
481
+ featureCollectionFieldType(d["type"] as? String),
482
+ name,
483
+ d["alias"] as? String ?: name,
484
+ (d["length"] as? Number)?.toInt() ?: 255,
485
+ null,
486
+ true,
487
+ true,
488
+ )
489
+ }
490
+
491
+ private fun featureCollectionFieldType(value: String?): FieldType = when (value) {
492
+ "int16" -> FieldType.Int16
493
+ "integer" -> FieldType.Int32
494
+ "long" -> FieldType.Int64
495
+ "double" -> FieldType.Float64
496
+ "date" -> FieldType.Date
497
+ else -> FieldType.Text
498
+ }