diff --git a/samples/generate-geodatabase-replica-from-feature-service/README.md b/samples/generate-geodatabase-replica-from-feature-service/README.md index 38a11ff8e..52b26d0e3 100644 --- a/samples/generate-geodatabase-replica-from-feature-service/README.md +++ b/samples/generate-geodatabase-replica-from-feature-service/README.md @@ -16,9 +16,9 @@ Zoom to any extent. Then click the generate button to generate a geodatabase of 1. Create a `GeodatabaseSyncTask` with the URL of the feature service and load it. 2. Create `GenerateGeodatabaseParameters` specifying the extent and whether to include attachments. -3. Create a `GenerateGeodatabaseJob` with `geodatabaseSyncTask.generateGeodatabaseAsync(parameters, downloadPath)`. Start the job with `job.start()`. +3. Create a `GenerateGeodatabaseJob` with `geodatabaseSyncTask.createGenerateGeodatabaseJob(parameters, downloadPath)`. Start the job with `job.start()`. 4. When the job is done, `job.result()` will return the geodatabase. Inside the geodatabase are feature tables which can be used to add feature layers to the map. -5. Call `syncTask.unregisterGeodatabaseAsync(geodatabase)` after generation when you're not planning on syncing changes to the service. +5. Call `geodatabaseSyncTask.unregisterGeodatabase(geodatabase)` after generation when you're not planning on syncing changes to the service. ## Relevant API @@ -27,6 +27,10 @@ Zoom to any extent. Then click the generate button to generate a geodatabase of * Geodatabase * GeodatabaseSyncTask +## Additional information + +This sample uses the GeoView-Compose Toolkit module to be able to implement a composable SceneView. + ## Tags -disconnected, local geodatabase, offline, replica, sync +disconnected, geoview-compose, local geodatabase, offline, replica, sync, toolkit diff --git a/samples/generate-geodatabase-replica-from-feature-service/README.metadata.json b/samples/generate-geodatabase-replica-from-feature-service/README.metadata.json index 1d7dcb0be..786223a33 100644 --- a/samples/generate-geodatabase-replica-from-feature-service/README.metadata.json +++ b/samples/generate-geodatabase-replica-from-feature-service/README.metadata.json @@ -8,17 +8,21 @@ ], "keywords": [ "disconnected", + "geoview-compose", "local geodatabase", "offline", "replica", "sync", + "toolkit", "GenerateGeodatabaseJob", "GenerateGeodatabaseParameters", "Geodatabase", "GeodatabaseSyncTask" ], "language": "kotlin", - "redirect_from": "/android/latest/sample-code/generate-geodatabase.htm", + "redirect_from": [ + "/android/latest/sample-code/generate-geodatabase.htm" + ], "relevant_apis": [ "GenerateGeodatabaseJob", "GenerateGeodatabaseParameters", @@ -26,7 +30,9 @@ "GeodatabaseSyncTask" ], "snippets": [ - "src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/MainActivity.kt" + "src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/components/GenerateGeodatabaseReplicaFromFeatureServiceViewModel.kt", + "src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/MainActivity.kt", + "src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/screens/GenerateGeodatabaseReplicaFromFeatureServiceScreen.kt" ], "title": "Generate geodatabase replica from feature service" -} \ No newline at end of file +} diff --git a/samples/generate-geodatabase-replica-from-feature-service/build.gradle.kts b/samples/generate-geodatabase-replica-from-feature-service/build.gradle.kts index e4871efcd..2e727c011 100644 --- a/samples/generate-geodatabase-replica-from-feature-service/build.gradle.kts +++ b/samples/generate-geodatabase-replica-from-feature-service/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.arcgismaps.android.library) + alias(libs.plugins.arcgismaps.android.library.compose) alias(libs.plugins.arcgismaps.kotlin.sample) alias(libs.plugins.gradle.secrets) } @@ -11,9 +12,7 @@ secrets { android { namespace = "com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice" - // For view based samples buildFeatures { - dataBinding = true buildConfig = true } } diff --git a/samples/generate-geodatabase-replica-from-feature-service/generate-geodatabase-replica-from-feature-service.png b/samples/generate-geodatabase-replica-from-feature-service/generate-geodatabase-replica-from-feature-service.png index 90ef8e642..ca3fa557b 100644 Binary files a/samples/generate-geodatabase-replica-from-feature-service/generate-geodatabase-replica-from-feature-service.png and b/samples/generate-geodatabase-replica-from-feature-service/generate-geodatabase-replica-from-feature-service.png differ diff --git a/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/MainActivity.kt b/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/MainActivity.kt index c0d4a7b40..440e65f65 100644 --- a/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/MainActivity.kt +++ b/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/MainActivity.kt @@ -18,316 +18,38 @@ package com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice import android.os.Bundle -import android.util.Log -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.databinding.DataBindingUtil -import androidx.lifecycle.lifecycleScope +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable import com.arcgismaps.ApiKey import com.arcgismaps.ArcGISEnvironment -import com.arcgismaps.Color -import com.arcgismaps.data.Geodatabase -import com.arcgismaps.data.ServiceFeatureTable -import com.arcgismaps.geometry.Envelope -import com.arcgismaps.geometry.SpatialReference -import com.arcgismaps.mapping.ArcGISMap -import com.arcgismaps.mapping.BasemapStyle -import com.arcgismaps.mapping.layers.FeatureLayer -import com.arcgismaps.mapping.symbology.SimpleLineSymbol -import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle -import com.arcgismaps.mapping.view.Graphic -import com.arcgismaps.mapping.view.GraphicsOverlay -import com.arcgismaps.mapping.view.ScreenCoordinate -import com.arcgismaps.tasks.geodatabase.GenerateGeodatabaseJob -import com.arcgismaps.tasks.geodatabase.GeodatabaseSyncTask -import com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.databinding.GenerateGeodatabaseReplicaFromFeatureServiceActivityMainBinding -import com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.databinding.GenerateGeodatabaseDialogLayoutBinding -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar -import kotlinx.coroutines.launch +import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme +import com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.screens.GenerateGeodatabaseReplicaFromFeatureServiceScreen +import com.esri.arcgismaps.sample.sampleslib.BuildConfig -class MainActivity : AppCompatActivity() { - - // set up data binding for the activity - private val activityMainBinding: GenerateGeodatabaseReplicaFromFeatureServiceActivityMainBinding by lazy { - DataBindingUtil.setContentView(this, R.layout.generate_geodatabase_replica_from_feature_service_activity_main) - } - - // setup data binding for the mapview - private val mapView by lazy { - activityMainBinding.mapView - } - - // starts the geodatabase replica process - private val generateButton by lazy { - activityMainBinding.generateButton - } - - private val resetButton by lazy { - activityMainBinding.resetButton - } - - // shows the geodatabase loading progress - private val progressDialog by lazy { - GenerateGeodatabaseDialogLayoutBinding.inflate(layoutInflater) - } - - // local file path to the geodatabase - private val geodatabaseFilePath by lazy { - getExternalFilesDir(null)?.path + getString(R.string.portland_trees_geodatabase_file) - } - - // keep track of the geodatabase replica generated by the feature service - private var geodatabase: Geodatabase? = null - - private val downloadArea: Graphic = Graphic() - - // create a Trees FeatureLayer using the first layer of the ServiceFeatureTable - private val featureLayer: FeatureLayer by lazy { - FeatureLayer.createWithFeatureTable( - featureTable = ServiceFeatureTable( - uri = application.getString(R.string.feature_server_url) + "/0" - ) - ) - } - - // creates a graphic overlay - private val graphicsOverlay: GraphicsOverlay = GraphicsOverlay() +class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // authentication with an API key or named user is // required to access basemaps and other location services ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN) - lifecycle.addObserver(mapView) - - // create and add a map with a Topographic basemap style - val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply { - operationalLayers.add(featureLayer) - } - // set the max map extents to that of the feature service - // representing portland area - map.maxExtent = Envelope( - -13687689.2185849, - 5687273.88331375, - -13622795.3756647, - 5727520.22085841, - spatialReference = SpatialReference.webMercator() - ) - // configure mapview assignments - mapView.apply { - this.map = map - // add the graphics overlay to display the boundary - graphicsOverlays.add(graphicsOverlay) - } - - // create a geodatabase sync task with the feature service url - // This feature service shows a web map of portland street trees, - // their attributes, as well as related inspection information - val geodatabaseSyncTask = GeodatabaseSyncTask(getString(R.string.feature_server_url)) - - // set the button's onClickListener - generateButton.setOnClickListener { - // start the geodatabase generation process - generateGeodatabase(geodatabaseSyncTask, map, downloadArea.geometry?.extent) - } - - resetButton.setOnClickListener { - // clear any layers already on the map - map.operationalLayers.clear() - // clear all symbols drawn - graphicsOverlay.graphics.clear() - // add the download boundary - graphicsOverlay.graphics.add(downloadArea) - // add back the feature layer - map.operationalLayers.add(featureLayer) - // close the current geodatabase, if a replica was already generated - geodatabase?.close() - // show generate button - generateButton.isEnabled = true - resetButton.isEnabled = false - } - - lifecycleScope.launch { - // show the error and return if map load failed - map.load().onFailure { - showError("Unable to load map") - return@launch - } - geodatabaseSyncTask.load().onFailure { - // if the metadata load fails, show the error and return - showError("Failed to fetch geodatabase metadata") - return@launch + setContent { + SampleAppTheme { + GenerateGeodatabaseReplicaFromFeatureServiceApp() } - - // show download area once map is loaded - updateDownloadArea() - - // enable the generate button since the task is now loaded - generateButton.isEnabled = true - - // create a symbol to show a box around the extent we want to download - downloadArea.symbol = SimpleLineSymbol(SimpleLineSymbolStyle.Solid, Color.red, 2F) - // add the graphic to the graphics overlay when it is created - graphicsOverlay.graphics.add(downloadArea) - // update the download area on viewpoint change - mapView.viewpointChanged.collect { - updateDownloadArea() - } - } - } - - /** - * Displays a red border on the map to signify the [downloadArea] - */ - private fun updateDownloadArea() { - // define screen area to create replica - val minScreenPoint = ScreenCoordinate(200.0, 200.0) - val maxScreenPoint = ScreenCoordinate( - mapView.measuredWidth - 200.0, - mapView.measuredHeight - 200.0 - ) - // convert screen points to map points - val minPoint = mapView.screenToLocation(minScreenPoint) - val maxPoint = mapView.screenToLocation(maxScreenPoint) - // use the points to define and return an envelope - if (minPoint != null && maxPoint != null) { - val envelope = Envelope(minPoint, maxPoint) - downloadArea.geometry = envelope - } - } - - /** - * Starts a [geodatabaseSyncTask] with the given [map] and [extents], - * runs a GenerateGeodatabaseJob and saves the geodatabase file into local storage - */ - private fun generateGeodatabase( - geodatabaseSyncTask: GeodatabaseSyncTask, - map: ArcGISMap, - extents: Envelope? - ) { - if (extents == null) { - return showError("Download area extent is null") - } - - lifecycleScope.launch { - // create generate geodatabase parameters for the selected extents - val defaultParameters = - geodatabaseSyncTask.createDefaultGenerateGeodatabaseParameters(extents).getOrElse { - // show the error and return if the task fails - showError("Error creating geodatabase parameters") - return@launch - }.apply { - // set the parameters to only create a replica of the Trees (0) layer - layerOptions.removeIf { layerOptions -> - layerOptions.layerId != 0L - } - } - - // set return attachments option to false - // indicates if any attachments are added to the geodatabase from the feature service - defaultParameters.returnAttachments = false - // create a generate geodatabase job - geodatabaseSyncTask.createGenerateGeodatabaseJob(defaultParameters, geodatabaseFilePath) - .run { - // create a dialog to show the jobs progress - val materialDialogBuilder = createProgressDialog(this) - - // show the dialog and obtain a reference to it - val jobProgressDialog = materialDialogBuilder.show() - - // launch a progress collector to display progress - launch { - progress.collect { value -> - // update the progress bar and progress text - progressDialog.progressBar.progress = value - progressDialog.progressTextView.text = value.toString() - } - } - // start the generateGeodatabase job - start() - // if the job completed successfully, get the geodatabase from the result - val geodatabase = result().getOrElse { - // show an error and return if job failed - showError("Error fetching geodatabase: ${it.message}") - // dismiss the dialog - jobProgressDialog.dismiss() - return@launch - } - - // load and display the geodatabase - loadGeodatabase(geodatabase, map) - // dismiss the dialog view - jobProgressDialog.dismiss() - // unregister since we are not syncing - geodatabaseSyncTask.unregisterGeodatabase(geodatabase) - // show reset button as the task is now complete - generateButton.isEnabled = false - resetButton.isEnabled = true - } - } - } - - /** - * Loads the [replicaGeodatabase] and renders the feature layers on to the [map] - */ - private suspend fun loadGeodatabase(replicaGeodatabase: Geodatabase, map: ArcGISMap) { - // clear any layers already on the map - map.operationalLayers.clear() - // clear all symbols drawn - graphicsOverlay.graphics.clear() - - // load the geodatabase - replicaGeodatabase.load().onFailure { - // if the load failed, show the error and return - showError("Error loading geodatabase") - return - } - - // add all of the geodatabase feature tables to the map as feature layers - map.operationalLayers += replicaGeodatabase.featureTables.map { featureTable -> - FeatureLayer.createWithFeatureTable(featureTable) } - - // keep track of the geodatabase to close it before generating a new replica - geodatabase = replicaGeodatabase } - /** - * Creates a new alert dialog using the progressDialog and provides - * GenerateGeodatabaseJob cancellation on dialog cancellation - * - * @param generateGeodatabaseJob the job to cancel - * - * @return returns an alert dialog - */ - private fun createProgressDialog(generateGeodatabaseJob: GenerateGeodatabaseJob): MaterialAlertDialogBuilder { - // build and return a new alert dialog - return MaterialAlertDialogBuilder(this).apply { - // setting it title - setTitle(getString(R.string.dialog_title)) - // allow it to be cancellable - setCancelable(false) - // sets negative button configuration - setNegativeButton("Cancel") { _, _ -> - // cancels the generateGeodatabaseJob - lifecycleScope.launch { - generateGeodatabaseJob.cancel() - } - } - // removes parent of the progressDialog layout, if previously assigned - progressDialog.root.parent?.let { parent -> - (parent as ViewGroup).removeAllViews() - } - // set the progressDialog Layout to this alert dialog - setView(progressDialog.root) + @Composable + private fun GenerateGeodatabaseReplicaFromFeatureServiceApp() { + Surface(color = MaterialTheme.colorScheme.background) { + GenerateGeodatabaseReplicaFromFeatureServiceScreen( + sampleName = getString(R.string.generate_geodatabase_replica_from_feature_service_app_name) + ) } } - - private fun showError(message: String) { - Log.e(localClassName, message) - Snackbar.make(mapView, message, Snackbar.LENGTH_SHORT).show() - } } diff --git a/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/components/GenerateGeodatabaseReplicaFromFeatureServiceViewModel.kt b/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/components/GenerateGeodatabaseReplicaFromFeatureServiceViewModel.kt new file mode 100644 index 000000000..2f9dcf58b --- /dev/null +++ b/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/components/GenerateGeodatabaseReplicaFromFeatureServiceViewModel.kt @@ -0,0 +1,320 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.components + +import android.app.Application +import androidx.compose.ui.unit.IntSize +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.arcgismaps.data.Geodatabase +import com.arcgismaps.data.ServiceFeatureTable +import com.arcgismaps.geometry.Envelope +import com.arcgismaps.geometry.SpatialReference +import com.arcgismaps.mapping.ArcGISMap +import com.arcgismaps.mapping.BasemapStyle +import com.arcgismaps.mapping.layers.FeatureLayer +import com.arcgismaps.mapping.symbology.SimpleLineSymbol +import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle +import com.arcgismaps.mapping.view.Graphic +import com.arcgismaps.mapping.view.GraphicsOverlay +import com.arcgismaps.mapping.view.ScreenCoordinate +import com.arcgismaps.tasks.geodatabase.GenerateGeodatabaseJob +import com.arcgismaps.tasks.geodatabase.GeodatabaseSyncTask +import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy +import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.io.File + +private const val FEATURE_SERVICE_URL = + "https://services2.arcgis.com/ZQgQTuoyBrtmoGdP/arcgis/rest/services/Mobile_Data_Collection_WFL1/FeatureServer" + +class GenerateGeodatabaseReplicaFromFeatureServiceViewModel( + private val application: Application +) : AndroidViewModel(application) { + // graphics overlay to display the download area + val graphicsOverlay = GraphicsOverlay() + + // symbol used to show a box around the extent we want to download + private val downloadArea: Graphic = Graphic( + symbol = SimpleLineSymbol( + style = SimpleLineSymbolStyle.Solid, + color = com.arcgismaps.Color.red, + width = 2F + ) + ) + + // a Trees FeatureLayer, using the first layer of the ServiceFeatureTable + private val featureLayer: FeatureLayer by lazy { + FeatureLayer.createWithFeatureTable( + featureTable = ServiceFeatureTable( + uri = "$FEATURE_SERVICE_URL/0" + ) + ) + } + + // create a MapViewProxy, used to convert screen points to map points + val mapViewProxy = MapViewProxy() + + // the dimensions of the MapView + private var mapViewSize = IntSize(0, 0) + + // create a map with a Topographic basemap style + val arcGISMap = ArcGISMap(BasemapStyle.ArcGISTopographic).apply { + // set the max extent to that of the feature service representing an area of Portland + maxExtent = Envelope( + -13687689.2185849, + 5687273.88331375, + -13622795.3756647, + 5727520.22085841, + spatialReference = SpatialReference.webMercator() + ) + + // add the FeatureLayer to the map + operationalLayers.add(featureLayer) + } + + // a message dialog view model for handling error messages + val messageDialogVM = MessageDialogViewModel() + + // state flow to expose the current UI state + private val _uiStateFlow = MutableStateFlow(UiState(appStatus = AppStatus.STARTING)) + val uiStateFlow = _uiStateFlow.asStateFlow() + + // create a GeodatabaseSyncTask with the URL of the feature service + private var geodatabaseSyncTask = GeodatabaseSyncTask(FEATURE_SERVICE_URL) + + // job used to generate the geodatabase replica + private var generateGeodatabaseJob: GenerateGeodatabaseJob? = null + + // the geodatabase replica + private var geodatabase: Geodatabase? = null + + init { + // add the download graphic to the graphics overlay + graphicsOverlay.graphics.add(downloadArea) + + viewModelScope.launch { + // load the map + arcGISMap.load().onSuccess { + // load the GeodatabaseSyncTask + geodatabaseSyncTask.load().onSuccess { + _uiStateFlow.value = UiState(appStatus = AppStatus.READY_TO_GENERATE) + }.onFailure { error -> + messageDialogVM.showMessageDialog( + title = "Failed to load GeodatabaseSyncTask", + description = error.message.toString() + ) + } + }.onFailure { error -> + messageDialogVM.showMessageDialog( + title = "Failed to load map", + description = error.message.toString() + ) + } + } + } + + /** + * Function called when the map view size is known. + */ + fun updateMapViewSize(size: IntSize) { + mapViewSize = size + } + + /** + * Use map view's size to determine dimensions of the area to download. + */ + fun calculateDownloadArea() { + // upper left corner of the area to take offline + val minScreenPoint = ScreenCoordinate(200.0, 200.0) + + // lower right corner of the downloaded area + val maxScreenPoint = ScreenCoordinate( + x = mapViewSize.width - 200.0, + y = mapViewSize.height - 200.0 + ) + + // convert screen points to map points + val minPoint = mapViewProxy.screenToLocationOrNull(minScreenPoint) + val maxPoint = mapViewProxy.screenToLocationOrNull(maxScreenPoint) + + // set the download area's geometry using the calculated bounds + if (minPoint != null && maxPoint != null) { + val envelope = Envelope(minPoint, maxPoint) + downloadArea.geometry = envelope + } + } + + /** + * Reset the map to its original state. + */ + fun resetMap() { + // clear any layers and symbols already on the map + arcGISMap.operationalLayers.clear() + graphicsOverlay.graphics.clear() + // add the download area boundary + graphicsOverlay.graphics.add(downloadArea) + // add back the feature layer + arcGISMap.operationalLayers.add(featureLayer) + // close the current geodatabase, if a replica was already generated + geodatabase?.close() + _uiStateFlow.value = UiState(appStatus = AppStatus.READY_TO_GENERATE) + } + + /** + * Generate the geodatabase replica. + */ + fun generateGeodatabaseReplica() { + _uiStateFlow.value = UiState(appStatus = AppStatus.GENERATING, jobProgress = 0) + + val offlineGeodatabasePath = + application.getExternalFilesDir(null)?.path + "/portland_trees_gdb.geodatabase" + + // delete any offline geodatabase already in the cache + File(offlineGeodatabasePath).deleteRecursively() + + // get the geometry of the download area + val geometry = downloadArea.geometry ?: return messageDialogVM.showMessageDialog( + title = "Could not get geometry of the downloadArea" + ) + + viewModelScope.launch(Dispatchers.Main) { + // create GenerateGeodatabaseParameters for the selected extent + val parameters = + geodatabaseSyncTask.createDefaultGenerateGeodatabaseParameters(geometry).getOrElse { + messageDialogVM.showMessageDialog( + title = "Error creating geodatabase parameters", + description = it.message.toString() + ) + return@launch + }.apply { + // modify the parameters to only include the Trees (0) layer + layerOptions.removeIf { layerOptions -> + layerOptions.layerId != 0L + } + } + + // we don't need attachments + parameters.returnAttachments = false + + // create a GenerateGeodatabaseJob + val job = geodatabaseSyncTask.createGenerateGeodatabaseJob( + parameters = parameters, + pathToGeodatabaseFile = offlineGeodatabasePath + ) + + // stash the job so the cancel function can use it + generateGeodatabaseJob = job + + // run the job + runGenerateGeodatabaseJob(job) + } + } + + /** + * Run the [job], showing the progress dialog and displaying the resultant data on the map. + */ + private suspend fun runGenerateGeodatabaseJob(job: GenerateGeodatabaseJob) { + // create a flow-collection for the job's progress + viewModelScope.launch(Dispatchers.Main) { + job.progress.collect { progress -> + _uiStateFlow.value = UiState(appStatus = AppStatus.GENERATING, jobProgress = progress) + } + } + + // start the job and wait for Job result + job.start() + job.result().onSuccess { geodatabase -> + // display the data + loadGeodatabaseAndAddToMap(geodatabase) + + // unregister the geodatabase since we will not sync changes to the service + geodatabaseSyncTask.unregisterGeodatabase(geodatabase).getOrElse { + messageDialogVM.showMessageDialog( + title = "Failed to unregister the geodatabase", + description = it.message.toString() + ) + } + }.onFailure { error -> + _uiStateFlow.value = UiState(appStatus = AppStatus.READY_TO_GENERATE) + messageDialogVM.showMessageDialog( + title = "Error generating geodatabase", + description = error.message.toString() + ) + } + } + + /** + * Loads the [replicaGeodatabase] and renders the feature layers on to the map. + */ + private suspend fun loadGeodatabaseAndAddToMap(replicaGeodatabase: Geodatabase) { + // clear any layers and symbols already on the map + arcGISMap.operationalLayers.clear() + graphicsOverlay.graphics.clear() + + // load the geodatabase + replicaGeodatabase.load().onSuccess { + // add all the geodatabase feature tables to the map as feature layers + arcGISMap.operationalLayers += replicaGeodatabase.featureTables.map { featureTable -> + FeatureLayer.createWithFeatureTable(featureTable) + } + // keep track of the geodatabase to close it before generating a new replica + geodatabase = replicaGeodatabase + _uiStateFlow.value = UiState(appStatus = AppStatus.REPLICA_DISPLAYED) + }.onFailure { error -> + _uiStateFlow.value = UiState(appStatus = AppStatus.READY_TO_GENERATE) + messageDialogVM.showMessageDialog( + title = "Error loading geodatabase", + description = error.message.toString() + ) + } + } + + /** + * Cancel the current [generateGeodatabaseJob]. + */ + fun cancelOfflineGeodatabaseJob() { + viewModelScope.launch(Dispatchers.IO) { + generateGeodatabaseJob?.cancel() + } + _uiStateFlow.value = UiState(appStatus = AppStatus.READY_TO_GENERATE) + } + + override fun onCleared() { + super.onCleared() + // close the current geodatabase, if any, to release internal resources and file locks + geodatabase?.close() + } +} + +/** + * Data class representing the UI state. + */ +data class UiState( + val appStatus: AppStatus, + val jobProgress: Int = 0 +) + +enum class AppStatus { + STARTING, + READY_TO_GENERATE, + GENERATING, + REPLICA_DISPLAYED +} diff --git a/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/screens/GenerateGeodatabaseReplicaFromFeatureServiceScreen.kt b/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/screens/GenerateGeodatabaseReplicaFromFeatureServiceScreen.kt new file mode 100644 index 000000000..9ffb89cc7 --- /dev/null +++ b/samples/generate-geodatabase-replica-from-feature-service/src/main/java/com/esri/arcgismaps/sample/generategeodatabasereplicafromfeatureservice/screens/GenerateGeodatabaseReplicaFromFeatureServiceScreen.kt @@ -0,0 +1,134 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.getString +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.arcgismaps.LoadStatus +import com.arcgismaps.toolkit.geoviewcompose.MapView +import com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.components.GenerateGeodatabaseReplicaFromFeatureServiceViewModel +import com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.R +import com.esri.arcgismaps.sample.generategeodatabasereplicafromfeatureservice.components.AppStatus +import com.esri.arcgismaps.sample.sampleslib.components.JobLoadingDialog +import com.esri.arcgismaps.sample.sampleslib.components.MessageDialog +import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar + +/** + * Main screen layout for the sample app. + */ +@Composable +fun GenerateGeodatabaseReplicaFromFeatureServiceScreen(sampleName: String) { + val application = LocalContext.current.applicationContext + val mapViewModel: GenerateGeodatabaseReplicaFromFeatureServiceViewModel = viewModel() + val uiState by mapViewModel.uiStateFlow.collectAsStateWithLifecycle() + Scaffold( + topBar = { SampleTopAppBar(title = sampleName) }, + content = { + Column( + modifier = Modifier + .fillMaxSize() + .padding(it), + ) { + MapView( + modifier = Modifier + .fillMaxSize() + .weight(1f) + // retrieve the size of the Composable MapView + .onSizeChanged { size -> + mapViewModel.updateMapViewSize(size) + }, + arcGISMap = mapViewModel.arcGISMap, + graphicsOverlays = listOf(mapViewModel.graphicsOverlay), + mapViewProxy = mapViewModel.mapViewProxy, + onLayerViewStateChanged = { + // on launch, calculate the download area + if (mapViewModel.arcGISMap.loadStatus.value == LoadStatus.Loaded) { + mapViewModel.calculateDownloadArea() + } + }, + onViewpointChangedForCenterAndScale = { + // recalculate the download area when viewpoint changes + if (mapViewModel.arcGISMap.loadStatus.value == LoadStatus.Loaded) { + mapViewModel.calculateDownloadArea() + } + }, + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Button( + onClick = { + mapViewModel.resetMap() + }, + enabled = uiState.appStatus == AppStatus.REPLICA_DISPLAYED + ) { + Text(text = getString(application, R.string.reset_map)) + } + + Button( + onClick = { + mapViewModel.generateGeodatabaseReplica() + }, + enabled = uiState.appStatus == AppStatus.READY_TO_GENERATE + ) { + Text(text = getString(application, R.string.generate_button_text)) + } + } + + // display progress dialog while generating a geodatabase replica + if (uiState.appStatus == AppStatus.GENERATING) { + JobLoadingDialog( + title = getString(application, R.string.dialog_title), + progress = uiState.jobProgress, + cancelJobRequest = { mapViewModel.cancelOfflineGeodatabaseJob() } + ) + } + + // display a dialog if the sample encounters an error + mapViewModel.messageDialogVM.apply { + if (dialogStatus) { + MessageDialog( + title = messageTitle, + description = messageDescription, + onDismissRequest = ::dismissDialog + ) + } + } + } + } + ) +} diff --git a/samples/generate-geodatabase-replica-from-feature-service/src/main/res/layout/generate_geodatabase_dialog_layout.xml b/samples/generate-geodatabase-replica-from-feature-service/src/main/res/layout/generate_geodatabase_dialog_layout.xml deleted file mode 100644 index bb1aeb60e..000000000 --- a/samples/generate-geodatabase-replica-from-feature-service/src/main/res/layout/generate_geodatabase_dialog_layout.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/samples/generate-geodatabase-replica-from-feature-service/src/main/res/layout/generate_geodatabase_replica_from_feature_service_activity_main.xml b/samples/generate-geodatabase-replica-from-feature-service/src/main/res/layout/generate_geodatabase_replica_from_feature_service_activity_main.xml deleted file mode 100644 index fbd959358..000000000 --- a/samples/generate-geodatabase-replica-from-feature-service/src/main/res/layout/generate_geodatabase_replica_from_feature_service_activity_main.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/samples/generate-geodatabase-replica-from-feature-service/src/main/res/values/strings.xml b/samples/generate-geodatabase-replica-from-feature-service/src/main/res/values/strings.xml index 74dacf670..eb01db93a 100644 --- a/samples/generate-geodatabase-replica-from-feature-service/src/main/res/values/strings.xml +++ b/samples/generate-geodatabase-replica-from-feature-service/src/main/res/values/strings.xml @@ -1,8 +1,6 @@ Generate geodatabase replica from feature service - https://services2.arcgis.com/ZQgQTuoyBrtmoGdP/arcgis/rest/services/Mobile_Data_Collection_WFL1/FeatureServer Generate - Fetching result - /portland_trees_gdb.geodatabase + Generating geodatabase replica... Reset map