Learn how to calculate the length and area of geometries.

find length and area

You can calculate the length of a line and determine the area of a polygon using the GeometryEngine. The measurement depends on the coordinate system (or spatial reference) defined for the geometry. If the geometry’s spatial reference is Web Mercator (3857) or WGS84 (4326), use geodesic calculations to account for the curvature of the Earth. Use planar measurements based on Euclidean distances if the spatial reference is a projected coordinate system (anything other than Web Mercator or WGS84).

In this tutorial, you will use the geometry editor tool named VertexTool to draw graphics on the view and the geometry engine to calculate both geodesic and planar lengths and areas to see the difference between the two measurements.

Prerequisites

Before starting this tutorial, you need the following:

  1. An ArcGIS Location Platform or ArcGIS Online account.

  2. A development and deployment environment that meets the system requirements.

  3. An IDE for Android development in Kotlin.

Develop or download

You have two options for completing this tutorial:

  1. Option 1: Develop the code or
  2. Option 2: Download the completed solution

Option 1: Develop the code

Open an Android Studio project

  1. Open the project you created by completing the Display a map tutorial.

  2. Continue with the following instructions to calculate the length and area of geometries.

  3. Modify the old project for use in this new tutorial.

  4. In libs.versions.toml, add a [libraries] entry for the dependency. In the module-level build.gradle.kts (app), add the dependency for the lifecycle view model.

    19 collapsed lines
    [versions]
    arcgisMapsKotlin = "300.0.0"
    # Version numbers added by Android Studio New Project Wizard
    agp = "8.12.1"
    kotlin = "2.2.10"
    coreKtx = "1.17.0"
    junit = "4.13.2"
    junitVersion = "1.3.0"
    espressoCore = "3.7.0"
    lifecycleRuntimeKtx = "2.9.2"
    activityCompose = "1.10.1"
    composeBom = "2025.08.00"
    # Other version numbers
    compileSdk = "36"
    minSdk = "28"
    targetSdk = "36"
    [libraries]
    arcgis-maps-kotlin = { group = "com.esri", name = "arcgis-maps-kotlin", version.ref = "arcgisMapsKotlin" }
    arcgis-maps-kotlin-toolkit-bom = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-bom", version.ref = "arcgisMapsKotlin" }
    arcgis-maps-kotlin-toolkit-geoview-compose = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-geoview-compose" }
    arcgis-maps-kotlin-toolkit-authentication = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-authentication" }
    androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
    junit = { group = "junit", name = "junit", version.ref = "junit" }
    androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
    androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
    androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
    androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
    androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
    15 collapsed lines
    androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
    androidx-ui = { group = "androidx.compose.ui", name = "ui" }
    androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
    androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
    androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
    androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
    androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
    androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
    androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" }
    [plugins]
    android-application = { id = "com.android.application", version.ref = "agp" }
    kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
    kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
  5. Click File > Sync Project with Gradle files.

Add imports

Modify import statements to reference the packages and classes required for this tutorial.

MainScreen.kt
@file:OptIn(ExperimentalMaterial3Api::class)
package com.example.app.screens
import android.app.Application
import android.util.Log
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.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.arcgismaps.geometry.AreaUnit
import com.arcgismaps.geometry.GeodeticCurveType
import com.arcgismaps.geometry.GeometryEngine
import com.arcgismaps.geometry.GeometryType
import com.arcgismaps.geometry.LinearUnit
import com.arcgismaps.geometry.MeasurementUnit
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.Polygon
import com.arcgismaps.geometry.Polyline
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
import com.arcgismaps.mapping.view.geometryeditor.VertexTool
import com.arcgismaps.toolkit.geoviewcompose.MapView
import com.example.app.R
import kotlinx.coroutines.launch
import kotlin.math.round

Create a view model

Modern app architecture uses a map view model to hold the business logic and the mutual state of your app.

  1. In Android Studio: in the Android view, open app > kotlin+java > com.example.app > screens > MainScreen.kt

    Create a map view model that extends ViewModel.

    MainScreen.kt
    56 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    }
  2. In the MainScreen composable, delete the entire function body. Leave just the function declaration as shown below. Then delete the top-level code for creating a map that is part of the Display a map tutorial. In this tutorial, you will create the map within the view model.

    MainScreen.kt
    60 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    }
    @Composable
    fun MainScreen() {
    }
    MainScreen.kt
    40 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.foundation.layout.padding
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.remember
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    @Composable
    fun MainScreen() {
    val map = remember {
    createMap()
    }
    Scaffold(
    topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }
    ) {
    MapView(
    modifier = Modifier.fillMaxSize().padding(it),
    arcGISMap = map
    )
    }
    }
    fun createMap(): ArcGISMap {
    return ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    latitude = 34.0270,
    longitude = -118.8050,
    scale = 72000.0
    )
    }
    }
  3. In the view model, create a map from a BasemapStyle and center the ArcGISMap.initialViewpoint on England with a scale of 1:10,000,000.

    MainScreen.kt
    58 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    2 collapsed lines
    }

Create a geometry editor and the display strings

A geometry editor allows the user to create geometries and edit them by tapping on the map view. The geometry engine returns the current geometry, which you measure. You will assign the measurements as strings to be displayed directly below the map. Declare map view model properties for the geometry editor and the strings.

  1. Create a GeometryEditor. Then create MutableState strings to display the measurements of the geometry.

    MainScreen.kt
    70 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    2 collapsed lines
    }

    The measureType can have the following values at runtime: “measurement”, “length”, and “area”. The displayUnits value can be “km” or “km²”.

  2. Create a function to reset the display strings to 0 and a utility to log errors.

    MainScreen.kt
    78 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    2 collapsed lines
    }

Create functions to start the geometry editor and delete the geometry

When the user taps the Polyine or Polygon tool, the geometry editor should be stopped and then restarted with the appropriate geometry type. When the user taps the Delete tool, the current geometry in the geometry editor should be deleted.

  1. Create a function to start the geometry editor with a Polyline or Polygon. Then create a function that deletes the current geometry in the geometry editor and resets the measurement display text.

    MainScreen.kt
    95 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    /**
    * Starts the GeometryEditor using the selected [GeometryType].
    */
    fun startEditor(selectedGeometry: GeometryType) {
    geometryEditor.apply {
    // Stops the current editing session
    stop()
    // Start new editing session
    start(selectedGeometry)
    }
    }
    /**
    * Deletes the selected element and stops the geometry editor if there are no
    * more elements in the geometry.
    */
    fun deleteSelection() {
    if (geometryEditor.geometry.value?.isEmpty == true) {
    geometryEditor.stop()
    return
    }
    // Select the entire geometry instead of the just the most recent edit.
    geometryEditor.selectGeometry()
    val selectedElement = geometryEditor.selectedElement.value
    if (selectedElement?.canDelete == true) {
    geometryEditor.deleteSelectedElement()
    }
    resetDisplayText()
    }
    2 collapsed lines
    }

    You call GeometryEditor.selectGeometry() to select the entire geometry instead of just the last edit. For instance, selecting the entire polygon instead of just the last line drawn on the screen.

Calculate measurements of the current geometry

