diff --git a/samples/create-and-edit-geometries/README.md b/samples/create-and-edit-geometries/README.md new file mode 100644 index 000000000..e9937546c --- /dev/null +++ b/samples/create-and-edit-geometries/README.md @@ -0,0 +1 @@ +# Create and edit geometries diff --git a/samples/create-and-edit-geometries/README.metadata.json b/samples/create-and-edit-geometries/README.metadata.json new file mode 100644 index 000000000..4bcd74115 --- /dev/null +++ b/samples/create-and-edit-geometries/README.metadata.json @@ -0,0 +1,19 @@ +{ + "category": "Edit and Manage Data", + "description": "TODO", + "formal_name": "CreateAndEditGeometries", + "ignore": false, + "images": [ + "create-and-edit-geometries.png" + ], + "keywords": [ ], + "language": "kotlin", + "redirect_from": "", + "relevant_apis": [ ], + "snippets": [ + "src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/CreateAndEditGeometriesViewModel.kt", + "src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/CreateAndEditGeometriesScreen.kt", + "src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/MainActivity.kt" + ], + "title": "Create and edit geometries" +} diff --git a/samples/create-and-edit-geometries/build.gradle.kts b/samples/create-and-edit-geometries/build.gradle.kts new file mode 100644 index 000000000..18dc9f4c1 --- /dev/null +++ b/samples/create-and-edit-geometries/build.gradle.kts @@ -0,0 +1,22 @@ +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) +} + +secrets { + // this file doesn't contain secrets, it just provides defaults which can be committed into git. + defaultPropertiesFileName = "secrets.defaults.properties" +} + +android { + namespace = "com.esri.arcgismaps.sample.createandeditgeometries" + buildFeatures { + buildConfig = true + } +} + +dependencies { + // Only module specific dependencies needed here +} diff --git a/samples/create-and-edit-geometries/create-and-edit-geometries.png b/samples/create-and-edit-geometries/create-and-edit-geometries.png new file mode 100644 index 000000000..84b726c2e Binary files /dev/null and b/samples/create-and-edit-geometries/create-and-edit-geometries.png differ diff --git a/samples/create-and-edit-geometries/src/main/AndroidManifest.xml b/samples/create-and-edit-geometries/src/main/AndroidManifest.xml new file mode 100644 index 000000000..7f287fc31 --- /dev/null +++ b/samples/create-and-edit-geometries/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/samples/create-and-edit-geometries/src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/MainActivity.kt b/samples/create-and-edit-geometries/src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/MainActivity.kt new file mode 100644 index 000000000..2c61db02f --- /dev/null +++ b/samples/create-and-edit-geometries/src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/MainActivity.kt @@ -0,0 +1,53 @@ +/* Copyright 2025 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.createandeditgeometries + +import android.os.Bundle +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.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme +import com.esri.arcgismaps.sample.createandeditgeometries.screens.CreateAndEditGeometriesScreen + +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) + + setContent { + SampleAppTheme { + CreateAndEditGeometriesApp() + } + } + } + + @Composable + private fun CreateAndEditGeometriesApp() { + Surface(color = MaterialTheme.colorScheme.background) { + CreateAndEditGeometriesScreen( + sampleName = getString(R.string.create_and_edit_geometries_app_name) + ) + } + } +} diff --git a/samples/create-and-edit-geometries/src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/components/CreateAndEditGeometriesViewModel.kt b/samples/create-and-edit-geometries/src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/components/CreateAndEditGeometriesViewModel.kt new file mode 100644 index 000000000..9fdcd1626 --- /dev/null +++ b/samples/create-and-edit-geometries/src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/components/CreateAndEditGeometriesViewModel.kt @@ -0,0 +1,167 @@ +/* Copyright 2025 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.createandeditgeometries.components + +import android.app.Application +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.unit.dp +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.arcgismaps.geometry.GeometryType +import com.arcgismaps.geometry.Multipoint +import com.arcgismaps.geometry.Point +import com.arcgismaps.geometry.Polygon +import com.arcgismaps.geometry.Polyline +import com.arcgismaps.mapping.ArcGISMap +import com.arcgismaps.mapping.BasemapStyle +import com.arcgismaps.mapping.Viewpoint +import com.arcgismaps.mapping.view.Graphic +import com.arcgismaps.mapping.view.GraphicsOverlay +import com.arcgismaps.mapping.view.SingleTapConfirmedEvent +import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor +import com.arcgismaps.mapping.view.geometryeditor.GeometryEditorStyle +import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy +import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel +import kotlinx.coroutines.launch + +class CreateAndEditGeometriesViewModel(application: Application) : AndroidViewModel(application) { + // create a map with the imagery basemap style + val arcGISMap by mutableStateOf( + ArcGISMap(BasemapStyle.ArcGISImagery).apply { + // a viewpoint centered at the island of Inis MeƔin (Aran Islands) in Ireland + initialViewpoint = Viewpoint( + latitude = 53.08230, + longitude = -9.5920, + scale = 5000.0 + ) + } + ) + + // create a message dialog view model for handling error messages + val messageDialogVM = MessageDialogViewModel() + + // create a MapViewProxy that will be used to identify features in the MapView and set the viewpoint + val mapViewProxy = MapViewProxy() + + // create a geometryEditorStyle + private val geometryEditorStyle = GeometryEditorStyle() + // create a graphic to hold graphics identified on tap + private var identifiedGraphic = Graphic() + // create a graphics overlay + val graphicsOverlay = GraphicsOverlay() + // create a geometry editor + val geometryEditor = GeometryEditor() + + init { + viewModelScope.launch { + // load the map + arcGISMap.load().onFailure { error -> + messageDialogVM.showMessageDialog( + title = "Failed to load map", + description = error.message.toString() + ) + } + } + } + + /** + * Starts the GeometryEditor using the selected [GeometryType]. + */ + fun startEditor(selectedGeometry: GeometryType) { + if (!geometryEditor.isStarted.value) { + geometryEditor.start(selectedGeometry) + } + } + + /** + * Stops the GeometryEditor and updates the identified graphic or calls [createGraphic]. + */ + fun stopEditor() { + // check if there was a previously identified graphic + if (identifiedGraphic.geometry != null) { + // update the identified graphic + identifiedGraphic.geometry = geometryEditor.stop() + // deselect the identified graphic + identifiedGraphic.isSelected = false + } else if (geometryEditor.isStarted.value) { + // create a graphic from the geometry that was being edited + createGraphic() + } + } + + /** + * Creates a graphic from the geometry and adds it to the GraphicsOverlay. + */ + private fun createGraphic() { + // stop the geometry editor and get its final geometry state + val geometry = geometryEditor.stop() + ?: return messageDialogVM.showMessageDialog( + title = "Error!", + description = "Error stopping editing session" + ) + + // create a graphic to represent the new geometry + val graphic = Graphic(geometry) + + // give the graphic an appropriate fill based on the geometry type + when (geometry) { + is Point, is Multipoint -> graphic.symbol = geometryEditorStyle.vertexSymbol + is Polyline -> graphic.symbol = geometryEditorStyle.lineSymbol + is Polygon -> graphic.symbol = geometryEditorStyle.fillSymbol + else -> {} + } + // add the graphic to the graphics overlay + graphicsOverlay.graphics.add(graphic) + // deselect the graphic + graphic.isSelected = false + } + + /** + * Identifies the graphic at the tapped screen coordinate in the provided [singleTapConfirmedEvent] + * and starts the GeometryEditor using the identified graphic's geometry. Hide the BottomSheet on + * [singleTapConfirmedEvent]. + */ + fun identify(singleTapConfirmedEvent: SingleTapConfirmedEvent) { + viewModelScope.launch { + // attempt to identify a graphic at the location the user tapped + val graphicsResult = mapViewProxy.identifyGraphicsOverlays( + screenCoordinate = singleTapConfirmedEvent.screenCoordinate, + tolerance = 10.0.dp, + returnPopupsOnly = false + ).getOrNull() + + if (!geometryEditor.isStarted.value) { + if (graphicsResult != null) { + if (graphicsResult.isNotEmpty()) { + // get the tapped graphic + identifiedGraphic = graphicsResult.first().graphics.first() + // select the graphic + identifiedGraphic.isSelected = true + // start the geometry editor with the identified graphic + identifiedGraphic.geometry?.let { + geometryEditor.start(it) + } + } + } + // reset the identified graphic back to null + identifiedGraphic.geometry = null + } + } + } + +} diff --git a/samples/create-and-edit-geometries/src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/screens/ButtonMenu.kt b/samples/create-and-edit-geometries/src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/screens/ButtonMenu.kt new file mode 100644 index 000000000..e005e2ed0 --- /dev/null +++ b/samples/create-and-edit-geometries/src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/screens/ButtonMenu.kt @@ -0,0 +1,107 @@ +/* Copyright 2025 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.createandeditgeometries.screens + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Create +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.arcgismaps.geometry.GeometryType + +/** + * Composable component to display the menu buttons. + */ +@Composable +fun ButtonMenu( + isGeometryEditorStarted: Boolean, + onStartEditingButtonClick: (GeometryType) -> Unit, + onStopEditingButtonClick: () -> Unit +) { + val rowModifier = Modifier + .padding(12.dp) + .fillMaxWidth() + + Row( + modifier = rowModifier + ) { + var expanded by remember { mutableStateOf(false) } + Box( + modifier = Modifier + ) { + IconButton( + enabled = !isGeometryEditorStarted, + onClick = { expanded = !expanded } + ) { + Icon(imageVector = Icons.Default.Create, contentDescription = "Start") + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + DropdownMenuItem( + text = { Text("Point") }, + onClick = { + onStartEditingButtonClick(GeometryType.Point) + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Multipoint") }, + onClick = { + onStartEditingButtonClick(GeometryType.Multipoint) + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Polyline") }, + onClick = { + onStartEditingButtonClick(GeometryType.Polyline) + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Polygon") }, + onClick = { + onStartEditingButtonClick(GeometryType.Polygon) + expanded = false + } + ) + } + } + IconButton( + enabled = isGeometryEditorStarted, + onClick = { onStopEditingButtonClick() } + ) { + Icon(imageVector = Icons.Default.Check, contentDescription = "Save Edits") + } + } +} diff --git a/samples/create-and-edit-geometries/src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/screens/CreateAndEditGeometriesScreen.kt b/samples/create-and-edit-geometries/src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/screens/CreateAndEditGeometriesScreen.kt new file mode 100644 index 000000000..e80aa770c --- /dev/null +++ b/samples/create-and-edit-geometries/src/main/java/com/esri/arcgismaps/sample/createandeditgeometries/screens/CreateAndEditGeometriesScreen.kt @@ -0,0 +1,75 @@ +/* Copyright 2025 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.createandeditgeometries.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.arcgismaps.toolkit.geoviewcompose.MapView +import com.esri.arcgismaps.sample.createandeditgeometries.components.CreateAndEditGeometriesViewModel +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 CreateAndEditGeometriesScreen(sampleName: String) { + // create a ViewModel to handle MapView interactions + val mapViewModel: CreateAndEditGeometriesViewModel = viewModel() + + Scaffold( + topBar = { SampleTopAppBar(title = sampleName) }, + content = { + Column( + modifier = Modifier + .fillMaxSize() + .padding(it), + ) { + MapView( + modifier = Modifier + .fillMaxSize() + .weight(1f), + arcGISMap = mapViewModel.arcGISMap, + mapViewProxy = mapViewModel.mapViewProxy, + geometryEditor = mapViewModel.geometryEditor, + graphicsOverlays = listOf(mapViewModel.graphicsOverlay), + onSingleTapConfirmed = mapViewModel::identify, + ) + ButtonMenu( + mapViewModel.geometryEditor.isStarted.collectAsStateWithLifecycle().value, + mapViewModel::startEditor, + mapViewModel::stopEditor + ) + mapViewModel.messageDialogVM.apply { + if (dialogStatus) { + MessageDialog( + title = messageTitle, + description = messageDescription, + onDismissRequest = ::dismissDialog + ) + } + } + } + } + ) +} diff --git a/samples/create-and-edit-geometries/src/main/res/values/strings.xml b/samples/create-and-edit-geometries/src/main/res/values/strings.xml new file mode 100644 index 000000000..af1a89fa8 --- /dev/null +++ b/samples/create-and-edit-geometries/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Create and edit geometries +