Calculate the geodetic and planar measurements for the current geometry, which can be Polyline or Polygon. Simplify the topology of the geometry, and call displayMeasurements().

  1. Create the following function to display geodetic and planar measurements of the same polyline (length) or polygon (area). Leave the body empty. You will complete the code for this function later.

    MainScreen.kt
    128 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    /**
    * Starts the GeometryEditor using the selected [GeometryType].
    */
    fun startEditor(selectedGeometry: GeometryType) {
    geometryEditor.apply {
    // Stops the current editing session
    stop()
    // Start new editing session
    start(selectedGeometry)
    }
    }
    /**
    * Deletes the selected element and stops the geometry editor if there are no
    * more elements in the geometry.
    */
    fun deleteSelection() {
    if (geometryEditor.geometry.value?.isEmpty == true) {
    geometryEditor.stop()
    return
    }
    // Select the entire geometry instead of the just the most recent edit.
    geometryEditor.selectGeometry()
    val selectedElement = geometryEditor.selectedElement.value
    if (selectedElement?.canDelete == true) {
    geometryEditor.deleteSelectedElement()
    }
    resetDisplayText()
    }
    private fun displayMeasurements(
    measurementGeodetic: Double,
    measurementPlanar: Double,
    geometryType: GeometryType,
    spatialReferenceUnit: MeasurementUnit
    ) {
    }
    2 collapsed lines
    }
  2. Create the following function that obtains the current geometry using the GeometryEditor. This is the geometry you will measure using the GeometryEngine.

    MainScreen.kt
    128 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    /**
    * Starts the GeometryEditor using the selected [GeometryType].
    */
    fun startEditor(selectedGeometry: GeometryType) {
    geometryEditor.apply {
    // Stops the current editing session
    stop()
    // Start new editing session
    start(selectedGeometry)
    }
    }
    /**
    * Deletes the selected element and stops the geometry editor if there are no
    * more elements in the geometry.
    */
    fun deleteSelection() {
    if (geometryEditor.geometry.value?.isEmpty == true) {
    geometryEditor.stop()
    return
    }
    // Select the entire geometry instead of the just the most recent edit.
    geometryEditor.selectGeometry()
    val selectedElement = geometryEditor.selectedElement.value
    if (selectedElement?.canDelete == true) {
    geometryEditor.deleteSelectedElement()
    }
    resetDisplayText()
    }
    /**
    * Calculates the geometry's measurement.
    */
    private fun calculateMeasurements() {
    // Retrieve the latest state of the geometry using the geometry editor.
    val geometryToMeasure = geometryEditor.geometry.value
    ?: return logError(Exception("Geometry passed is null."))
    }
    12 collapsed lines
    private fun displayMeasurements(
    measurementGeodetic: Double,
    measurementPlanar: Double,
    geometryType: GeometryType,
    spatialReferenceUnit: MeasurementUnit
    ) {
    }
    }
  3. Simplify the geometry to make it topologically consistent. Simplifying ensures that areas are not returned as negative values. (If you omit simplification with this map, for example, creating a triangle by tapping its vertices in counterclockwise order can yield a negative area, whereas tapping in clockwise order yields a positive area.)

    MainScreen.kt
    137 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    /**
    * Starts the GeometryEditor using the selected [GeometryType].
    */
    fun startEditor(selectedGeometry: GeometryType) {
    geometryEditor.apply {
    // Stops the current editing session
    stop()
    // Start new editing session
    start(selectedGeometry)
    }
    }
    /**
    * Deletes the selected element and stops the geometry editor if there are no
    * more elements in the geometry.
    */
    fun deleteSelection() {
    if (geometryEditor.geometry.value?.isEmpty == true) {
    geometryEditor.stop()
    return
    }
    // Select the entire geometry instead of the just the most recent edit.
    geometryEditor.selectGeometry()
    val selectedElement = geometryEditor.selectedElement.value
    if (selectedElement?.canDelete == true) {
    geometryEditor.deleteSelectedElement()
    }
    resetDisplayText()
    }
    /**
    * Calculates the geometry's measurement.
    */
    private fun calculateMeasurements() {
    // Retrieve the latest state of the geometry using the geometry editor.
    val geometryToMeasure = geometryEditor.geometry.value
    ?: return logError(Exception("Geometry passed is null."))
    // Simplify the geometry to make it topologically consistent according to its type.
    // Among other things, simplifying ensures that areas are never returned as negative values.
    val simplifiedGeometry = GeometryEngine.simplifyOrNull(geometry = geometryToMeasure)
    ?: return logError(Exception("Failed to simplify the geometry"))
    14 collapsed lines
    }
    private fun displayMeasurements(
    measurementGeodetic: Double,
    measurementPlanar: Double,
    geometryType: GeometryType,
    spatialReferenceUnit: MeasurementUnit
    ) {
    }
    }
  4. Create a local variable to store the spatial reference of the simplified geometry.

    MainScreen.kt
    142 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    /**
    * Starts the GeometryEditor using the selected [GeometryType].
    */
    fun startEditor(selectedGeometry: GeometryType) {
    geometryEditor.apply {
    // Stops the current editing session
    stop()
    // Start new editing session
    start(selectedGeometry)
    }
    }
    /**
    * Deletes the selected element and stops the geometry editor if there are no
    * more elements in the geometry.
    */
    fun deleteSelection() {
    if (geometryEditor.geometry.value?.isEmpty == true) {
    geometryEditor.stop()
    return
    }
    // Select the entire geometry instead of the just the most recent edit.
    geometryEditor.selectGeometry()
    val selectedElement = geometryEditor.selectedElement.value
    if (selectedElement?.canDelete == true) {
    geometryEditor.deleteSelectedElement()
    }
    resetDisplayText()
    }
    /**
    * Calculates the geometry's measurement.
    */
    private fun calculateMeasurements() {
    // Retrieve the latest state of the geometry using the geometry editor.
    val geometryToMeasure = geometryEditor.geometry.value
    ?: return logError(Exception("Geometry passed is null."))
    // Simplify the geometry to make it topologically consistent according to its type.
    // Among other things, simplifying ensures that areas are never returned as negative values.
    val simplifiedGeometry = GeometryEngine.simplifyOrNull(geometry = geometryToMeasure)
    ?: return logError(Exception("Failed to simplify the geometry"))
    val spatialReferenceOfGeometry = simplifiedGeometry.spatialReference
    ?: return logError(Exception("The geometry has no spatial reference"))
    14 collapsed lines
    }
    private fun displayMeasurements(
    measurementGeodetic: Double,
    measurementPlanar: Double,
    geometryType: GeometryType,
    spatialReferenceUnit: MeasurementUnit
    ) {
    }
    }
  5. Test if the geometry is a Polyline or a Polygon.

    If the geometry is a Polyline, call GeometryEngine.lengthGeodetic() with the geometry, kilometers as the unit of measure, and GeodeticCurveType.Geodesic as the geodetic curve type. Then call GeometryEngine.length() to get the planar length of the geometry. Last, call your function to display the results. Pass the geodetic length, planar length, geometry type, and the unit of measure of the geometry’s spatial reference.

    MainScreen.kt
    145 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    /**
    * Starts the GeometryEditor using the selected [GeometryType].
    */
    fun startEditor(selectedGeometry: GeometryType) {
    geometryEditor.apply {
    // Stops the current editing session
    stop()
    // Start new editing session
    start(selectedGeometry)
    }
    }
    /**
    * Deletes the selected element and stops the geometry editor if there are no
    * more elements in the geometry.
    */
    fun deleteSelection() {
    if (geometryEditor.geometry.value?.isEmpty == true) {
    geometryEditor.stop()
    return
    }
    // Select the entire geometry instead of the just the most recent edit.
    geometryEditor.selectGeometry()
    val selectedElement = geometryEditor.selectedElement.value
    if (selectedElement?.canDelete == true) {
    geometryEditor.deleteSelectedElement()
    }
    resetDisplayText()
    }
    /**
    * Calculates the geometry's measurement.
    */
    private fun calculateMeasurements() {
    // Retrieve the latest state of the geometry using the geometry editor.
    val geometryToMeasure = geometryEditor.geometry.value
    ?: return logError(Exception("Geometry passed is null."))
    // Simplify the geometry to make it topologically consistent according to its type.
    // Among other things, simplifying ensures that areas are never returned as negative values.
    val simplifiedGeometry = GeometryEngine.simplifyOrNull(geometry = geometryToMeasure)
    ?: return logError(Exception("Failed to simplify the geometry"))
    val spatialReferenceOfGeometry = simplifiedGeometry.spatialReference
    ?: return logError(Exception("The geometry has no spatial reference"))
    when (simplifiedGeometry) {
    is Polyline -> {
    // Get the geodetic length.
    val lengthGeodetic = GeometryEngine.lengthGeodetic(
    geometry = simplifiedGeometry,
    lengthUnit = LinearUnit.kilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar length, which is returned in meters.
    val lengthPlanar = GeometryEngine.length(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = lengthGeodetic,
    measurementPlanar = lengthPlanar,
    geometryType = GeometryType.Polyline,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    13 collapsed lines
    }
    private fun displayMeasurements(
    measurementGeodetic: Double,
    measurementPlanar: Double,
    geometryType: GeometryType,
    spatialReferenceUnit: MeasurementUnit
    ) {
    }
    }
  6. If the geometry is a Polygon, write similar code to measure the polygon’s area. Note that you call GeometryEngine.areaGeodetic() with square kilometers as the units of measure.

    MainScreen.kt
    166 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    /**
    * Starts the GeometryEditor using the selected [GeometryType].
    */
    fun startEditor(selectedGeometry: GeometryType) {
    geometryEditor.apply {
    // Stops the current editing session
    stop()
    // Start new editing session
    start(selectedGeometry)
    }
    }
    /**
    * Deletes the selected element and stops the geometry editor if there are no
    * more elements in the geometry.
    */
    fun deleteSelection() {
    if (geometryEditor.geometry.value?.isEmpty == true) {
    geometryEditor.stop()
    return
    }
    // Select the entire geometry instead of the just the most recent edit.
    geometryEditor.selectGeometry()
    val selectedElement = geometryEditor.selectedElement.value
    if (selectedElement?.canDelete == true) {
    geometryEditor.deleteSelectedElement()
    }
    resetDisplayText()
    }
    /**
    * Calculates the geometry's measurement.
    */
    private fun calculateMeasurements() {
    // Retrieve the latest state of the geometry using the geometry editor.
    val geometryToMeasure = geometryEditor.geometry.value
    ?: return logError(Exception("Geometry passed is null."))
    // Simplify the geometry to make it topologically consistent according to its type.
    // Among other things, simplifying ensures that areas are never returned as negative values.
    val simplifiedGeometry = GeometryEngine.simplifyOrNull(geometry = geometryToMeasure)
    ?: return logError(Exception("Failed to simplify the geometry"))
    val spatialReferenceOfGeometry = simplifiedGeometry.spatialReference
    ?: return logError(Exception("The geometry has no spatial reference"))
    when (simplifiedGeometry) {
    is Polyline -> {
    // Get the geodetic length.
    val lengthGeodetic = GeometryEngine.lengthGeodetic(
    geometry = simplifiedGeometry,
    lengthUnit = LinearUnit.kilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar length, which is returned in meters.
    val lengthPlanar = GeometryEngine.length(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = lengthGeodetic,
    measurementPlanar = lengthPlanar,
    geometryType = GeometryType.Polyline,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    is Polygon -> {
    // Get the geodetic area.
    val areaGeodetic = GeometryEngine.areaGeodetic(
    geometry = simplifiedGeometry,
    unit = AreaUnit.squareKilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar area, which is returned in square meters.
    val areaPlanar = GeometryEngine.area(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = areaGeodetic,
    measurementPlanar = areaPlanar,
    geometryType = GeometryType.Polygon,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    else -> {}
    }
    14 collapsed lines
    }
    private fun displayMeasurements(
    measurementGeodetic: Double,
    measurementPlanar: Double,
    geometryType: GeometryType,
    spatialReferenceUnit: MeasurementUnit
    ) {
    }
    }

Display the current measurements

You declared the displayMeasurements() function above and called it twice in calculateMeasurements(). Now you will implement the function, which converts units of measurement so that the planar units match the geodetic units, rounds the measurements, and displays them by assigning to the MutableState display strings.

  1. Test if the geometry type is Polyline or Polygon. If the geometry type is Polyline, set the displayUnits property to “km” and measureType to “length” and do the following:

    • Create a rounding function as an extension function on Double. You will round to 3 decimal places.
    • Round the geodetic measurement and display it.
    • Test if the units of measurement of the geometry’s spatial reference is LinearUnit. If so, you know the geometry has a projected spatial reference.
    • Convert the length to kilometers. (The GeometryEngine.length() function returns length in meters.)
    • Round the converted measurement and assign to planarMeasurement.
    MainScreen.kt
    190 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    /**
    * Starts the GeometryEditor using the selected [GeometryType].
    */
    fun startEditor(selectedGeometry: GeometryType) {
    geometryEditor.apply {
    // Stops the current editing session
    stop()
    // Start new editing session
    start(selectedGeometry)
    }
    }
    /**
    * Deletes the selected element and stops the geometry editor if there are no
    * more elements in the geometry.
    */
    fun deleteSelection() {
    if (geometryEditor.geometry.value?.isEmpty == true) {
    geometryEditor.stop()
    return
    }
    // Select the entire geometry instead of the just the most recent edit.
    geometryEditor.selectGeometry()
    val selectedElement = geometryEditor.selectedElement.value
    if (selectedElement?.canDelete == true) {
    geometryEditor.deleteSelectedElement()
    }
    resetDisplayText()
    }
    /**
    * Calculates the geometry's measurement.
    */
    private fun calculateMeasurements() {
    // Retrieve the latest state of the geometry using the geometry editor.
    val geometryToMeasure = geometryEditor.geometry.value
    ?: return logError(Exception("Geometry passed is null."))
    // Simplify the geometry to make it topologically consistent according to its type.
    // Among other things, simplifying ensures that areas are never returned as negative values.
    val simplifiedGeometry = GeometryEngine.simplifyOrNull(geometry = geometryToMeasure)
    ?: return logError(Exception("Failed to simplify the geometry"))
    val spatialReferenceOfGeometry = simplifiedGeometry.spatialReference
    ?: return logError(Exception("The geometry has no spatial reference"))
    when (simplifiedGeometry) {
    is Polyline -> {
    // Get the geodetic length.
    val lengthGeodetic = GeometryEngine.lengthGeodetic(
    geometry = simplifiedGeometry,
    lengthUnit = LinearUnit.kilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar length, which is returned in meters.
    val lengthPlanar = GeometryEngine.length(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = lengthGeodetic,
    measurementPlanar = lengthPlanar,
    geometryType = GeometryType.Polyline,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    is Polygon -> {
    // Get the geodetic area.
    val areaGeodetic = GeometryEngine.areaGeodetic(
    geometry = simplifiedGeometry,
    unit = AreaUnit.squareKilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar area, which is returned in square meters.
    val areaPlanar = GeometryEngine.area(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = areaGeodetic,
    measurementPlanar = areaPlanar,
    geometryType = GeometryType.Polygon,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    else -> {}
    }
    }
    /**
    * Rounds number to 3 decimal places.
    */
    private fun Double.roundToThreeDecimals(): Double {
    return round(this * 1000.0) / 1000.0
    }
    private fun displayMeasurements(
    measurementGeodetic: Double,
    measurementPlanar: Double,
    geometryType: GeometryType,
    spatialReferenceUnit: MeasurementUnit
    ) {
    when (geometryType) {
    GeometryType.Polyline -> {
    displayUnits = "km"
    measureType = "length"
    // Round the geodetic measurement. Then display it.
    geodeticMeasurement = measurementGeodetic.roundToThreeDecimals().toString()
    // If a spatial reference is projected, its measurement unit is LinearUnit. In this tutorial, you can tell
    // the spatial reference by inspection. However, for more complex apps, it is good practice to check.
    if (spatialReferenceUnit is LinearUnit) {
    val convertedPlanarMeasurement = spatialReferenceUnit.convertTo(
    toUnit = LinearUnit.kilometers,
    value = measurementPlanar
    )
    planarMeasurement = convertedPlanarMeasurement.roundToThreeDecimals().toString()
    }
    }
    }
    }
    2 collapsed lines
    }
  2. If the geometry type is Polygon, set the displayUnit property to “km²” and measureType to “area” and do the following:

    • Round the geodetic measurement to 3 decimal places and display it.
    • Test if the units of measurement of the geometry’s spatial reference are LinearUnit. If so, you know the geometry has a projected spatial reference.
    • Convert the area to square kilometers. (The GeometryEngine.area() function returns a result in square meters).
    • Round the converted measurement and assign to planarMeasurement.
    MainScreen.kt
    224 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    /**
    * Starts the GeometryEditor using the selected [GeometryType].
    */
    fun startEditor(selectedGeometry: GeometryType) {
    geometryEditor.apply {
    // Stops the current editing session
    stop()
    // Start new editing session
    start(selectedGeometry)
    }
    }
    /**
    * Deletes the selected element and stops the geometry editor if there are no
    * more elements in the geometry.
    */
    fun deleteSelection() {
    if (geometryEditor.geometry.value?.isEmpty == true) {
    geometryEditor.stop()
    return
    }
    // Select the entire geometry instead of the just the most recent edit.
    geometryEditor.selectGeometry()
    val selectedElement = geometryEditor.selectedElement.value
    if (selectedElement?.canDelete == true) {
    geometryEditor.deleteSelectedElement()
    }
    resetDisplayText()
    }
    /**
    * Calculates the geometry's measurement.
    */
    private fun calculateMeasurements() {
    // Retrieve the latest state of the geometry using the geometry editor.
    val geometryToMeasure = geometryEditor.geometry.value
    ?: return logError(Exception("Geometry passed is null."))
    // Simplify the geometry to make it topologically consistent according to its type.
    // Among other things, simplifying ensures that areas are never returned as negative values.
    val simplifiedGeometry = GeometryEngine.simplifyOrNull(geometry = geometryToMeasure)
    ?: return logError(Exception("Failed to simplify the geometry"))
    val spatialReferenceOfGeometry = simplifiedGeometry.spatialReference
    ?: return logError(Exception("The geometry has no spatial reference"))
    when (simplifiedGeometry) {
    is Polyline -> {
    // Get the geodetic length.
    val lengthGeodetic = GeometryEngine.lengthGeodetic(
    geometry = simplifiedGeometry,
    lengthUnit = LinearUnit.kilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar length, which is returned in meters.
    val lengthPlanar = GeometryEngine.length(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = lengthGeodetic,
    measurementPlanar = lengthPlanar,
    geometryType = GeometryType.Polyline,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    is Polygon -> {
    // Get the geodetic area.
    val areaGeodetic = GeometryEngine.areaGeodetic(
    geometry = simplifiedGeometry,
    unit = AreaUnit.squareKilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar area, which is returned in square meters.
    val areaPlanar = GeometryEngine.area(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = areaGeodetic,
    measurementPlanar = areaPlanar,
    geometryType = GeometryType.Polygon,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    else -> {}
    }
    }
    /**
    * Rounds number to 3 decimal places.
    */
    private fun Double.roundToThreeDecimals(): Double {
    return round(this * 1000.0) / 1000.0
    }
    private fun displayMeasurements(
    measurementGeodetic: Double,
    measurementPlanar: Double,
    geometryType: GeometryType,
    spatialReferenceUnit: MeasurementUnit
    ) {
    when (geometryType) {
    GeometryType.Polyline -> {
    displayUnits = "km"
    measureType = "length"
    // Round the geodetic measurement. Then display it.
    geodeticMeasurement = measurementGeodetic.roundToThreeDecimals().toString()
    // If a spatial reference is projected, its measurement unit is LinearUnit. In this tutorial, you can tell
    // the spatial reference by inspection. However, for more complex apps, it is good practice to check.
    if (spatialReferenceUnit is LinearUnit) {
    val convertedPlanarMeasurement = spatialReferenceUnit.convertTo(
    toUnit = LinearUnit.kilometers,
    value = measurementPlanar
    )
    planarMeasurement = convertedPlanarMeasurement.roundToThreeDecimals().toString()
    }
    }
    GeometryType.Polygon -> {
    displayUnits = "km²"
    measureType = "area"
    // Round the geodetic measurement. Then display it.
    geodeticMeasurement = measurementGeodetic.roundToThreeDecimals().toString()
    // If a spatial reference is projected, its measurement unit is LinearUnit. In this tutorial, you can tell
    // the spatial reference by inspection. However, for more complex apps, it is good practice to check.
    if (spatialReferenceUnit is LinearUnit) {
    val convertedPlanarMeasurement = AreaUnit(spatialReferenceUnit).convertTo(
    toUnit = AreaUnit.squareKilometers,
    area = measurementPlanar
    )
    planarMeasurement = convertedPlanarMeasurement.roundToThreeDecimals().toString()
    }
    }
    else -> {}
    6 collapsed lines
    }
    }
    }

Start collecting geometry changes

The geometry editor provides access to the current geometry. GeometryEditor.geometry is a StateFlow that emits a value every time the current geometry changes. You should collect the values and call calculateMeasurements().

  1. In the view model’s initialization block, do the following:

    MainScreen.kt
    248 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    /**
    * Starts the GeometryEditor using the selected [GeometryType].
    */
    fun startEditor(selectedGeometry: GeometryType) {
    geometryEditor.apply {
    // Stops the current editing session
    stop()
    // Start new editing session
    start(selectedGeometry)
    }
    }
    /**
    * Deletes the selected element and stops the geometry editor if there are no
    * more elements in the geometry.
    */
    fun deleteSelection() {
    if (geometryEditor.geometry.value?.isEmpty == true) {
    geometryEditor.stop()
    return
    }
    // Select the entire geometry instead of the just the most recent edit.
    geometryEditor.selectGeometry()
    val selectedElement = geometryEditor.selectedElement.value
    if (selectedElement?.canDelete == true) {
    geometryEditor.deleteSelectedElement()
    }
    resetDisplayText()
    }
    /**
    * Calculates the geometry's measurement.
    */
    private fun calculateMeasurements() {
    // Retrieve the latest state of the geometry using the geometry editor.
    val geometryToMeasure = geometryEditor.geometry.value
    ?: return logError(Exception("Geometry passed is null."))
    // Simplify the geometry to make it topologically consistent according to its type.
    // Among other things, simplifying ensures that areas are never returned as negative values.
    val simplifiedGeometry = GeometryEngine.simplifyOrNull(geometry = geometryToMeasure)
    ?: return logError(Exception("Failed to simplify the geometry"))
    val spatialReferenceOfGeometry = simplifiedGeometry.spatialReference
    ?: return logError(Exception("The geometry has no spatial reference"))
    when (simplifiedGeometry) {
    is Polyline -> {
    // Get the geodetic length.
    val lengthGeodetic = GeometryEngine.lengthGeodetic(
    geometry = simplifiedGeometry,
    lengthUnit = LinearUnit.kilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar length, which is returned in meters.
    val lengthPlanar = GeometryEngine.length(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = lengthGeodetic,
    measurementPlanar = lengthPlanar,
    geometryType = GeometryType.Polyline,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    is Polygon -> {
    // Get the geodetic area.
    val areaGeodetic = GeometryEngine.areaGeodetic(
    geometry = simplifiedGeometry,
    unit = AreaUnit.squareKilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar area, which is returned in square meters.
    val areaPlanar = GeometryEngine.area(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = areaGeodetic,
    measurementPlanar = areaPlanar,
    geometryType = GeometryType.Polygon,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    else -> {}
    }
    }
    /**
    * Rounds number to 3 decimal places.
    */
    private fun Double.roundToThreeDecimals(): Double {
    return round(this * 1000.0) / 1000.0
    }
    private fun displayMeasurements(
    measurementGeodetic: Double,
    measurementPlanar: Double,
    geometryType: GeometryType,
    spatialReferenceUnit: MeasurementUnit
    ) {
    when (geometryType) {
    GeometryType.Polyline -> {
    displayUnits = "km"
    measureType = "length"
    // Round the geodetic measurement. Then display it.
    geodeticMeasurement = measurementGeodetic.roundToThreeDecimals().toString()
    // If a spatial reference is projected, its measurement unit is LinearUnit. In this tutorial, you can tell
    // the spatial reference by inspection. However, for more complex apps, it is good practice to check.
    if (spatialReferenceUnit is LinearUnit) {
    val convertedPlanarMeasurement = spatialReferenceUnit.convertTo(
    toUnit = LinearUnit.kilometers,
    value = measurementPlanar
    )
    planarMeasurement = convertedPlanarMeasurement.roundToThreeDecimals().toString()
    }
    }
    GeometryType.Polygon -> {
    displayUnits = "km²"
    measureType = "area"
    // Round the geodetic measurement. Then display it.
    geodeticMeasurement = measurementGeodetic.roundToThreeDecimals().toString()
    // If a spatial reference is projected, its measurement unit is LinearUnit. In this tutorial, you can tell
    // the spatial reference by inspection. However, for more complex apps, it is good practice to check.
    if (spatialReferenceUnit is LinearUnit) {
    val convertedPlanarMeasurement = AreaUnit(spatialReferenceUnit).convertTo(
    toUnit = AreaUnit.squareKilometers,
    area = measurementPlanar
    )
    planarMeasurement = convertedPlanarMeasurement.roundToThreeDecimals().toString()
    }
    }
    else -> {}
    }
    }
    /**
    * Load the map and set GeometryEditor tool after the map is loaded.
    */
    init {
    viewModelScope.launch {
    // Load the map.
    map.load().onFailure { error ->
    logError(error)
    }
    // Set Geometry Editor tool when map is loaded.
    geometryEditor.tool = VertexTool()
    viewModelScope.launch {
    geometryEditor.geometry.collect {
    // Update the length/area measurement as the geometry changes.
    calculateMeasurements()
    }
    }
    }
    }
    2 collapsed lines
    }

Implement the UI

  1. After the MainScreen composable, declare a composable to display the measurement text and then the Polyline, Polygon, and Delete tools. Leave the body empty for now. You will complete the code for this composable in a later step.

    MainScreen.kt
    275 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    /**
    * Starts the GeometryEditor using the selected [GeometryType].
    */
    fun startEditor(selectedGeometry: GeometryType) {
    geometryEditor.apply {
    // Stops the current editing session
    stop()
    // Start new editing session
    start(selectedGeometry)
    }
    }
    /**
    * Deletes the selected element and stops the geometry editor if there are no
    * more elements in the geometry.
    */
    fun deleteSelection() {
    if (geometryEditor.geometry.value?.isEmpty == true) {
    geometryEditor.stop()
    return
    }
    // Select the entire geometry instead of the just the most recent edit.
    geometryEditor.selectGeometry()
    val selectedElement = geometryEditor.selectedElement.value
    if (selectedElement?.canDelete == true) {
    geometryEditor.deleteSelectedElement()
    }
    resetDisplayText()
    }
    /**
    * Calculates the geometry's measurement.
    */
    private fun calculateMeasurements() {
    // Retrieve the latest state of the geometry using the geometry editor.
    val geometryToMeasure = geometryEditor.geometry.value
    ?: return logError(Exception("Geometry passed is null."))
    // Simplify the geometry to make it topologically consistent according to its type.
    // Among other things, simplifying ensures that areas are never returned as negative values.
    val simplifiedGeometry = GeometryEngine.simplifyOrNull(geometry = geometryToMeasure)
    ?: return logError(Exception("Failed to simplify the geometry"))
    val spatialReferenceOfGeometry = simplifiedGeometry.spatialReference
    ?: return logError(Exception("The geometry has no spatial reference"))
    when (simplifiedGeometry) {
    is Polyline -> {
    // Get the geodetic length.
    val lengthGeodetic = GeometryEngine.lengthGeodetic(
    geometry = simplifiedGeometry,
    lengthUnit = LinearUnit.kilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar length, which is returned in meters.
    val lengthPlanar = GeometryEngine.length(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = lengthGeodetic,
    measurementPlanar = lengthPlanar,
    geometryType = GeometryType.Polyline,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    is Polygon -> {
    // Get the geodetic area.
    val areaGeodetic = GeometryEngine.areaGeodetic(
    geometry = simplifiedGeometry,
    unit = AreaUnit.squareKilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar area, which is returned in square meters.
    val areaPlanar = GeometryEngine.area(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = areaGeodetic,
    measurementPlanar = areaPlanar,
    geometryType = GeometryType.Polygon,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    else -> {}
    }
    }
    /**
    * Rounds number to 3 decimal places.
    */
    private fun Double.roundToThreeDecimals(): Double {
    return round(this * 1000.0) / 1000.0
    }
    private fun displayMeasurements(
    measurementGeodetic: Double,
    measurementPlanar: Double,
    geometryType: GeometryType,
    spatialReferenceUnit: MeasurementUnit
    ) {
    when (geometryType) {
    GeometryType.Polyline -> {
    displayUnits = "km"
    measureType = "length"
    // Round the geodetic measurement. Then display it.
    geodeticMeasurement = measurementGeodetic.roundToThreeDecimals().toString()
    // If a spatial reference is projected, its measurement unit is LinearUnit. In this tutorial, you can tell
    // the spatial reference by inspection. However, for more complex apps, it is good practice to check.
    if (spatialReferenceUnit is LinearUnit) {
    val convertedPlanarMeasurement = spatialReferenceUnit.convertTo(
    toUnit = LinearUnit.kilometers,
    value = measurementPlanar
    )
    planarMeasurement = convertedPlanarMeasurement.roundToThreeDecimals().toString()
    }
    }
    GeometryType.Polygon -> {
    displayUnits = "km²"
    measureType = "area"
    // Round the geodetic measurement. Then display it.
    geodeticMeasurement = measurementGeodetic.roundToThreeDecimals().toString()
    // If a spatial reference is projected, its measurement unit is LinearUnit. In this tutorial, you can tell
    // the spatial reference by inspection. However, for more complex apps, it is good practice to check.
    if (spatialReferenceUnit is LinearUnit) {
    val convertedPlanarMeasurement = AreaUnit(spatialReferenceUnit).convertTo(
    toUnit = AreaUnit.squareKilometers,
    area = measurementPlanar
    )
    planarMeasurement = convertedPlanarMeasurement.roundToThreeDecimals().toString()
    }
    }
    else -> {}
    }
    }
    /**
    * Load the map and set GeometryEditor tool after the map is loaded.
    */
    init {
    viewModelScope.launch {
    // Load the map.
    map.load().onFailure { error ->
    logError(error)
    }
    // Set Geometry Editor tool when map is loaded.
    geometryEditor.tool = VertexTool()
    viewModelScope.launch {
    geometryEditor.geometry.collect {
    // Update the length/area measurement as the geometry changes.
    calculateMeasurements()
    }
    }
    }
    }
    }
    @Composable
    fun MainScreen() {
    }
    @Composable
    fun GeometryOptions(mapViewModel: MapViewModel) {
    }
  2. In the MainScreen composable, instantiate the view model. Then create a Scaffold that contains a Column. The column will contain the MapView, followed by the controls defined in the GeometryOptions composable.

    MainScreen.kt
    270 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    /**
    * Starts the GeometryEditor using the selected [GeometryType].
    */
    fun startEditor(selectedGeometry: GeometryType) {
    geometryEditor.apply {
    // Stops the current editing session
    stop()
    // Start new editing session
    start(selectedGeometry)
    }
    }
    /**
    * Deletes the selected element and stops the geometry editor if there are no
    * more elements in the geometry.
    */
    fun deleteSelection() {
    if (geometryEditor.geometry.value?.isEmpty == true) {
    geometryEditor.stop()
    return
    }
    // Select the entire geometry instead of the just the most recent edit.
    geometryEditor.selectGeometry()
    val selectedElement = geometryEditor.selectedElement.value
    if (selectedElement?.canDelete == true) {
    geometryEditor.deleteSelectedElement()
    }
    resetDisplayText()
    }
    /**
    * Calculates the geometry's measurement.
    */
    private fun calculateMeasurements() {
    // Retrieve the latest state of the geometry using the geometry editor.
    val geometryToMeasure = geometryEditor.geometry.value
    ?: return logError(Exception("Geometry passed is null."))
    // Simplify the geometry to make it topologically consistent according to its type.
    // Among other things, simplifying ensures that areas are never returned as negative values.
    val simplifiedGeometry = GeometryEngine.simplifyOrNull(geometry = geometryToMeasure)
    ?: return logError(Exception("Failed to simplify the geometry"))
    val spatialReferenceOfGeometry = simplifiedGeometry.spatialReference
    ?: return logError(Exception("The geometry has no spatial reference"))
    when (simplifiedGeometry) {
    is Polyline -> {
    // Get the geodetic length.
    val lengthGeodetic = GeometryEngine.lengthGeodetic(
    geometry = simplifiedGeometry,
    lengthUnit = LinearUnit.kilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar length, which is returned in meters.
    val lengthPlanar = GeometryEngine.length(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = lengthGeodetic,
    measurementPlanar = lengthPlanar,
    geometryType = GeometryType.Polyline,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    is Polygon -> {
    // Get the geodetic area.
    val areaGeodetic = GeometryEngine.areaGeodetic(
    geometry = simplifiedGeometry,
    unit = AreaUnit.squareKilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar area, which is returned in square meters.
    val areaPlanar = GeometryEngine.area(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = areaGeodetic,
    measurementPlanar = areaPlanar,
    geometryType = GeometryType.Polygon,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    else -> {}
    }
    }
    /**
    * Rounds number to 3 decimal places.
    */
    private fun Double.roundToThreeDecimals(): Double {
    return round(this * 1000.0) / 1000.0
    }
    private fun displayMeasurements(
    measurementGeodetic: Double,
    measurementPlanar: Double,
    geometryType: GeometryType,
    spatialReferenceUnit: MeasurementUnit
    ) {
    when (geometryType) {
    GeometryType.Polyline -> {
    displayUnits = "km"
    measureType = "length"
    // Round the geodetic measurement. Then display it.
    geodeticMeasurement = measurementGeodetic.roundToThreeDecimals().toString()
    // If a spatial reference is projected, its measurement unit is LinearUnit. In this tutorial, you can tell
    // the spatial reference by inspection. However, for more complex apps, it is good practice to check.
    if (spatialReferenceUnit is LinearUnit) {
    val convertedPlanarMeasurement = spatialReferenceUnit.convertTo(
    toUnit = LinearUnit.kilometers,
    value = measurementPlanar
    )
    planarMeasurement = convertedPlanarMeasurement.roundToThreeDecimals().toString()
    }
    }
    GeometryType.Polygon -> {
    displayUnits = "km²"
    measureType = "area"
    // Round the geodetic measurement. Then display it.
    geodeticMeasurement = measurementGeodetic.roundToThreeDecimals().toString()
    // If a spatial reference is projected, its measurement unit is LinearUnit. In this tutorial, you can tell
    // the spatial reference by inspection. However, for more complex apps, it is good practice to check.
    if (spatialReferenceUnit is LinearUnit) {
    val convertedPlanarMeasurement = AreaUnit(spatialReferenceUnit).convertTo(
    toUnit = AreaUnit.squareKilometers,
    area = measurementPlanar
    )
    planarMeasurement = convertedPlanarMeasurement.roundToThreeDecimals().toString()
    }
    }
    else -> {}
    }
    }
    /**
    * Load the map and set GeometryEditor tool after the map is loaded.
    */
    init {
    viewModelScope.launch {
    // Load the map.
    map.load().onFailure { error ->
    logError(error)
    }
    // Set Geometry Editor tool when map is loaded.
    geometryEditor.tool = VertexTool()
    viewModelScope.launch {
    geometryEditor.geometry.collect {
    // Update the length/area measurement as the geometry changes.
    calculateMeasurements()
    }
    }
    }
    }
    }
    @Composable
    fun MainScreen() {
    // Create a ViewModel to handle MapView interactions.
    val mapViewModel: MapViewModel = viewModel()
    Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) {
    Column(
    modifier = Modifier
    .fillMaxSize()
    .padding(it)
    ) {
    MapView(
    modifier = Modifier
    .fillMaxSize()
    .weight(1f),
    arcGISMap = mapViewModel.map,
    geometryEditor = mapViewModel.geometryEditor
    )
    GeometryOptions(mapViewModel)
    }
    }
    }
    5 collapsed lines
    @Composable
    fun GeometryOptions(mapViewModel: MapViewModel) {
    }
  3. In the GeometryOptions composable, create a Column to display the measurement texts, a horizontal divider, and a tool row (which you will define next).

    MainScreen.kt
    296 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    /**
    * Starts the GeometryEditor using the selected [GeometryType].
    */
    fun startEditor(selectedGeometry: GeometryType) {
    geometryEditor.apply {
    // Stops the current editing session
    stop()
    // Start new editing session
    start(selectedGeometry)
    }
    }
    /**
    * Deletes the selected element and stops the geometry editor if there are no
    * more elements in the geometry.
    */
    fun deleteSelection() {
    if (geometryEditor.geometry.value?.isEmpty == true) {
    geometryEditor.stop()
    return
    }
    // Select the entire geometry instead of the just the most recent edit.
    geometryEditor.selectGeometry()
    val selectedElement = geometryEditor.selectedElement.value
    if (selectedElement?.canDelete == true) {
    geometryEditor.deleteSelectedElement()
    }
    resetDisplayText()
    }
    /**
    * Calculates the geometry's measurement.
    */
    private fun calculateMeasurements() {
    // Retrieve the latest state of the geometry using the geometry editor.
    val geometryToMeasure = geometryEditor.geometry.value
    ?: return logError(Exception("Geometry passed is null."))
    // Simplify the geometry to make it topologically consistent according to its type.
    // Among other things, simplifying ensures that areas are never returned as negative values.
    val simplifiedGeometry = GeometryEngine.simplifyOrNull(geometry = geometryToMeasure)
    ?: return logError(Exception("Failed to simplify the geometry"))
    val spatialReferenceOfGeometry = simplifiedGeometry.spatialReference
    ?: return logError(Exception("The geometry has no spatial reference"))
    when (simplifiedGeometry) {
    is Polyline -> {
    // Get the geodetic length.
    val lengthGeodetic = GeometryEngine.lengthGeodetic(
    geometry = simplifiedGeometry,
    lengthUnit = LinearUnit.kilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar length, which is returned in meters.
    val lengthPlanar = GeometryEngine.length(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = lengthGeodetic,
    measurementPlanar = lengthPlanar,
    geometryType = GeometryType.Polyline,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    is Polygon -> {
    // Get the geodetic area.
    val areaGeodetic = GeometryEngine.areaGeodetic(
    geometry = simplifiedGeometry,
    unit = AreaUnit.squareKilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar area, which is returned in square meters.
    val areaPlanar = GeometryEngine.area(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = areaGeodetic,
    measurementPlanar = areaPlanar,
    geometryType = GeometryType.Polygon,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    else -> {}
    }
    }
    /**
    * Rounds number to 3 decimal places.
    */
    private fun Double.roundToThreeDecimals(): Double {
    return round(this * 1000.0) / 1000.0
    }
    private fun displayMeasurements(
    measurementGeodetic: Double,
    measurementPlanar: Double,
    geometryType: GeometryType,
    spatialReferenceUnit: MeasurementUnit
    ) {
    when (geometryType) {
    GeometryType.Polyline -> {
    displayUnits = "km"
    measureType = "length"
    // Round the geodetic measurement. Then display it.
    geodeticMeasurement = measurementGeodetic.roundToThreeDecimals().toString()
    // If a spatial reference is projected, its measurement unit is LinearUnit. In this tutorial, you can tell
    // the spatial reference by inspection. However, for more complex apps, it is good practice to check.
    if (spatialReferenceUnit is LinearUnit) {
    val convertedPlanarMeasurement = spatialReferenceUnit.convertTo(
    toUnit = LinearUnit.kilometers,
    value = measurementPlanar
    )
    planarMeasurement = convertedPlanarMeasurement.roundToThreeDecimals().toString()
    }
    }
    GeometryType.Polygon -> {
    displayUnits = "km²"
    measureType = "area"
    // Round the geodetic measurement. Then display it.
    geodeticMeasurement = measurementGeodetic.roundToThreeDecimals().toString()
    // If a spatial reference is projected, its measurement unit is LinearUnit. In this tutorial, you can tell
    // the spatial reference by inspection. However, for more complex apps, it is good practice to check.
    if (spatialReferenceUnit is LinearUnit) {
    val convertedPlanarMeasurement = AreaUnit(spatialReferenceUnit).convertTo(
    toUnit = AreaUnit.squareKilometers,
    area = measurementPlanar
    )
    planarMeasurement = convertedPlanarMeasurement.roundToThreeDecimals().toString()
    }
    }
    else -> {}
    }
    }
    /**
    * Load the map and set GeometryEditor tool after the map is loaded.
    */
    init {
    viewModelScope.launch {
    // Load the map.
    map.load().onFailure { error ->
    logError(error)
    }
    // Set Geometry Editor tool when map is loaded.
    geometryEditor.tool = VertexTool()
    viewModelScope.launch {
    geometryEditor.geometry.collect {
    // Update the length/area measurement as the geometry changes.
    calculateMeasurements()
    }
    }
    }
    }
    }
    @Composable
    fun MainScreen() {
    // Create a ViewModel to handle MapView interactions.
    val mapViewModel: MapViewModel = viewModel()
    Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) {
    Column(
    modifier = Modifier
    .fillMaxSize()
    .padding(it)
    ) {
    MapView(
    modifier = Modifier
    .fillMaxSize()
    .weight(1f),
    arcGISMap = mapViewModel.map,
    geometryEditor = mapViewModel.geometryEditor
    )
    GeometryOptions(mapViewModel)
    }
    }
    }
    @Composable
    fun GeometryOptions(mapViewModel: MapViewModel) {
    Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
    Text(text = "Geodesic ${mapViewModel.measureType}: ${mapViewModel.geodeticMeasurement} ${mapViewModel.displayUnits}")
    Text(text = "Planar ${mapViewModel.measureType}: ${mapViewModel.planarMeasurement} ${mapViewModel.displayUnits}")
    HorizontalDivider()
    }
    }
  4. Create a Row to display a two-choice button for the Polyline and Polygon tools, and a Delete tool (trash icon).

    MainScreen.kt
    296 collapsed lines
    @file:OptIn(ExperimentalMaterial3Api::class)
    package com.example.app.screens
    import android.app.Application
    import android.util.Log
    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.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.ExperimentalMaterial3Api
    import androidx.compose.material3.HorizontalDivider
    import androidx.compose.material3.Icon
    import androidx.compose.material3.OutlinedIconButton
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.SegmentedButton
    import androidx.compose.material3.SegmentedButtonDefaults
    import androidx.compose.material3.SingleChoiceSegmentedButtonRow
    import androidx.compose.material3.Text
    import androidx.compose.material3.TopAppBar
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableIntStateOf
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.res.stringResource
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import com.arcgismaps.geometry.AreaUnit
    import com.arcgismaps.geometry.GeodeticCurveType
    import com.arcgismaps.geometry.GeometryEngine
    import com.arcgismaps.geometry.GeometryType
    import com.arcgismaps.geometry.LinearUnit
    import com.arcgismaps.geometry.MeasurementUnit
    import com.arcgismaps.geometry.Point
    import com.arcgismaps.geometry.Polygon
    import com.arcgismaps.geometry.Polyline
    import com.arcgismaps.geometry.SpatialReference
    import com.arcgismaps.mapping.ArcGISMap
    import com.arcgismaps.mapping.BasemapStyle
    import com.arcgismaps.mapping.Viewpoint
    import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
    import com.arcgismaps.mapping.view.geometryeditor.VertexTool
    import com.arcgismaps.toolkit.geoviewcompose.MapView
    import com.example.app.R
    import kotlinx.coroutines.launch
    import kotlin.math.round
    class MapViewModel : ViewModel() {
    // Create a map using the basemap style ArcGISTopographic.
    val map = ArcGISMap(BasemapStyle.ArcGISTopographic).apply {
    initialViewpoint = Viewpoint(
    center = Point(
    x = -1.880843,
    y = 53.479979,
    spatialReference = SpatialReference.wgs84()
    ),
    scale = 10_000_000.0
    )
    }
    val geometryEditor = GeometryEditor()
    // Create mutable state variables for displaying the current planar and geodetic measurements.
    var geodeticMeasurement by mutableStateOf("0")
    var planarMeasurement by mutableStateOf("0")
    var measureType by mutableStateOf("measurement")
    var displayUnits by mutableStateOf("")
    /**
    * Reset the strings that display measurements.
    */
    fun resetDisplayText() {
    displayUnits = ""
    measureType = "measurement"
    geodeticMeasurement = "0"
    planarMeasurement = "0"
    }
    /**
    * Log errors.
    */
    private fun logError(error: Throwable) {
    Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)
    }
    /**
    * Starts the GeometryEditor using the selected [GeometryType].
    */
    fun startEditor(selectedGeometry: GeometryType) {
    geometryEditor.apply {
    // Stops the current editing session
    stop()
    // Start new editing session
    start(selectedGeometry)
    }
    }
    /**
    * Deletes the selected element and stops the geometry editor if there are no
    * more elements in the geometry.
    */
    fun deleteSelection() {
    if (geometryEditor.geometry.value?.isEmpty == true) {
    geometryEditor.stop()
    return
    }
    // Select the entire geometry instead of the just the most recent edit.
    geometryEditor.selectGeometry()
    val selectedElement = geometryEditor.selectedElement.value
    if (selectedElement?.canDelete == true) {
    geometryEditor.deleteSelectedElement()
    }
    resetDisplayText()
    }
    /**
    * Calculates the geometry's measurement.
    */
    private fun calculateMeasurements() {
    // Retrieve the latest state of the geometry using the geometry editor.
    val geometryToMeasure = geometryEditor.geometry.value
    ?: return logError(Exception("Geometry passed is null."))
    // Simplify the geometry to make it topologically consistent according to its type.
    // Among other things, simplifying ensures that areas are never returned as negative values.
    val simplifiedGeometry = GeometryEngine.simplifyOrNull(geometry = geometryToMeasure)
    ?: return logError(Exception("Failed to simplify the geometry"))
    val spatialReferenceOfGeometry = simplifiedGeometry.spatialReference
    ?: return logError(Exception("The geometry has no spatial reference"))
    when (simplifiedGeometry) {
    is Polyline -> {
    // Get the geodetic length.
    val lengthGeodetic = GeometryEngine.lengthGeodetic(
    geometry = simplifiedGeometry,
    lengthUnit = LinearUnit.kilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar length, which is returned in meters.
    val lengthPlanar = GeometryEngine.length(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = lengthGeodetic,
    measurementPlanar = lengthPlanar,
    geometryType = GeometryType.Polyline,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    is Polygon -> {
    // Get the geodetic area.
    val areaGeodetic = GeometryEngine.areaGeodetic(
    geometry = simplifiedGeometry,
    unit = AreaUnit.squareKilometers,
    curveType = GeodeticCurveType.Geodesic
    )
    // Get the planar area, which is returned in square meters.
    val areaPlanar = GeometryEngine.area(geometry = simplifiedGeometry)
    // Update UI fields using the GeometryEngine results.
    displayMeasurements(
    measurementGeodetic = areaGeodetic,
    measurementPlanar = areaPlanar,
    geometryType = GeometryType.Polygon,
    spatialReferenceUnit = spatialReferenceOfGeometry.unit
    )
    }
    else -> {}
    }
    }
    /**
    * Rounds number to 3 decimal places.
    */
    private fun Double.roundToThreeDecimals(): Double {
    return round(this * 1000.0) / 1000.0
    }
    private fun displayMeasurements(
    measurementGeodetic: Double,
    measurementPlanar: Double,
    geometryType: GeometryType,
    spatialReferenceUnit: MeasurementUnit
    ) {
    when (geometryType) {
    GeometryType.Polyline -> {
    displayUnits = "km"
    measureType = "length"
    // Round the geodetic measurement. Then display it.
    geodeticMeasurement = measurementGeodetic.roundToThreeDecimals().toString()
    // If a spatial reference is projected, its measurement unit is LinearUnit. In this tutorial, you can tell
    // the spatial reference by inspection. However, for more complex apps, it is good practice to check.
    if (spatialReferenceUnit is LinearUnit) {
    val convertedPlanarMeasurement = spatialReferenceUnit.convertTo(
    toUnit = LinearUnit.kilometers,
    value = measurementPlanar
    )
    planarMeasurement = convertedPlanarMeasurement.roundToThreeDecimals().toString()
    }
    }
    GeometryType.Polygon -> {
    displayUnits = "km²"
    measureType = "area"
    // Round the geodetic measurement. Then display it.
    geodeticMeasurement = measurementGeodetic.roundToThreeDecimals().toString()
    // If a spatial reference is projected, its measurement unit is LinearUnit. In this tutorial, you can tell
    // the spatial reference by inspection. However, for more complex apps, it is good practice to check.
    if (spatialReferenceUnit is LinearUnit) {
    val convertedPlanarMeasurement = AreaUnit(spatialReferenceUnit).convertTo(
    toUnit = AreaUnit.squareKilometers,
    area = measurementPlanar
    )
    planarMeasurement = convertedPlanarMeasurement.roundToThreeDecimals().toString()
    }
    }
    else -> {}
    }
    }
    /**
    * Load the map and set GeometryEditor tool after the map is loaded.
    */
    init {
    viewModelScope.launch {
    // Load the map.
    map.load().onFailure { error ->
    logError(error)
    }
    // Set Geometry Editor tool when map is loaded.
    geometryEditor.tool = VertexTool()
    viewModelScope.launch {
    geometryEditor.geometry.collect {
    // Update the length/area measurement as the geometry changes.
    calculateMeasurements()
    }
    }
    }
    }
    }
    @Composable
    fun MainScreen() {
    // Create a ViewModel to handle MapView interactions.
    val mapViewModel: MapViewModel = viewModel()
    Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) {
    Column(
    modifier = Modifier
    .fillMaxSize()
    .padding(it)
    ) {
    MapView(
    modifier = Modifier
    .fillMaxSize()
    .weight(1f),
    arcGISMap = mapViewModel.map,
    geometryEditor = mapViewModel.geometryEditor
    )
    GeometryOptions(mapViewModel)
    }
    }
    }
    @Composable
    fun GeometryOptions(mapViewModel: MapViewModel) {
    Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
    Text(text = "Geodesic ${mapViewModel.measureType}: ${mapViewModel.geodeticMeasurement} ${mapViewModel.displayUnits}")
    Text(text = "Planar ${mapViewModel.measureType}: ${mapViewModel.planarMeasurement} ${mapViewModel.displayUnits}")
    HorizontalDivider()
    Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.SpaceEvenly
    ) {
    // Default to an unselected state.
    var selectedIndex by remember { mutableIntStateOf(-1) }
    // A two-choice button: Polyline and Polygon tools.
    SingleChoiceSegmentedButtonRow {
    // Create segmented buttons for each Geometry.
    listOf("Polyline", "Polygon").forEachIndexed { index, label ->
    SegmentedButton(
    shape = SegmentedButtonDefaults.itemShape(index = index, count = 2),
    selected = index == selectedIndex,
    onClick = {
    // Update the selected index.
    selectedIndex = index
    // Reset UI text fields.
    mapViewModel.resetDisplayText()
    // Start the GeometryEditor
    mapViewModel.startEditor(
    selectedGeometry = if (index == 0) GeometryType.Polyline else GeometryType.Polygon
    )
    }
    ) { Text(label) }
    }
    }
    // The Delete tool uses the default delete icon: trash can.
    OutlinedIconButton(onClick = mapViewModel::deleteSelection) {
    Icon(
    imageVector = Icons.Default.Delete,
    contentDescription = "Delete"
    )
    }
    }
    }
    }

Click Run > Run > app to run the app.

You should see a map centered on England with two buttons at the bottom of the screen. Click the Polyline button, and tap at least two points. The geodetic and planar length of the polyline should display below the map. Then click the Polygon button, and tap at least three points. The geodetic and planar area of the polygon should display.

Project geometries for better planar results (optional)

In this app, the map’s spatial reference is the projected coordinate system Web Mercator, which determines the planar measurements. Compare geodetic vs. planar results:

GeometryGeoedeticPlanar
London-Edinburgh533 km903 km
London-Edinburgh-Cardiff52790 km²143041 km²

These discrepancies are large, even in a relatively small region.

Requesting geodetic length or area returns reliable results. Although planar results are less accurate than geodetic ones, some projected spatial references yield more accurate results than others, particularly in relatively “narrow” regions. For more useful planar results within the United Kingdom, try using the British National Grid (BNG) Coordinate System (WKID = 27700).

Try these minor changes to the code:

  1. In calculateMeasurements(), add code that projects geomteryToMeasure to the British National Grid.

    MainScreen.kt
    private fun calculateMeasurements() {
    val geometryToMeasure = geometryEditor.geometry.value
    ?: return logError(Exception("Geometry passed is null."))
    val projectedGeometry = GeometryEngine.projectOrNull(
    geometry = geometryToMeasure,
    spatialReference = SpatialReference(27700)
    ) ?: return logError(Exception("Failed to project geometry"))
  2. Modify the simplifyOrNull() call by passing projectedGeometry instead of geometryToMeasure.

    MainScreen.kt
    val simplifiedGeometry = GeometryEngine.simplifyOrNull(geometry = projectedGeometry)
    ?: return logError(Exception("Failed to simplify the geometry"))
  3. Run the app, and tap the Polyline tool. Tap London and then Edinburgh. For comparison with Web Mercator: The discrepancy is now about 0.2 km. That is a decided improvement.

  4. Now tap the Polygon tool. Tap London, Edinburgh, and Cardiff. For comparison with Web Mercator: The discrepancy is now about 66 km².

Note that the discrepancies increase as you measure distances and areas outside the BNG. For example, measure the length between London and San Francisco.

There are many other projections that optimize certain kinds of measurement. Examples include the (1) various StatePlane projections in the United States and (2) UTM with an appropriate zone throughout most of the world.

Alternatively, you can download the tutorial solution, as follows.

Option 2: Download the solution

  1. Click the Download solution link in the right-hand side of this page.

  2. Unzip the file to a location on your machine.

  3. Run Android Studio.

  4. Go to File > Open…. Navigate to the solution folder and click Open.

    On Windows: If you are in the Welcome to Android Studio dialog, click Open and navigate to the solution folder. Then click Open.

Since the downloaded solution does not contain authentication credentials, you must first set up authentication to create credentials, and then add the developer credentials to the solution.

Set up authentication

To access the secure ArcGIS location services ArcGIS Location Services, also referred to as Location Services, are services hosted by Esri that provide geospatial functionality for developing mapping applications. They include the ArcGIS Basemap Styles service, ArcGIS Static Basemap Tiles service, ArcGIS Places service, ArcGIS Geocoding service, ArcGIS Routing service, ArcGIS GeoEnrichment service, and ArcGIS Elevation service. An ArcGIS Location Platform or ArcGIS Online account is required to use the services. Learn more used in this tutorial, you must implement API key authentication API key authentication is a type of authentication that uses an API key to authenticate requests to ArcGIS services and secure portal items. Learn more or user authentication User authentication is a type of authentication that allows users with an ArcGIS account to sign into an application and allow it to access ArcGIS content, services, and resources on their behalf. The typical authorization protocol used is OAuth2.0. Learn more using an ArcGIS Location Platform An ArcGIS Location Platform account, formerly known as an ArcGIS Developer account, is an identity associated with an ArcGIS Location Platform subscription. Learn more or an ArcGIS Online An ArcGIS Online account, also known as an ArcGIS Organization account, is an identity associated with an ArcGIS Online subscription. It can be used to access ArcGIS tools and develop applications with ArcGIS location services for an organization. Learn more account.

To complete this tutorial, click on the tab in the switcher below for your authentication type of choice, either API key authentication or User authentication.

Create a new API key access token An access token is an authorization string that provides access to secure ArcGIS content, data, and services. Its capabilities are determined by the privileges it supports. It is obtained by implementing API key authentication, User authentication, or App authentication. Learn more with privileges Privileges are a set of permissions assigned to ArcGIS accounts, developer credentials, and applications that grant access to secure resources and functionality in ArcGIS. Learn more to access the secure resources used in this tutorial.

  1. Complete the Create an API key tutorial and create an API key with the following privilege(s) Privileges are a set of permissions assigned to ArcGIS accounts, developer credentials, and applications that grant access to secure resources and functionality in ArcGIS. Learn more :

    • Privileges
      • Location services > Basemaps
  2. Copy and paste the API key access token into a safe location. It will be used in a later step.

Set developer credentials in the solution

To allow your app users to access ArcGIS location services ArcGIS Location Services, also referred to as Location Services, are services hosted by Esri that provide geospatial functionality for developing mapping applications. They include the ArcGIS Basemap Styles service, ArcGIS Static Basemap Tiles service, ArcGIS Places service, ArcGIS Geocoding service, ArcGIS Routing service, ArcGIS GeoEnrichment service, and ArcGIS Elevation service. An ArcGIS Location Platform or ArcGIS Online account is required to use the services. Learn more , use the developer credentials that you created in the Set up authentication step to authenticate requests for resources.

  1. In the Android view of Android Studio, open app > kotlin+java > com.example.app > MainActivity. Set the AuthenticationMode to .API_KEY.

    MainActivity.kt
    14 collapsed lines
    package com.example.app
    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.activity.enableEdgeToEdge
    import com.arcgismaps.ApiKey
    import com.arcgismaps.ArcGISEnvironment
    import com.arcgismaps.httpcore.authentication.OAuthUserConfiguration
    import com.arcgismaps.toolkit.authentication.AuthenticatorState
    import com.arcgismaps.toolkit.authentication.DialogAuthenticator
    import com.example.app.screens.MainScreen
    import com.example.app.ui.theme.TutorialTheme
    class MainActivity : ComponentActivity() {
    private enum class AuthenticationMode { API_KEY, USER_AUTH }
    private val authenticationMode = AuthenticationMode.API_KEY
    42 collapsed lines
    private val authenticatorState = AuthenticatorState()
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    when (authenticationMode) {
    AuthenticationMode.API_KEY -> {
    ArcGISEnvironment.apiKey = ApiKey.create("YOUR_ACCESS_TOKEN")
    }
    AuthenticationMode.USER_AUTH -> {
    authenticatorState.oAuthUserConfigurations = listOf(
    OAuthUserConfiguration(
    portalUrl = "https://www.arcgis.com",
    clientId = "YOUR_CLIENT_ID",
    redirectUrl = "YOUR_REDIRECT_URL"
    )
    )
    }
    }
    enableEdgeToEdge()
    setContent {
    TutorialTheme {
    MainScreen()
    if (authenticationMode == AuthenticationMode.USER_AUTH) {
    DialogAuthenticator(authenticatorState)
    }
    }
    }
    }
    }
  2. Set the apiKey property with your API key access token.

    MainActivity.kt
    22 collapsed lines
    package com.example.app
    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.activity.enableEdgeToEdge
    import com.arcgismaps.ApiKey
    import com.arcgismaps.ArcGISEnvironment
    import com.arcgismaps.httpcore.authentication.OAuthUserConfiguration
    import com.arcgismaps.toolkit.authentication.AuthenticatorState
    import com.arcgismaps.toolkit.authentication.DialogAuthenticator
    import com.example.app.screens.MainScreen
    import com.example.app.ui.theme.TutorialTheme
    class MainActivity : ComponentActivity() {
    private enum class AuthenticationMode { API_KEY, USER_AUTH }
    private val authenticationMode = AuthenticationMode.API_KEY
    private val authenticatorState = AuthenticatorState()
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    when (authenticationMode) {
    AuthenticationMode.API_KEY -> {
    ArcGISEnvironment.apiKey = ApiKey.create("YOUR_ACCESS_TOKEN")
    }
    30 collapsed lines
    AuthenticationMode.USER_AUTH -> {
    authenticatorState.oAuthUserConfigurations = listOf(
    OAuthUserConfiguration(
    portalUrl = "https://www.arcgis.com",
    clientId = "YOUR_CLIENT_ID",
    redirectUrl = "YOUR_REDIRECT_URL"
    )
    )
    }
    }
    enableEdgeToEdge()
    setContent {
    TutorialTheme {
    MainScreen()
    if (authenticationMode == AuthenticationMode.USER_AUTH) {
    DialogAuthenticator(authenticatorState)
    }
    }
    }
    }
    }

Best Practice: The access token is stored directly in the code as a convenience for this tutorial. Do not store credentials directly in source code in a production environment.

Run the app

Click Run > Run > app to run the app.

You should see a map centered on England with two buttons at the bottom of the screen. Click the Polyline button, and tap at least two points. The geodetic and planar length of the polyline should display below the map. Then click the Polygon button, and tap at least three points. The geodetic and planar area of the polygon should display.

What’s next?

Learn how to use additional API features, ArcGIS location services, and ArcGIS tools in these tutorials: