Learn how to display geometries in different projections.

A geometry projection transforms the vertices of a geometric shape from one coordinate system
Each projection
In this tutorial, you will access a feature service
For detailed information on projected coordinate systems, including well-known IDs (WKIDs), areas of use, and maximum/minimum latitude and longitude, download the Coordinate systems and transformation zip file and see the Projected Coordinate System tables PDF.
For general information on spatial references, see Spatial references in Reference topics.
For specific information on spatial references in ArcGIS Maps SDKs for Native Apps, see Spatial references.
Prerequisites
Before starting this tutorial, you need the following:
-
An ArcGIS Location Platform or ArcGIS Online account.
-
A development and deployment environment that meets the system requirements.
-
An IDE for Android development in Kotlin.
Develop or download
You have two options for completing this tutorial:
Option 1: Develop the code
Open an Android Studio project
-
Open the project you created by completing the Display a map tutorial.
-
Continue with the following instructions to display geometries in different projections.
-
Modify the old project for use in this new tutorial.
-
On your file system, delete the .idea folder, if present, at the top level of your project.
-
In the Android view, open app > res > values > strings.xml.
In the
<string name="app_name">element, change the text content to Display projected geometries.strings.xml<resources><string name="app_name">Display projected geometries</string></resources> -
In the Android view, open Gradle Scripts > settings.gradle.kts.
Change the value of
rootProject.nameto “Display projected geometries”.settings.gradle.kts14 collapsed linespluginManagement {repositories {google {content {includeGroupByRegex("com\\.android.*")includeGroupByRegex("com\\.google.*")includeGroupByRegex("androidx.*")}}mavenCentral()gradlePluginPortal()}}dependencyResolutionManagement {repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)repositories {google()mavenCentral()maven { url = uri("https://esri.jfrog.io/artifactory/arcgis") }}}rootProject.name = "Display projected geometries"include(":app") -
Click File > Sync Project with Gradle files. Android Studio will recognize your changes and create a new .idea folder.
-
Add imports
Modify import statements to reference the packages and classes required for this tutorial.
@file:OptIn(ExperimentalMaterial3Api::class)
package com.example.app.screens
import android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.PaddingValuesimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.ExposedDropdownMenuBoximport androidx.compose.material3.ExposedDropdownMenuDefaultsimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.MenuAnchorTypeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TextFieldimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.text.TextStyleimport androidx.compose.ui.unit.TextUnitimport androidx.compose.ui.unit.TextUnitTypeimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.data.ServiceFeatureTableimport com.arcgismaps.geometry.Envelopeimport com.arcgismaps.geometry.GeodeticCurveTypeimport com.arcgismaps.geometry.GeometryEngineimport com.arcgismaps.geometry.LinearUnitimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.layers.FeatureLayerimport com.arcgismaps.mapping.symbology.SimpleFillSymbolimport com.arcgismaps.mapping.symbology.SimpleFillSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaultsimport com.arcgismaps.toolkit.geoviewcompose.MapViewScopeimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundAdd utilities
Delete some unneeded code inherited from the Display a map tutorial. Then add some utilities.
-
In
MainScreen.kt, delete the body of theMainScreen()composable and the entirecreateMap()function.MainScreen.kt@Composablefun 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)}} -
At the top-level of the file, create two utilities that will be used throughout this tutorial: a function to log errors and an extension function to round
Doublevalues to five decimal places.Top-level in MainScreen.ktprivate fun logError(error: Throwable) {Log.e("MainScreen.kt", error.message.toString(), error.cause)}private fun Double.roundToFiveDecimals(): Double {return round(this * 100000.0) / 100000.0}
Create a graphic and a callout
A line graphic will show the full extent of the current map. A callout will indicate a point on the map and display the point’s x/y coordinates.
-
Create a
Graphicto represent the world extentAn extent is a bounding rectangle with points that delineate an area for a map or scene. using dashed-lines to denote the boundaries. Then create aGraphicsOverlayand add the world boundary graphic to it. The envelope for the extent is expressed in WGS84, but the vertices will be projected on-the-flyOn-the-fly reprojection is a process that takes data stored in latitude and longitude format (unprojected data) and transforms its visual representation to match other projected datasets. to the spatial reference of the map.Top-level in MainScreen.ktprivate val worldBoundaryGraphic = Graphic(symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.transparent,outline = SimpleLineSymbol().apply {color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)width = 0.5fstyle = SimpleLineSymbolStyle.Dash}),// The envelope covers the world extent of the map.geometry = Envelope(xMin = -180.0,yMin = -90.0,xMax = 180.0,yMax = 90.0,spatialReference = SpatialReference.wgs84()))private val boundaryGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(worldBoundaryGraphic))) -
Create a variable with mutable state named
calloutPoint. Initialize it to (10.0, -30.0), specifying the WGS84 spatial reference.Top-level in MainScreen.kt// Keep track of the state of the callout point.private var calloutPoint: Point by mutableStateOf(Point(x = -10.0, y = 30.0, SpatialReference.wgs84())) -
Define a composable function named
CalloutContainerthat adds the ArcGIS Maps SDK for Kotlin ToolkitCalloutcomponent. TheCalloutContainertakes aMapViewScopeparameter.Note that the callout will be visible when the app launches.
Top-level in MainScreen.kt97 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.PaddingValuesimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.ExposedDropdownMenuBoximport androidx.compose.material3.ExposedDropdownMenuDefaultsimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.MenuAnchorTypeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TextFieldimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.text.TextStyleimport androidx.compose.ui.unit.TextUnitimport androidx.compose.ui.unit.TextUnitTypeimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.data.ServiceFeatureTableimport com.arcgismaps.geometry.Envelopeimport com.arcgismaps.geometry.GeodeticCurveTypeimport com.arcgismaps.geometry.GeometryEngineimport com.arcgismaps.geometry.LinearUnitimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.layers.FeatureLayerimport com.arcgismaps.mapping.symbology.SimpleFillSymbolimport com.arcgismaps.mapping.symbology.SimpleFillSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaultsimport com.arcgismaps.toolkit.geoviewcompose.MapViewScopeimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundprivate fun logError(error: Throwable) {Log.e("MainScreen.kt", error.message.toString(), error.cause)}private fun Double.roundToFiveDecimals(): Double {return round(this * 100000.0) / 100000.0}private val worldBoundaryGraphic = Graphic(symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.transparent,outline = SimpleLineSymbol().apply {color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)width = 0.5fstyle = SimpleLineSymbolStyle.Dash}),// The envelope covers the world extent of the map.geometry = Envelope(xMin = -180.0,yMin = -90.0,xMax = 180.0,yMax = 90.0,spatialReference = SpatialReference.wgs84()))private val boundaryGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(worldBoundaryGraphic)))// Keep track of the state of the callout point.private var calloutPoint: Point by mutableStateOf(Point(x = -10.0, y = 30.0, SpatialReference.wgs84()))// Show a callout using the callout point for location and text.@Composableprivate fun CalloutContainer(mapViewScope: MapViewScope) {mapViewScope.Callout(location = calloutPoint,// Optional parameters to customize the callout appearance.shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))) {Column {Text(text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",style = MaterialTheme.typography.labelSmall)}}}5 collapsed lines@Composablefun MainScreen() {}
Create a blank map and a feature layer
In this tutorial, you will create a mapBasemap. A feature layer
First, create a new map.
-
Create a mutable state
ArcGISMapwith the WGS84 spatial referenceA spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. and a white background to hide the grid. Note that the mapA map is a collection of layers that are displayed in 2D. It is typically composed of a basemap layer and data layers. starts out as blank.Top-level in MainScreen.ktprivate var map by mutableStateOf(ArcGISMap(SpatialReference.wgs84()).apply {backgroundColor = Color.white}) -
Create a
ServiceFeatureTablefrom the feature serviceA feature service is a data service that provides access to spatial and non-spatial data in feature layers, feature layer views, and tables. . Then create aFeatureLayerfrom that table.Top-level in MainScreen.kt120 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.PaddingValuesimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.ExposedDropdownMenuBoximport androidx.compose.material3.ExposedDropdownMenuDefaultsimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.MenuAnchorTypeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TextFieldimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.text.TextStyleimport androidx.compose.ui.unit.TextUnitimport androidx.compose.ui.unit.TextUnitTypeimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.data.ServiceFeatureTableimport com.arcgismaps.geometry.Envelopeimport com.arcgismaps.geometry.GeodeticCurveTypeimport com.arcgismaps.geometry.GeometryEngineimport com.arcgismaps.geometry.LinearUnitimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.layers.FeatureLayerimport com.arcgismaps.mapping.symbology.SimpleFillSymbolimport com.arcgismaps.mapping.symbology.SimpleFillSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaultsimport com.arcgismaps.toolkit.geoviewcompose.MapViewScopeimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundprivate fun logError(error: Throwable) {Log.e("MainScreen.kt", error.message.toString(), error.cause)}private fun Double.roundToFiveDecimals(): Double {return round(this * 100000.0) / 100000.0}private val worldBoundaryGraphic = Graphic(symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.transparent,outline = SimpleLineSymbol().apply {color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)width = 0.5fstyle = SimpleLineSymbolStyle.Dash}),// The envelope covers the world extent of the map.geometry = Envelope(xMin = -180.0,yMin = -90.0,xMax = 180.0,yMax = 90.0,spatialReference = SpatialReference.wgs84()))private val boundaryGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(worldBoundaryGraphic)))// Keep track of the state of the callout point.private var calloutPoint: Point by mutableStateOf(Point(x = -10.0, y = 30.0, SpatialReference.wgs84()))// Show a callout using the callout point for location and text.@Composableprivate fun CalloutContainer(mapViewScope: MapViewScope) {mapViewScope.Callout(location = calloutPoint,// Optional parameters to customize the callout appearance.shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))) {Column {Text(text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",style = MaterialTheme.typography.labelSmall)}}}private var map by mutableStateOf(ArcGISMap(SpatialReference.wgs84()).apply {backgroundColor = Color.white})private val serviceFeatureTable = ServiceFeatureTable(uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0")private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)5 collapsed lines@Composablefun MainScreen() {}
Add feature layer and project callout point
When the map’s spatial reference is ready to access, the MapView invokes a callback, where you can add the feature layer to the map and project the callout point.
-
Create the callback
onSpatialReferenceChanged()as a suspend function that takes the spatial referenceA spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. . Then add the feature layerA feature layer (client-side) is a data layer that can access and display features from a feature service that has the same type of geometry and attribute fields. to the map’s operational layersAn operational layer is a layer used by a map or a scene to visualize geographic data. Operational layers are displayed on top of a basemap layer. , and load the layer.Top-level in MainScreen.ktprivate suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {map.operationalLayers.add(featureLayer)featureLayer.load().onFailure { error ->logError(error)}} -
Project the callout point to the spatial reference
A spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. .Top-level in MainScreen.kt126 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.PaddingValuesimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.ExposedDropdownMenuBoximport androidx.compose.material3.ExposedDropdownMenuDefaultsimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.MenuAnchorTypeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TextFieldimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.text.TextStyleimport androidx.compose.ui.unit.TextUnitimport androidx.compose.ui.unit.TextUnitTypeimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.data.ServiceFeatureTableimport com.arcgismaps.geometry.Envelopeimport com.arcgismaps.geometry.GeodeticCurveTypeimport com.arcgismaps.geometry.GeometryEngineimport com.arcgismaps.geometry.LinearUnitimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.layers.FeatureLayerimport com.arcgismaps.mapping.symbology.SimpleFillSymbolimport com.arcgismaps.mapping.symbology.SimpleFillSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaultsimport com.arcgismaps.toolkit.geoviewcompose.MapViewScopeimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundprivate fun logError(error: Throwable) {Log.e("MainScreen.kt", error.message.toString(), error.cause)}private fun Double.roundToFiveDecimals(): Double {return round(this * 100000.0) / 100000.0}private val worldBoundaryGraphic = Graphic(symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.transparent,outline = SimpleLineSymbol().apply {color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)width = 0.5fstyle = SimpleLineSymbolStyle.Dash}),// The envelope covers the world extent of the map.geometry = Envelope(xMin = -180.0,yMin = -90.0,xMax = 180.0,yMax = 90.0,spatialReference = SpatialReference.wgs84()))private val boundaryGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(worldBoundaryGraphic)))// Keep track of the state of the callout point.private var calloutPoint: Point by mutableStateOf(Point(x = -10.0, y = 30.0, SpatialReference.wgs84()))// Show a callout using the callout point for location and text.@Composableprivate fun CalloutContainer(mapViewScope: MapViewScope) {mapViewScope.Callout(location = calloutPoint,// Optional parameters to customize the callout appearance.shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))) {Column {Text(text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",style = MaterialTheme.typography.labelSmall)}}}private var map by mutableStateOf(ArcGISMap(SpatialReference.wgs84()).apply {backgroundColor = Color.white})private val serviceFeatureTable = ServiceFeatureTable(uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0")private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {map.operationalLayers.add(featureLayer)featureLayer.load().onFailure { error ->logError(error)}// Project the callout point so it has the right spatial reference when used by the callout.calloutPoint = GeometryEngine.projectOrNull(geometry = calloutPoint,spatialReference = newSpatialReference) ?: return logError(Exception("Callout point's projection is null."))}5 collapsed lines@Composablefun MainScreen() {}
Enable user to select a spatial reference
The user will select a spatial reference by name from a drop-down menu to see the feature layer projected in a different spatial reference.
The projection’s name is the same as the short name of the spatial reference to which it is projecting (the output spatial reference of the projection).
Your finished app will have two drop-down menus, one above the other. The upper menu displays the projection types, such as “maintain area” or “maintain length”. The lower menu displays the projection names for that type.
-
Create an enum that contains a constant for each projection that a user can select. Each enum constant has two properties:
labelandwkid.Top-level in MainScreen.kt// List of various projection names along with their respective label and wkid.// The name of a projection is the spatial reference it projects to.private enum class ProjectionName(val label: String, val wkid: Int) {WGS84(label = "WGS84 (GCS) -> pseudo Plate Carrée (Cylindrical)", wkid = 4326),WorldCassini(label = "World Cassini (Cylindrical)", wkid = 54028),WorldEquidistant(label = "World Equidistant conic (Conic)", wkid = 54027),WorldStereographic(label = "World Stereographic (Azimuthal)", wkid = 54026),WorldEckertVI(label = "World Eckert VI (Pseudocylindrical)", wkid = 54010),WorldSinusoidal(label = "World Sinusoidal (Pseudocylindrical)", wkid = 54008),NorthPoleGnomonic(label = "North Pole Gnomonic (Azimuthal", wkid = 102034),WebMercator(label = "Web Mercator Auxiliary Sphere (Cylindrical)", wkid = 3857),WorldGallStereographic(label = "World Gall Stereographic (Cylindrical)", wkid = 54016),WorldWinkelTripel(label = "World Winkel Tripel (Pseudoazimuthal)", wkid = 54042),WorldFullerDymaxionMap(label = "World Fuller / Dymaxion map (Polyhedral)", wkid = 54050)} -
Create a list of projection types. Each item in the list is a
Pairin which the first element is the projection type and the second element is a list of projection names that have that type. Note that each projection name belongs to only one projection type.Top-level in MainScreen.kt// List of various projection types long with their respective list of spatial references.private val projectionTypes = listOf("Equidistant (maintain length)" to listOf(ProjectionName.WGS84,ProjectionName.WorldCassini,ProjectionName.WorldEquidistant),"Conformal (maintain angles)" to listOf(ProjectionName.WorldStereographic),"Equal-area (maintain area)" to listOf(ProjectionName.WorldEckertVI,ProjectionName.WorldSinusoidal),"Gnomonic (distances)" to listOf(ProjectionName.NorthPoleGnomonic),"Compromise (distort all)" to listOf(ProjectionName.WebMercator,ProjectionName.WorldGallStereographic,ProjectionName.WorldWinkelTripel,ProjectionName.WorldFullerDymaxionMap)) -
Create a composable named
ProjectionByTypeDropdownMenuthat will contain the two drop-down menus.Define state for the drop-down menus:
isProjectionTypeExpanded: Boolean for whether the projection type drop-down menu is currently expanded.isProjectionNameExpanded: Boolean for whether the projection name drop-down menu is currently expanded.selectedProjectionTypeIndex: Index of the current selection in the projection type menu.selectedProjectionNameIndex: Index of the current selection in the projection name menu.
Top-level in MainScreen.kt@OptIn(ExperimentalMaterial3Api::class)@Composableprivate fun ProjectionByTypeDropdownMenu(onProjectionSelected: (ProjectionName) -> Unit) {// Expanded boolean for the projection type drop-down menu.var isProjectionTypeExpanded by remember { mutableStateOf(false) }// Expanded boolean for the project name drop-down menu.var isProjectionNameExpanded by remember { mutableStateOf(false) }// The index of the current selection in project types menu.var selectedProjectionTypeIndex by remember { mutableIntStateOf(0) }// The index of the current selection in the project name menu.var selectedProjectionNameIndex by remember { mutableIntStateOf(0) }} -
Inside the new
ProjectionByTypeDropdownMenucomposable, create the upper drop-down menu for selecting the projection type. Add anExposedDropDownMenuBox.In ProjectionByTypeDropdownMenu()ExposedDropdownMenuBox(modifier = Modifier.fillMaxWidth(),expanded = isProjectionTypeExpanded,onExpandedChange = { isProjectionTypeExpanded = !isProjectionTypeExpanded }) {TextField(label = { Text("Select a projection type") },value = projectionTypes[selectedProjectionTypeIndex].first,onValueChange = {},readOnly = true,trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionTypeExpanded) },modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable, true))ExposedDropdownMenu(expanded = isProjectionTypeExpanded,onDismissRequest = { isProjectionTypeExpanded = false }) {projectionTypes.forEachIndexed { index, selectedOption ->DropdownMenuItem(text = { Text(text = selectedOption.first) },onClick = {selectedProjectionTypeIndex = indexisProjectionTypeExpanded = falseselectedProjectionNameIndex = 0onProjectionSelected(selectedOption.second[0])})}}} -
Create the lower drop-down menu for selecting projections of a selected type. Add an
ExposedDropDownMenuBox.In ProjectionByTypeDropdownMenu()224 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.PaddingValuesimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.ExposedDropdownMenuBoximport androidx.compose.material3.ExposedDropdownMenuDefaultsimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.MenuAnchorTypeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TextFieldimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.text.TextStyleimport androidx.compose.ui.unit.TextUnitimport androidx.compose.ui.unit.TextUnitTypeimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.data.ServiceFeatureTableimport com.arcgismaps.geometry.Envelopeimport com.arcgismaps.geometry.GeodeticCurveTypeimport com.arcgismaps.geometry.GeometryEngineimport com.arcgismaps.geometry.LinearUnitimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.layers.FeatureLayerimport com.arcgismaps.mapping.symbology.SimpleFillSymbolimport com.arcgismaps.mapping.symbology.SimpleFillSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaultsimport com.arcgismaps.toolkit.geoviewcompose.MapViewScopeimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundprivate fun logError(error: Throwable) {Log.e("MainScreen.kt", error.message.toString(), error.cause)}private fun Double.roundToFiveDecimals(): Double {return round(this * 100000.0) / 100000.0}private val worldBoundaryGraphic = Graphic(symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.transparent,outline = SimpleLineSymbol().apply {color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)width = 0.5fstyle = SimpleLineSymbolStyle.Dash}),// The envelope covers the world extent of the map.geometry = Envelope(xMin = -180.0,yMin = -90.0,xMax = 180.0,yMax = 90.0,spatialReference = SpatialReference.wgs84()))private val boundaryGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(worldBoundaryGraphic)))// Keep track of the state of the callout point.private var calloutPoint: Point by mutableStateOf(Point(x = -10.0, y = 30.0, SpatialReference.wgs84()))// Show a callout using the callout point for location and text.@Composableprivate fun CalloutContainer(mapViewScope: MapViewScope) {mapViewScope.Callout(location = calloutPoint,// Optional parameters to customize the callout appearance.shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))) {Column {Text(text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",style = MaterialTheme.typography.labelSmall)}}}private var map by mutableStateOf(ArcGISMap(SpatialReference.wgs84()).apply {backgroundColor = Color.white})private val serviceFeatureTable = ServiceFeatureTable(uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0")private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {map.operationalLayers.add(featureLayer)featureLayer.load().onFailure { error ->logError(error)}// Project the callout point so it has the right spatial reference when used by the callout.calloutPoint = GeometryEngine.projectOrNull(geometry = calloutPoint,spatialReference = newSpatialReference) ?: return logError(Exception("Callout point's projection is null."))}// List of various projection names along with their respective label and wkid.// The name of a projection is the spatial reference it projects to.private enum class ProjectionName(val label: String, val wkid: Int) {WGS84(label = "WGS84 (GCS) -> pseudo Plate Carrée (Cylindrical)", wkid = 4326),WorldCassini(label = "World Cassini (Cylindrical)", wkid = 54028),WorldEquidistant(label = "World Equidistant conic (Conic)", wkid = 54027),WorldStereographic(label = "World Stereographic (Azimuthal)", wkid = 54026),WorldEckertVI(label = "World Eckert VI (Pseudocylindrical)", wkid = 54010),WorldSinusoidal(label = "World Sinusoidal (Pseudocylindrical)", wkid = 54008),NorthPoleGnomonic(label = "North Pole Gnomonic (Azimuthal", wkid = 102034),WebMercator(label = "Web Mercator Auxiliary Sphere (Cylindrical)", wkid = 3857),WorldGallStereographic(label = "World Gall Stereographic (Cylindrical)", wkid = 54016),WorldWinkelTripel(label = "World Winkel Tripel (Pseudoazimuthal)", wkid = 54042),WorldFullerDymaxionMap(label = "World Fuller / Dymaxion map (Polyhedral)", wkid = 54050)}// List of various projection types long with their respective list of spatial references.private val projectionTypes = listOf("Equidistant (maintain length)" to listOf(ProjectionName.WGS84,ProjectionName.WorldCassini,ProjectionName.WorldEquidistant),"Conformal (maintain angles)" to listOf(ProjectionName.WorldStereographic),"Equal-area (maintain area)" to listOf(ProjectionName.WorldEckertVI,ProjectionName.WorldSinusoidal),"Gnomonic (distances)" to listOf(ProjectionName.NorthPoleGnomonic),"Compromise (distort all)" to listOf(ProjectionName.WebMercator,ProjectionName.WorldGallStereographic,ProjectionName.WorldWinkelTripel,ProjectionName.WorldFullerDymaxionMap))@OptIn(ExperimentalMaterial3Api::class)@Composableprivate fun ProjectionByTypeDropdownMenu(onProjectionSelected: (ProjectionName) -> Unit) {// Expanded boolean for the projection type drop-down menu.var isProjectionTypeExpanded by remember { mutableStateOf(false) }// Expanded boolean for the project name drop-down menu.var isProjectionNameExpanded by remember { mutableStateOf(false) }// The index of the current selection in project types menu.var selectedProjectionTypeIndex by remember { mutableIntStateOf(0) }// The index of the current selection in the project name menu.var selectedProjectionNameIndex by remember { mutableIntStateOf(0) }ExposedDropdownMenuBox(modifier = Modifier.fillMaxWidth(),expanded = isProjectionTypeExpanded,onExpandedChange = { isProjectionTypeExpanded = !isProjectionTypeExpanded }) {TextField(label = { Text("Select a projection type") },value = projectionTypes[selectedProjectionTypeIndex].first,onValueChange = {},readOnly = true,trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionTypeExpanded) },modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable, true))ExposedDropdownMenu(expanded = isProjectionTypeExpanded,onDismissRequest = { isProjectionTypeExpanded = false }) {projectionTypes.forEachIndexed { index, selectedOption ->DropdownMenuItem(text = { Text(text = selectedOption.first) },onClick = {selectedProjectionTypeIndex = indexisProjectionTypeExpanded = falseselectedProjectionNameIndex = 0onProjectionSelected(selectedOption.second[0])})}}}ExposedDropdownMenuBox(expanded = isProjectionNameExpanded,onExpandedChange = { isProjectionNameExpanded = !isProjectionNameExpanded }) {TextField(textStyle = TextStyle(fontSize = TextUnit(14f, TextUnitType.Sp)),value = projectionTypes[selectedProjectionTypeIndex].second[selectedProjectionNameIndex].label,label = {Text(text = "Select spacial reference using projection type",fontSize = TextUnit(10f, TextUnitType.Sp))},onValueChange = {},readOnly = true,trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionNameExpanded) },modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable, true))ExposedDropdownMenu(expanded = isProjectionNameExpanded,onDismissRequest = { isProjectionNameExpanded = false }) {projectionTypes[selectedProjectionTypeIndex].second.forEachIndexed { index, selectedOption ->DropdownMenuItem(text = { Text(text = selectedOption.label) },onClick = {selectedProjectionNameIndex = indexisProjectionNameExpanded = falseonProjectionSelected(selectedOption)})}}}7 collapsed lines}@Composablefun MainScreen() {}
Change the spatial reference
To display the feature layer in a new spatial reference, you create a new SpatialReference using the selected projection name, and then create a new map
-
Create a suspend function named
changeSpatialReference()that takes theProjectionNameselected by the user. Create a new spatial referenceA spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. , using thewkidof the selected option. If the new spatial reference is the same as the current one, the user has selected the same projection name in the drop-down menu. Return without creating a new map.Top-level in MainScreen.ktprivate suspend fun changeSpatialReference(selectedOption: ProjectionName) {val newSpatialReference = SpatialReference(selectedOption.wkid)// If user clicked on the same spatial reference then, don't create a new map.if (newSpatialReference == map.spatialReference) return} -
Remove the feature layer
A feature layer (client-side) is a data layer that can access and display features from a feature service that has the same type of geometry and attribute fields. from the operational layersAn operational layer is a layer used by a map or a scene to visualize geographic data. Operational layers are displayed on top of a basemap layer. of the current mapA map is a collection of layers that are displayed in 2D. It is typically composed of a basemap layer and data layers. so the layer is no longer owned by that map. Then create a newArcGISMapusing the new spatial referenceA spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. , and load the map.Top-level in MainScreen.ktprivate suspend fun changeSpatialReference(selectedOption: ProjectionName) {val newSpatialReference = SpatialReference(selectedOption.wkid)// If user clicked on the same spatial reference then, don't create a new map.if (newSpatialReference == map.spatialReference) return// Remove the feature layer on the old map so the layer is free to be used on the new map.map.operationalLayers.remove(featureLayer)map = ArcGISMap(newSpatialReference).apply {backgroundColor = Color.white}map.load().onFailure { error -> logError(error) }}
When the map’s spatial reference is ready to be accessed, the onSpatialReferenceChanged() you created in an earlier step will be automatically invoked.
Create buffer graphics
You will need graphics to display a geodetic buffer and its pointx,y coordinates and a spatial reference.
-
Create graphics
A graphic is a visual element composed of a geometry, symbol, and attributes that is displayed on a map or scene. to display a point and a geodetic buffer around the point. Then create aGraphicsOverlayand add the graphics to it.Top-level in MainScreen.kt275 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.PaddingValuesimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.ExposedDropdownMenuBoximport androidx.compose.material3.ExposedDropdownMenuDefaultsimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.MenuAnchorTypeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TextFieldimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.text.TextStyleimport androidx.compose.ui.unit.TextUnitimport androidx.compose.ui.unit.TextUnitTypeimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.data.ServiceFeatureTableimport com.arcgismaps.geometry.Envelopeimport com.arcgismaps.geometry.GeodeticCurveTypeimport com.arcgismaps.geometry.GeometryEngineimport com.arcgismaps.geometry.LinearUnitimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.layers.FeatureLayerimport com.arcgismaps.mapping.symbology.SimpleFillSymbolimport com.arcgismaps.mapping.symbology.SimpleFillSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaultsimport com.arcgismaps.toolkit.geoviewcompose.MapViewScopeimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundprivate fun logError(error: Throwable) {Log.e("MainScreen.kt", error.message.toString(), error.cause)}private fun Double.roundToFiveDecimals(): Double {return round(this * 100000.0) / 100000.0}private val worldBoundaryGraphic = Graphic(symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.transparent,outline = SimpleLineSymbol().apply {color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)width = 0.5fstyle = SimpleLineSymbolStyle.Dash}),// The envelope covers the world extent of the map.geometry = Envelope(xMin = -180.0,yMin = -90.0,xMax = 180.0,yMax = 90.0,spatialReference = SpatialReference.wgs84()))private val boundaryGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(worldBoundaryGraphic)))// Keep track of the state of the callout point.private var calloutPoint: Point by mutableStateOf(Point(x = -10.0, y = 30.0, SpatialReference.wgs84()))// Show a callout using the callout point for location and text.@Composableprivate fun CalloutContainer(mapViewScope: MapViewScope) {mapViewScope.Callout(location = calloutPoint,// Optional parameters to customize the callout appearance.shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))) {Column {Text(text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",style = MaterialTheme.typography.labelSmall)}}}private var map by mutableStateOf(ArcGISMap(SpatialReference.wgs84()).apply {backgroundColor = Color.white})private val serviceFeatureTable = ServiceFeatureTable(uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0")private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {map.operationalLayers.add(featureLayer)featureLayer.load().onFailure { error ->logError(error)}// Project the callout point so it has the right spatial reference when used by the callout.calloutPoint = GeometryEngine.projectOrNull(geometry = calloutPoint,spatialReference = newSpatialReference) ?: return logError(Exception("Callout point's projection is null."))}// List of various projection names along with their respective label and wkid.// The name of a projection is the spatial reference it projects to.private enum class ProjectionName(val label: String, val wkid: Int) {WGS84(label = "WGS84 (GCS) -> pseudo Plate Carrée (Cylindrical)", wkid = 4326),WorldCassini(label = "World Cassini (Cylindrical)", wkid = 54028),WorldEquidistant(label = "World Equidistant conic (Conic)", wkid = 54027),WorldStereographic(label = "World Stereographic (Azimuthal)", wkid = 54026),WorldEckertVI(label = "World Eckert VI (Pseudocylindrical)", wkid = 54010),WorldSinusoidal(label = "World Sinusoidal (Pseudocylindrical)", wkid = 54008),NorthPoleGnomonic(label = "North Pole Gnomonic (Azimuthal", wkid = 102034),WebMercator(label = "Web Mercator Auxiliary Sphere (Cylindrical)", wkid = 3857),WorldGallStereographic(label = "World Gall Stereographic (Cylindrical)", wkid = 54016),WorldWinkelTripel(label = "World Winkel Tripel (Pseudoazimuthal)", wkid = 54042),WorldFullerDymaxionMap(label = "World Fuller / Dymaxion map (Polyhedral)", wkid = 54050)}// List of various projection types long with their respective list of spatial references.private val projectionTypes = listOf("Equidistant (maintain length)" to listOf(ProjectionName.WGS84,ProjectionName.WorldCassini,ProjectionName.WorldEquidistant),"Conformal (maintain angles)" to listOf(ProjectionName.WorldStereographic),"Equal-area (maintain area)" to listOf(ProjectionName.WorldEckertVI,ProjectionName.WorldSinusoidal),"Gnomonic (distances)" to listOf(ProjectionName.NorthPoleGnomonic),"Compromise (distort all)" to listOf(ProjectionName.WebMercator,ProjectionName.WorldGallStereographic,ProjectionName.WorldWinkelTripel,ProjectionName.WorldFullerDymaxionMap))@OptIn(ExperimentalMaterial3Api::class)@Composableprivate fun ProjectionByTypeDropdownMenu(onProjectionSelected: (ProjectionName) -> Unit) {// Expanded boolean for the projection type drop-down menu.var isProjectionTypeExpanded by remember { mutableStateOf(false) }// Expanded boolean for the project name drop-down menu.var isProjectionNameExpanded by remember { mutableStateOf(false) }// The index of the current selection in project types menu.var selectedProjectionTypeIndex by remember { mutableIntStateOf(0) }// The index of the current selection in the project name menu.var selectedProjectionNameIndex by remember { mutableIntStateOf(0) }ExposedDropdownMenuBox(modifier = Modifier.fillMaxWidth(),expanded = isProjectionTypeExpanded,onExpandedChange = { isProjectionTypeExpanded = !isProjectionTypeExpanded }) {TextField(label = { Text("Select a projection type") },value = projectionTypes[selectedProjectionTypeIndex].first,onValueChange = {},readOnly = true,trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionTypeExpanded) },modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable, true))ExposedDropdownMenu(expanded = isProjectionTypeExpanded,onDismissRequest = { isProjectionTypeExpanded = false }) {projectionTypes.forEachIndexed { index, selectedOption ->DropdownMenuItem(text = { Text(text = selectedOption.first) },onClick = {selectedProjectionTypeIndex = indexisProjectionTypeExpanded = falseselectedProjectionNameIndex = 0onProjectionSelected(selectedOption.second[0])})}}}ExposedDropdownMenuBox(expanded = isProjectionNameExpanded,onExpandedChange = { isProjectionNameExpanded = !isProjectionNameExpanded }) {TextField(textStyle = TextStyle(fontSize = TextUnit(14f, TextUnitType.Sp)),value = projectionTypes[selectedProjectionTypeIndex].second[selectedProjectionNameIndex].label,label = {Text(text = "Select spacial reference using projection type",fontSize = TextUnit(10f, TextUnitType.Sp))},onValueChange = {},readOnly = true,trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionNameExpanded) },modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable, true))ExposedDropdownMenu(expanded = isProjectionNameExpanded,onDismissRequest = { isProjectionNameExpanded = false }) {projectionTypes[selectedProjectionTypeIndex].second.forEachIndexed { index, selectedOption ->DropdownMenuItem(text = { Text(text = selectedOption.label) },onClick = {selectedProjectionNameIndex = indexisProjectionNameExpanded = falseonProjectionSelected(selectedOption)})}}}}private suspend fun changeSpatialReference(selectedOption: ProjectionName) {val newSpatialReference = SpatialReference(selectedOption.wkid)// If user clicked on the same spatial reference then, don't create a new map.if (newSpatialReference == map.spatialReference) return// Remove the feature layer on the old map so the layer is free to be used on the new map.map.operationalLayers.remove(featureLayer)map = ArcGISMap(newSpatialReference).apply {backgroundColor = Color.white}map.load().onFailure { error -> logError(error) }}private var bufferPointGraphic = Graphic(geometry = null,symbol = SimpleMarkerSymbol(style = SimpleMarkerSymbolStyle.Circle,color = Color.red,size = 5f).apply {outline = SimpleLineSymbol(style = SimpleLineSymbolStyle.Dot,color = Color.white,width = 0.5f,markerStyle = SimpleLineSymbolMarkerStyle.None)})var bufferGraphic = Graphic(geometry = null, symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.fromRgba(r = 150, g = 130, b = 220, a = 216),outline = SimpleLineSymbol(style = SimpleLineSymbolStyle.Dash,color = Color.fromRgba(r = 255, g = 255, b = 255, a = 255),width = 0.5f,markerStyle = SimpleLineSymbolMarkerStyle.None)))private val bufferGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(bufferPointGraphic, bufferGraphic)))5 collapsed lines@Composablefun MainScreen() {}
Visualize spatial distortion effects
When you tap on the map, the buffer graphics display at the tapped point, and the Callout moves to that location. As you tap around the map, the buffer becomes distorted in size and/or shape. The distortions vary, depending on the current spatial reference.
The screen tap results in a single-tap confirmed event, whose mapPoint property returns the tapped point in the spatial reference of the map. Use the map point to create a point buffer and trigger the callout.
-
Create a function that takes a single-tap confirmed event.
Get the map point from the event and call
GeometryEngine.bufferGeodeticOrNull()with the map point, a distance of 1000 kilometers, and curve type of geodesic. Next, display the graphics by assigning the buffer as the geometry of the buffer graphic, and the map point as the geometry of the buffer point graphic.Top-level in MainScreen.ktprivate fun createBuffer(singleTapConfirmedEvent: SingleTapConfirmedEvent) {val mapPoint = singleTapConfirmedEvent.mapPoint?: return logError(Exception("Tap event has no map point."))val buffer = GeometryEngine.bufferGeodeticOrNull(geometry = mapPoint,distance = 1000.0,distanceUnit = LinearUnit.kilometers,maxDeviation = Double.NaN,curveType = GeodeticCurveType.Geodesic) ?: return logError(Exception("Failed to create a buffer from the map point"))bufferGraphic.geometry = bufferbufferPointGraphic.geometry = mapPoint} -
Assign the map point to
calloutPoint. In response, theCalloutwill display at the map point, showing the point’s x/y coordinates in the current spatial reference.Top-level in MainScreen.kt320 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.PaddingValuesimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.ExposedDropdownMenuBoximport androidx.compose.material3.ExposedDropdownMenuDefaultsimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.MenuAnchorTypeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TextFieldimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.text.TextStyleimport androidx.compose.ui.unit.TextUnitimport androidx.compose.ui.unit.TextUnitTypeimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.data.ServiceFeatureTableimport com.arcgismaps.geometry.Envelopeimport com.arcgismaps.geometry.GeodeticCurveTypeimport com.arcgismaps.geometry.GeometryEngineimport com.arcgismaps.geometry.LinearUnitimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.layers.FeatureLayerimport com.arcgismaps.mapping.symbology.SimpleFillSymbolimport com.arcgismaps.mapping.symbology.SimpleFillSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaultsimport com.arcgismaps.toolkit.geoviewcompose.MapViewScopeimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundprivate fun logError(error: Throwable) {Log.e("MainScreen.kt", error.message.toString(), error.cause)}private fun Double.roundToFiveDecimals(): Double {return round(this * 100000.0) / 100000.0}private val worldBoundaryGraphic = Graphic(symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.transparent,outline = SimpleLineSymbol().apply {color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)width = 0.5fstyle = SimpleLineSymbolStyle.Dash}),// The envelope covers the world extent of the map.geometry = Envelope(xMin = -180.0,yMin = -90.0,xMax = 180.0,yMax = 90.0,spatialReference = SpatialReference.wgs84()))private val boundaryGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(worldBoundaryGraphic)))// Keep track of the state of the callout point.private var calloutPoint: Point by mutableStateOf(Point(x = -10.0, y = 30.0, SpatialReference.wgs84()))// Show a callout using the callout point for location and text.@Composableprivate fun CalloutContainer(mapViewScope: MapViewScope) {mapViewScope.Callout(location = calloutPoint,// Optional parameters to customize the callout appearance.shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))) {Column {Text(text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",style = MaterialTheme.typography.labelSmall)}}}private var map by mutableStateOf(ArcGISMap(SpatialReference.wgs84()).apply {backgroundColor = Color.white})private val serviceFeatureTable = ServiceFeatureTable(uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0")private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {map.operationalLayers.add(featureLayer)featureLayer.load().onFailure { error ->logError(error)}// Project the callout point so it has the right spatial reference when used by the callout.calloutPoint = GeometryEngine.projectOrNull(geometry = calloutPoint,spatialReference = newSpatialReference) ?: return logError(Exception("Callout point's projection is null."))}// List of various projection names along with their respective label and wkid.// The name of a projection is the spatial reference it projects to.private enum class ProjectionName(val label: String, val wkid: Int) {WGS84(label = "WGS84 (GCS) -> pseudo Plate Carrée (Cylindrical)", wkid = 4326),WorldCassini(label = "World Cassini (Cylindrical)", wkid = 54028),WorldEquidistant(label = "World Equidistant conic (Conic)", wkid = 54027),WorldStereographic(label = "World Stereographic (Azimuthal)", wkid = 54026),WorldEckertVI(label = "World Eckert VI (Pseudocylindrical)", wkid = 54010),WorldSinusoidal(label = "World Sinusoidal (Pseudocylindrical)", wkid = 54008),NorthPoleGnomonic(label = "North Pole Gnomonic (Azimuthal", wkid = 102034),WebMercator(label = "Web Mercator Auxiliary Sphere (Cylindrical)", wkid = 3857),WorldGallStereographic(label = "World Gall Stereographic (Cylindrical)", wkid = 54016),WorldWinkelTripel(label = "World Winkel Tripel (Pseudoazimuthal)", wkid = 54042),WorldFullerDymaxionMap(label = "World Fuller / Dymaxion map (Polyhedral)", wkid = 54050)}// List of various projection types long with their respective list of spatial references.private val projectionTypes = listOf("Equidistant (maintain length)" to listOf(ProjectionName.WGS84,ProjectionName.WorldCassini,ProjectionName.WorldEquidistant),"Conformal (maintain angles)" to listOf(ProjectionName.WorldStereographic),"Equal-area (maintain area)" to listOf(ProjectionName.WorldEckertVI,ProjectionName.WorldSinusoidal),"Gnomonic (distances)" to listOf(ProjectionName.NorthPoleGnomonic),"Compromise (distort all)" to listOf(ProjectionName.WebMercator,ProjectionName.WorldGallStereographic,ProjectionName.WorldWinkelTripel,ProjectionName.WorldFullerDymaxionMap))@OptIn(ExperimentalMaterial3Api::class)@Composableprivate fun ProjectionByTypeDropdownMenu(onProjectionSelected: (ProjectionName) -> Unit) {// Expanded boolean for the projection type drop-down menu.var isProjectionTypeExpanded by remember { mutableStateOf(false) }// Expanded boolean for the project name drop-down menu.var isProjectionNameExpanded by remember { mutableStateOf(false) }// The index of the current selection in project types menu.var selectedProjectionTypeIndex by remember { mutableIntStateOf(0) }// The index of the current selection in the project name menu.var selectedProjectionNameIndex by remember { mutableIntStateOf(0) }ExposedDropdownMenuBox(modifier = Modifier.fillMaxWidth(),expanded = isProjectionTypeExpanded,onExpandedChange = { isProjectionTypeExpanded = !isProjectionTypeExpanded }) {TextField(label = { Text("Select a projection type") },value = projectionTypes[selectedProjectionTypeIndex].first,onValueChange = {},readOnly = true,trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionTypeExpanded) },modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable, true))ExposedDropdownMenu(expanded = isProjectionTypeExpanded,onDismissRequest = { isProjectionTypeExpanded = false }) {projectionTypes.forEachIndexed { index, selectedOption ->DropdownMenuItem(text = { Text(text = selectedOption.first) },onClick = {selectedProjectionTypeIndex = indexisProjectionTypeExpanded = falseselectedProjectionNameIndex = 0onProjectionSelected(selectedOption.second[0])})}}}ExposedDropdownMenuBox(expanded = isProjectionNameExpanded,onExpandedChange = { isProjectionNameExpanded = !isProjectionNameExpanded }) {TextField(textStyle = TextStyle(fontSize = TextUnit(14f, TextUnitType.Sp)),value = projectionTypes[selectedProjectionTypeIndex].second[selectedProjectionNameIndex].label,label = {Text(text = "Select spacial reference using projection type",fontSize = TextUnit(10f, TextUnitType.Sp))},onValueChange = {},readOnly = true,trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionNameExpanded) },modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable, true))ExposedDropdownMenu(expanded = isProjectionNameExpanded,onDismissRequest = { isProjectionNameExpanded = false }) {projectionTypes[selectedProjectionTypeIndex].second.forEachIndexed { index, selectedOption ->DropdownMenuItem(text = { Text(text = selectedOption.label) },onClick = {selectedProjectionNameIndex = indexisProjectionNameExpanded = falseonProjectionSelected(selectedOption)})}}}}private suspend fun changeSpatialReference(selectedOption: ProjectionName) {val newSpatialReference = SpatialReference(selectedOption.wkid)// If user clicked on the same spatial reference then, don't create a new map.if (newSpatialReference == map.spatialReference) return// Remove the feature layer on the old map so the layer is free to be used on the new map.map.operationalLayers.remove(featureLayer)map = ArcGISMap(newSpatialReference).apply {backgroundColor = Color.white}map.load().onFailure { error -> logError(error) }}private var bufferPointGraphic = Graphic(geometry = null,symbol = SimpleMarkerSymbol(style = SimpleMarkerSymbolStyle.Circle,color = Color.red,size = 5f).apply {outline = SimpleLineSymbol(style = SimpleLineSymbolStyle.Dot,color = Color.white,width = 0.5f,markerStyle = SimpleLineSymbolMarkerStyle.None)})var bufferGraphic = Graphic(geometry = null, symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.fromRgba(r = 150, g = 130, b = 220, a = 216),outline = SimpleLineSymbol(style = SimpleLineSymbolStyle.Dash,color = Color.fromRgba(r = 255, g = 255, b = 255, a = 255),width = 0.5f,markerStyle = SimpleLineSymbolMarkerStyle.None)))private val bufferGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(bufferPointGraphic, bufferGraphic)))private fun createBuffer(singleTapConfirmedEvent: SingleTapConfirmedEvent) {val mapPoint = singleTapConfirmedEvent.mapPoint?: return logError(Exception("Tap event has no map point."))val buffer = GeometryEngine.bufferGeodeticOrNull(geometry = mapPoint,distance = 1000.0,distanceUnit = LinearUnit.kilometers,maxDeviation = Double.NaN,curveType = GeodeticCurveType.Geodesic) ?: return logError(Exception("Failed to create a buffer from the map point"))bufferGraphic.geometry = bufferbufferPointGraphic.geometry = mapPointcalloutPoint = mapPoint7 collapsed lines}@Composablefun MainScreen() {}
Connect your functionality to the map view
You will now connect your functionality to the MapView composable. Pass additional parameters to the MapView so it can display UI for your top-level functions and properties.
-
In the
MainScreencomposable, create a local variable namedcoroutineScope.In MainScreen() composable327 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.PaddingValuesimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.ExposedDropdownMenuBoximport androidx.compose.material3.ExposedDropdownMenuDefaultsimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.MenuAnchorTypeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TextFieldimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.text.TextStyleimport androidx.compose.ui.unit.TextUnitimport androidx.compose.ui.unit.TextUnitTypeimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.data.ServiceFeatureTableimport com.arcgismaps.geometry.Envelopeimport com.arcgismaps.geometry.GeodeticCurveTypeimport com.arcgismaps.geometry.GeometryEngineimport com.arcgismaps.geometry.LinearUnitimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.layers.FeatureLayerimport com.arcgismaps.mapping.symbology.SimpleFillSymbolimport com.arcgismaps.mapping.symbology.SimpleFillSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaultsimport com.arcgismaps.toolkit.geoviewcompose.MapViewScopeimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundprivate fun logError(error: Throwable) {Log.e("MainScreen.kt", error.message.toString(), error.cause)}private fun Double.roundToFiveDecimals(): Double {return round(this * 100000.0) / 100000.0}private val worldBoundaryGraphic = Graphic(symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.transparent,outline = SimpleLineSymbol().apply {color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)width = 0.5fstyle = SimpleLineSymbolStyle.Dash}),// The envelope covers the world extent of the map.geometry = Envelope(xMin = -180.0,yMin = -90.0,xMax = 180.0,yMax = 90.0,spatialReference = SpatialReference.wgs84()))private val boundaryGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(worldBoundaryGraphic)))// Keep track of the state of the callout point.private var calloutPoint: Point by mutableStateOf(Point(x = -10.0, y = 30.0, SpatialReference.wgs84()))// Show a callout using the callout point for location and text.@Composableprivate fun CalloutContainer(mapViewScope: MapViewScope) {mapViewScope.Callout(location = calloutPoint,// Optional parameters to customize the callout appearance.shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))) {Column {Text(text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",style = MaterialTheme.typography.labelSmall)}}}private var map by mutableStateOf(ArcGISMap(SpatialReference.wgs84()).apply {backgroundColor = Color.white})private val serviceFeatureTable = ServiceFeatureTable(uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0")private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {map.operationalLayers.add(featureLayer)featureLayer.load().onFailure { error ->logError(error)}// Project the callout point so it has the right spatial reference when used by the callout.calloutPoint = GeometryEngine.projectOrNull(geometry = calloutPoint,spatialReference = newSpatialReference) ?: return logError(Exception("Callout point's projection is null."))}// List of various projection names along with their respective label and wkid.// The name of a projection is the spatial reference it projects to.private enum class ProjectionName(val label: String, val wkid: Int) {WGS84(label = "WGS84 (GCS) -> pseudo Plate Carrée (Cylindrical)", wkid = 4326),WorldCassini(label = "World Cassini (Cylindrical)", wkid = 54028),WorldEquidistant(label = "World Equidistant conic (Conic)", wkid = 54027),WorldStereographic(label = "World Stereographic (Azimuthal)", wkid = 54026),WorldEckertVI(label = "World Eckert VI (Pseudocylindrical)", wkid = 54010),WorldSinusoidal(label = "World Sinusoidal (Pseudocylindrical)", wkid = 54008),NorthPoleGnomonic(label = "North Pole Gnomonic (Azimuthal", wkid = 102034),WebMercator(label = "Web Mercator Auxiliary Sphere (Cylindrical)", wkid = 3857),WorldGallStereographic(label = "World Gall Stereographic (Cylindrical)", wkid = 54016),WorldWinkelTripel(label = "World Winkel Tripel (Pseudoazimuthal)", wkid = 54042),WorldFullerDymaxionMap(label = "World Fuller / Dymaxion map (Polyhedral)", wkid = 54050)}// List of various projection types long with their respective list of spatial references.private val projectionTypes = listOf("Equidistant (maintain length)" to listOf(ProjectionName.WGS84,ProjectionName.WorldCassini,ProjectionName.WorldEquidistant),"Conformal (maintain angles)" to listOf(ProjectionName.WorldStereographic),"Equal-area (maintain area)" to listOf(ProjectionName.WorldEckertVI,ProjectionName.WorldSinusoidal),"Gnomonic (distances)" to listOf(ProjectionName.NorthPoleGnomonic),"Compromise (distort all)" to listOf(ProjectionName.WebMercator,ProjectionName.WorldGallStereographic,ProjectionName.WorldWinkelTripel,ProjectionName.WorldFullerDymaxionMap))@OptIn(ExperimentalMaterial3Api::class)@Composableprivate fun ProjectionByTypeDropdownMenu(onProjectionSelected: (ProjectionName) -> Unit) {// Expanded boolean for the projection type drop-down menu.var isProjectionTypeExpanded by remember { mutableStateOf(false) }// Expanded boolean for the project name drop-down menu.var isProjectionNameExpanded by remember { mutableStateOf(false) }// The index of the current selection in project types menu.var selectedProjectionTypeIndex by remember { mutableIntStateOf(0) }// The index of the current selection in the project name menu.var selectedProjectionNameIndex by remember { mutableIntStateOf(0) }ExposedDropdownMenuBox(modifier = Modifier.fillMaxWidth(),expanded = isProjectionTypeExpanded,onExpandedChange = { isProjectionTypeExpanded = !isProjectionTypeExpanded }) {TextField(label = { Text("Select a projection type") },value = projectionTypes[selectedProjectionTypeIndex].first,onValueChange = {},readOnly = true,trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionTypeExpanded) },modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable, true))ExposedDropdownMenu(expanded = isProjectionTypeExpanded,onDismissRequest = { isProjectionTypeExpanded = false }) {projectionTypes.forEachIndexed { index, selectedOption ->DropdownMenuItem(text = { Text(text = selectedOption.first) },onClick = {selectedProjectionTypeIndex = indexisProjectionTypeExpanded = falseselectedProjectionNameIndex = 0onProjectionSelected(selectedOption.second[0])})}}}ExposedDropdownMenuBox(expanded = isProjectionNameExpanded,onExpandedChange = { isProjectionNameExpanded = !isProjectionNameExpanded }) {TextField(textStyle = TextStyle(fontSize = TextUnit(14f, TextUnitType.Sp)),value = projectionTypes[selectedProjectionTypeIndex].second[selectedProjectionNameIndex].label,label = {Text(text = "Select spacial reference using projection type",fontSize = TextUnit(10f, TextUnitType.Sp))},onValueChange = {},readOnly = true,trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionNameExpanded) },modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable, true))ExposedDropdownMenu(expanded = isProjectionNameExpanded,onDismissRequest = { isProjectionNameExpanded = false }) {projectionTypes[selectedProjectionTypeIndex].second.forEachIndexed { index, selectedOption ->DropdownMenuItem(text = { Text(text = selectedOption.label) },onClick = {selectedProjectionNameIndex = indexisProjectionNameExpanded = falseonProjectionSelected(selectedOption)})}}}}private suspend fun changeSpatialReference(selectedOption: ProjectionName) {val newSpatialReference = SpatialReference(selectedOption.wkid)// If user clicked on the same spatial reference then, don't create a new map.if (newSpatialReference == map.spatialReference) return// Remove the feature layer on the old map so the layer is free to be used on the new map.map.operationalLayers.remove(featureLayer)map = ArcGISMap(newSpatialReference).apply {backgroundColor = Color.white}map.load().onFailure { error -> logError(error) }}private var bufferPointGraphic = Graphic(geometry = null,symbol = SimpleMarkerSymbol(style = SimpleMarkerSymbolStyle.Circle,color = Color.red,size = 5f).apply {outline = SimpleLineSymbol(style = SimpleLineSymbolStyle.Dot,color = Color.white,width = 0.5f,markerStyle = SimpleLineSymbolMarkerStyle.None)})var bufferGraphic = Graphic(geometry = null, symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.fromRgba(r = 150, g = 130, b = 220, a = 216),outline = SimpleLineSymbol(style = SimpleLineSymbolStyle.Dash,color = Color.fromRgba(r = 255, g = 255, b = 255, a = 255),width = 0.5f,markerStyle = SimpleLineSymbolMarkerStyle.None)))private val bufferGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(bufferPointGraphic, bufferGraphic)))private fun createBuffer(singleTapConfirmedEvent: SingleTapConfirmedEvent) {val mapPoint = singleTapConfirmedEvent.mapPoint?: return logError(Exception("Tap event has no map point."))val buffer = GeometryEngine.bufferGeodeticOrNull(geometry = mapPoint,distance = 1000.0,distanceUnit = LinearUnit.kilometers,maxDeviation = Double.NaN,curveType = GeodeticCurveType.Geodesic) ?: return logError(Exception("Failed to create a buffer from the map point"))bufferGraphic.geometry = bufferbufferPointGraphic.geometry = mapPointcalloutPoint = mapPoint}@Composablefun MainScreen() {val coroutineScope = rememberCoroutineScope()} -
Add a
Scaffoldwith the following code, which adds aColumnand theMapView.Note that in the
Column’s content, you call theProjectionByTypeDropdownMenu()composable, passing in a lambda. The lambda takes adropdownSelection: ProjectionNameparameter and callschangeSpatialReference()with it.In MainScreen() composable@Composablefun MainScreen() {val coroutineScope = rememberCoroutineScope()Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) { innerPadding ->Column(modifier = Modifier.padding(innerPadding),verticalArrangement = Arrangement.spacedBy(12.dp)) {ProjectionByTypeDropdownMenu(onProjectionSelected = { dropdownSelection ->coroutineScope.launch { changeSpatialReference(dropdownSelection) }})MapView(modifier = Modifier.fillMaxSize().weight(1f),arcGISMap = map,) {}}}} -
In the
MapViewcall, pass the following additional parameters:- A list of the two graphics overlays
A graphics overlay is a client-side, temporary container of graphics to display on a map view or scene view. . - A lambda that invokes your
onSpatialReferenceChanged()callback, if the new spatial referenceA spatial reference is a set of parameters, typically defined by a WKID, that define the coordinate system and spatial properties for geographic data. Applications use a spatial reference to correctly display the position of geographic data in a map or scene. is not null. - A function reference to your
createBuffer()callback that handles the single tap confirmed event.
In MainScreen() composable344 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.PaddingValuesimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.ExposedDropdownMenuBoximport androidx.compose.material3.ExposedDropdownMenuDefaultsimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.MenuAnchorTypeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TextFieldimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.text.TextStyleimport androidx.compose.ui.unit.TextUnitimport androidx.compose.ui.unit.TextUnitTypeimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.data.ServiceFeatureTableimport com.arcgismaps.geometry.Envelopeimport com.arcgismaps.geometry.GeodeticCurveTypeimport com.arcgismaps.geometry.GeometryEngineimport com.arcgismaps.geometry.LinearUnitimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.layers.FeatureLayerimport com.arcgismaps.mapping.symbology.SimpleFillSymbolimport com.arcgismaps.mapping.symbology.SimpleFillSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaultsimport com.arcgismaps.toolkit.geoviewcompose.MapViewScopeimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundprivate fun logError(error: Throwable) {Log.e("MainScreen.kt", error.message.toString(), error.cause)}private fun Double.roundToFiveDecimals(): Double {return round(this * 100000.0) / 100000.0}private val worldBoundaryGraphic = Graphic(symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.transparent,outline = SimpleLineSymbol().apply {color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)width = 0.5fstyle = SimpleLineSymbolStyle.Dash}),// The envelope covers the world extent of the map.geometry = Envelope(xMin = -180.0,yMin = -90.0,xMax = 180.0,yMax = 90.0,spatialReference = SpatialReference.wgs84()))private val boundaryGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(worldBoundaryGraphic)))// Keep track of the state of the callout point.private var calloutPoint: Point by mutableStateOf(Point(x = -10.0, y = 30.0, SpatialReference.wgs84()))// Show a callout using the callout point for location and text.@Composableprivate fun CalloutContainer(mapViewScope: MapViewScope) {mapViewScope.Callout(location = calloutPoint,// Optional parameters to customize the callout appearance.shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))) {Column {Text(text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",style = MaterialTheme.typography.labelSmall)}}}private var map by mutableStateOf(ArcGISMap(SpatialReference.wgs84()).apply {backgroundColor = Color.white})private val serviceFeatureTable = ServiceFeatureTable(uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0")private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {map.operationalLayers.add(featureLayer)featureLayer.load().onFailure { error ->logError(error)}// Project the callout point so it has the right spatial reference when used by the callout.calloutPoint = GeometryEngine.projectOrNull(geometry = calloutPoint,spatialReference = newSpatialReference) ?: return logError(Exception("Callout point's projection is null."))}// List of various projection names along with their respective label and wkid.// The name of a projection is the spatial reference it projects to.private enum class ProjectionName(val label: String, val wkid: Int) {WGS84(label = "WGS84 (GCS) -> pseudo Plate Carrée (Cylindrical)", wkid = 4326),WorldCassini(label = "World Cassini (Cylindrical)", wkid = 54028),WorldEquidistant(label = "World Equidistant conic (Conic)", wkid = 54027),WorldStereographic(label = "World Stereographic (Azimuthal)", wkid = 54026),WorldEckertVI(label = "World Eckert VI (Pseudocylindrical)", wkid = 54010),WorldSinusoidal(label = "World Sinusoidal (Pseudocylindrical)", wkid = 54008),NorthPoleGnomonic(label = "North Pole Gnomonic (Azimuthal", wkid = 102034),WebMercator(label = "Web Mercator Auxiliary Sphere (Cylindrical)", wkid = 3857),WorldGallStereographic(label = "World Gall Stereographic (Cylindrical)", wkid = 54016),WorldWinkelTripel(label = "World Winkel Tripel (Pseudoazimuthal)", wkid = 54042),WorldFullerDymaxionMap(label = "World Fuller / Dymaxion map (Polyhedral)", wkid = 54050)}// List of various projection types long with their respective list of spatial references.private val projectionTypes = listOf("Equidistant (maintain length)" to listOf(ProjectionName.WGS84,ProjectionName.WorldCassini,ProjectionName.WorldEquidistant),"Conformal (maintain angles)" to listOf(ProjectionName.WorldStereographic),"Equal-area (maintain area)" to listOf(ProjectionName.WorldEckertVI,ProjectionName.WorldSinusoidal),"Gnomonic (distances)" to listOf(ProjectionName.NorthPoleGnomonic),"Compromise (distort all)" to listOf(ProjectionName.WebMercator,ProjectionName.WorldGallStereographic,ProjectionName.WorldWinkelTripel,ProjectionName.WorldFullerDymaxionMap))@OptIn(ExperimentalMaterial3Api::class)@Composableprivate fun ProjectionByTypeDropdownMenu(onProjectionSelected: (ProjectionName) -> Unit) {// Expanded boolean for the projection type drop-down menu.var isProjectionTypeExpanded by remember { mutableStateOf(false) }// Expanded boolean for the project name drop-down menu.var isProjectionNameExpanded by remember { mutableStateOf(false) }// The index of the current selection in project types menu.var selectedProjectionTypeIndex by remember { mutableIntStateOf(0) }// The index of the current selection in the project name menu.var selectedProjectionNameIndex by remember { mutableIntStateOf(0) }ExposedDropdownMenuBox(modifier = Modifier.fillMaxWidth(),expanded = isProjectionTypeExpanded,onExpandedChange = { isProjectionTypeExpanded = !isProjectionTypeExpanded }) {TextField(label = { Text("Select a projection type") },value = projectionTypes[selectedProjectionTypeIndex].first,onValueChange = {},readOnly = true,trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionTypeExpanded) },modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable, true))ExposedDropdownMenu(expanded = isProjectionTypeExpanded,onDismissRequest = { isProjectionTypeExpanded = false }) {projectionTypes.forEachIndexed { index, selectedOption ->DropdownMenuItem(text = { Text(text = selectedOption.first) },onClick = {selectedProjectionTypeIndex = indexisProjectionTypeExpanded = falseselectedProjectionNameIndex = 0onProjectionSelected(selectedOption.second[0])})}}}ExposedDropdownMenuBox(expanded = isProjectionNameExpanded,onExpandedChange = { isProjectionNameExpanded = !isProjectionNameExpanded }) {TextField(textStyle = TextStyle(fontSize = TextUnit(14f, TextUnitType.Sp)),value = projectionTypes[selectedProjectionTypeIndex].second[selectedProjectionNameIndex].label,label = {Text(text = "Select spacial reference using projection type",fontSize = TextUnit(10f, TextUnitType.Sp))},onValueChange = {},readOnly = true,trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionNameExpanded) },modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable, true))ExposedDropdownMenu(expanded = isProjectionNameExpanded,onDismissRequest = { isProjectionNameExpanded = false }) {projectionTypes[selectedProjectionTypeIndex].second.forEachIndexed { index, selectedOption ->DropdownMenuItem(text = { Text(text = selectedOption.label) },onClick = {selectedProjectionNameIndex = indexisProjectionNameExpanded = falseonProjectionSelected(selectedOption)})}}}}private suspend fun changeSpatialReference(selectedOption: ProjectionName) {val newSpatialReference = SpatialReference(selectedOption.wkid)// If user clicked on the same spatial reference then, don't create a new map.if (newSpatialReference == map.spatialReference) return// Remove the feature layer on the old map so the layer is free to be used on the new map.map.operationalLayers.remove(featureLayer)map = ArcGISMap(newSpatialReference).apply {backgroundColor = Color.white}map.load().onFailure { error -> logError(error) }}private var bufferPointGraphic = Graphic(geometry = null,symbol = SimpleMarkerSymbol(style = SimpleMarkerSymbolStyle.Circle,color = Color.red,size = 5f).apply {outline = SimpleLineSymbol(style = SimpleLineSymbolStyle.Dot,color = Color.white,width = 0.5f,markerStyle = SimpleLineSymbolMarkerStyle.None)})var bufferGraphic = Graphic(geometry = null, symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.fromRgba(r = 150, g = 130, b = 220, a = 216),outline = SimpleLineSymbol(style = SimpleLineSymbolStyle.Dash,color = Color.fromRgba(r = 255, g = 255, b = 255, a = 255),width = 0.5f,markerStyle = SimpleLineSymbolMarkerStyle.None)))private val bufferGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(bufferPointGraphic, bufferGraphic)))private fun createBuffer(singleTapConfirmedEvent: SingleTapConfirmedEvent) {val mapPoint = singleTapConfirmedEvent.mapPoint?: return logError(Exception("Tap event has no map point."))val buffer = GeometryEngine.bufferGeodeticOrNull(geometry = mapPoint,distance = 1000.0,distanceUnit = LinearUnit.kilometers,maxDeviation = Double.NaN,curveType = GeodeticCurveType.Geodesic) ?: return logError(Exception("Failed to create a buffer from the map point"))bufferGraphic.geometry = bufferbufferPointGraphic.geometry = mapPointcalloutPoint = mapPoint}@Composablefun MainScreen() {val coroutineScope = rememberCoroutineScope()Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) { innerPadding ->Column(modifier = Modifier.padding(innerPadding),verticalArrangement = Arrangement.spacedBy(12.dp)) {ProjectionByTypeDropdownMenu(onProjectionSelected = { dropdownSelection ->coroutineScope.launch { changeSpatialReference(dropdownSelection) }})MapView(modifier = Modifier.fillMaxSize().weight(1f),arcGISMap = map,graphicsOverlays = listOf(boundaryGraphicsOverlay, bufferGraphicsOverlay),onSpatialReferenceChanged = { newSpatialReference ->newSpatialReference?.let {coroutineScope.launch {onSpatialReferenceChanged(newSpatialReference)}}},onSingleTapConfirmed = ::createBuffer) {}6 collapsed lines}}} - A list of the two graphics overlays
-
In
MapView‘s content lambda, callCalloutContainer, passing inthis, which is a reference to theMapViewScope.In MainScreen() composable359 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.PaddingValuesimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.ExposedDropdownMenuBoximport androidx.compose.material3.ExposedDropdownMenuDefaultsimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.MenuAnchorTypeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TextFieldimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.text.TextStyleimport androidx.compose.ui.unit.TextUnitimport androidx.compose.ui.unit.TextUnitTypeimport androidx.compose.ui.unit.dpimport com.arcgismaps.Colorimport com.arcgismaps.data.ServiceFeatureTableimport com.arcgismaps.geometry.Envelopeimport com.arcgismaps.geometry.GeodeticCurveTypeimport com.arcgismaps.geometry.GeometryEngineimport com.arcgismaps.geometry.LinearUnitimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.layers.FeatureLayerimport com.arcgismaps.mapping.symbology.SimpleFillSymbolimport com.arcgismaps.mapping.symbology.SimpleFillSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolimport com.arcgismaps.mapping.symbology.SimpleLineSymbolMarkerStyleimport com.arcgismaps.mapping.symbology.SimpleLineSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.theme.CalloutDefaultsimport com.arcgismaps.toolkit.geoviewcompose.MapViewScopeimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundprivate fun logError(error: Throwable) {Log.e("MainScreen.kt", error.message.toString(), error.cause)}private fun Double.roundToFiveDecimals(): Double {return round(this * 100000.0) / 100000.0}private val worldBoundaryGraphic = Graphic(symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.transparent,outline = SimpleLineSymbol().apply {color = Color.fromRgba(r = 50, g = 50, b = 50, a = 192)width = 0.5fstyle = SimpleLineSymbolStyle.Dash}),// The envelope covers the world extent of the map.geometry = Envelope(xMin = -180.0,yMin = -90.0,xMax = 180.0,yMax = 90.0,spatialReference = SpatialReference.wgs84()))private val boundaryGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(worldBoundaryGraphic)))// Keep track of the state of the callout point.private var calloutPoint: Point by mutableStateOf(Point(x = -10.0, y = 30.0, SpatialReference.wgs84()))// Show a callout using the callout point for location and text.@Composableprivate fun CalloutContainer(mapViewScope: MapViewScope) {mapViewScope.Callout(location = calloutPoint,// Optional parameters to customize the callout appearance.shapes = CalloutDefaults.shapes(calloutContentPadding = PaddingValues(8.dp))) {Column {Text(text = "x = ${calloutPoint.x.roundToFiveDecimals()}\ny = ${calloutPoint.y.roundToFiveDecimals()}",style = MaterialTheme.typography.labelSmall)}}}private var map by mutableStateOf(ArcGISMap(SpatialReference.wgs84()).apply {backgroundColor = Color.white})private val serviceFeatureTable = ServiceFeatureTable(uri = "https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/World_Countries_(Generalized)/FeatureServer/0")private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable = serviceFeatureTable)private suspend fun onSpatialReferenceChanged(newSpatialReference: SpatialReference) {map.operationalLayers.add(featureLayer)featureLayer.load().onFailure { error ->logError(error)}// Project the callout point so it has the right spatial reference when used by the callout.calloutPoint = GeometryEngine.projectOrNull(geometry = calloutPoint,spatialReference = newSpatialReference) ?: return logError(Exception("Callout point's projection is null."))}// List of various projection names along with their respective label and wkid.// The name of a projection is the spatial reference it projects to.private enum class ProjectionName(val label: String, val wkid: Int) {WGS84(label = "WGS84 (GCS) -> pseudo Plate Carrée (Cylindrical)", wkid = 4326),WorldCassini(label = "World Cassini (Cylindrical)", wkid = 54028),WorldEquidistant(label = "World Equidistant conic (Conic)", wkid = 54027),WorldStereographic(label = "World Stereographic (Azimuthal)", wkid = 54026),WorldEckertVI(label = "World Eckert VI (Pseudocylindrical)", wkid = 54010),WorldSinusoidal(label = "World Sinusoidal (Pseudocylindrical)", wkid = 54008),NorthPoleGnomonic(label = "North Pole Gnomonic (Azimuthal", wkid = 102034),WebMercator(label = "Web Mercator Auxiliary Sphere (Cylindrical)", wkid = 3857),WorldGallStereographic(label = "World Gall Stereographic (Cylindrical)", wkid = 54016),WorldWinkelTripel(label = "World Winkel Tripel (Pseudoazimuthal)", wkid = 54042),WorldFullerDymaxionMap(label = "World Fuller / Dymaxion map (Polyhedral)", wkid = 54050)}// List of various projection types long with their respective list of spatial references.private val projectionTypes = listOf("Equidistant (maintain length)" to listOf(ProjectionName.WGS84,ProjectionName.WorldCassini,ProjectionName.WorldEquidistant),"Conformal (maintain angles)" to listOf(ProjectionName.WorldStereographic),"Equal-area (maintain area)" to listOf(ProjectionName.WorldEckertVI,ProjectionName.WorldSinusoidal),"Gnomonic (distances)" to listOf(ProjectionName.NorthPoleGnomonic),"Compromise (distort all)" to listOf(ProjectionName.WebMercator,ProjectionName.WorldGallStereographic,ProjectionName.WorldWinkelTripel,ProjectionName.WorldFullerDymaxionMap))@OptIn(ExperimentalMaterial3Api::class)@Composableprivate fun ProjectionByTypeDropdownMenu(onProjectionSelected: (ProjectionName) -> Unit) {// Expanded boolean for the projection type drop-down menu.var isProjectionTypeExpanded by remember { mutableStateOf(false) }// Expanded boolean for the project name drop-down menu.var isProjectionNameExpanded by remember { mutableStateOf(false) }// The index of the current selection in project types menu.var selectedProjectionTypeIndex by remember { mutableIntStateOf(0) }// The index of the current selection in the project name menu.var selectedProjectionNameIndex by remember { mutableIntStateOf(0) }ExposedDropdownMenuBox(modifier = Modifier.fillMaxWidth(),expanded = isProjectionTypeExpanded,onExpandedChange = { isProjectionTypeExpanded = !isProjectionTypeExpanded }) {TextField(label = { Text("Select a projection type") },value = projectionTypes[selectedProjectionTypeIndex].first,onValueChange = {},readOnly = true,trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionTypeExpanded) },modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable, true))ExposedDropdownMenu(expanded = isProjectionTypeExpanded,onDismissRequest = { isProjectionTypeExpanded = false }) {projectionTypes.forEachIndexed { index, selectedOption ->DropdownMenuItem(text = { Text(text = selectedOption.first) },onClick = {selectedProjectionTypeIndex = indexisProjectionTypeExpanded = falseselectedProjectionNameIndex = 0onProjectionSelected(selectedOption.second[0])})}}}ExposedDropdownMenuBox(expanded = isProjectionNameExpanded,onExpandedChange = { isProjectionNameExpanded = !isProjectionNameExpanded }) {TextField(textStyle = TextStyle(fontSize = TextUnit(14f, TextUnitType.Sp)),value = projectionTypes[selectedProjectionTypeIndex].second[selectedProjectionNameIndex].label,label = {Text(text = "Select spacial reference using projection type",fontSize = TextUnit(10f, TextUnitType.Sp))},onValueChange = {},readOnly = true,trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isProjectionNameExpanded) },modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable, true))ExposedDropdownMenu(expanded = isProjectionNameExpanded,onDismissRequest = { isProjectionNameExpanded = false }) {projectionTypes[selectedProjectionTypeIndex].second.forEachIndexed { index, selectedOption ->DropdownMenuItem(text = { Text(text = selectedOption.label) },onClick = {selectedProjectionNameIndex = indexisProjectionNameExpanded = falseonProjectionSelected(selectedOption)})}}}}private suspend fun changeSpatialReference(selectedOption: ProjectionName) {val newSpatialReference = SpatialReference(selectedOption.wkid)// If user clicked on the same spatial reference then, don't create a new map.if (newSpatialReference == map.spatialReference) return// Remove the feature layer on the old map so the layer is free to be used on the new map.map.operationalLayers.remove(featureLayer)map = ArcGISMap(newSpatialReference).apply {backgroundColor = Color.white}map.load().onFailure { error -> logError(error) }}private var bufferPointGraphic = Graphic(geometry = null,symbol = SimpleMarkerSymbol(style = SimpleMarkerSymbolStyle.Circle,color = Color.red,size = 5f).apply {outline = SimpleLineSymbol(style = SimpleLineSymbolStyle.Dot,color = Color.white,width = 0.5f,markerStyle = SimpleLineSymbolMarkerStyle.None)})var bufferGraphic = Graphic(geometry = null, symbol = SimpleFillSymbol(style = SimpleFillSymbolStyle.Solid,color = Color.fromRgba(r = 150, g = 130, b = 220, a = 216),outline = SimpleLineSymbol(style = SimpleLineSymbolStyle.Dash,color = Color.fromRgba(r = 255, g = 255, b = 255, a = 255),width = 0.5f,markerStyle = SimpleLineSymbolMarkerStyle.None)))private val bufferGraphicsOverlay by mutableStateOf(GraphicsOverlay(graphics = listOf(bufferPointGraphic, bufferGraphic)))private fun createBuffer(singleTapConfirmedEvent: SingleTapConfirmedEvent) {val mapPoint = singleTapConfirmedEvent.mapPoint?: return logError(Exception("Tap event has no map point."))val buffer = GeometryEngine.bufferGeodeticOrNull(geometry = mapPoint,distance = 1000.0,distanceUnit = LinearUnit.kilometers,maxDeviation = Double.NaN,curveType = GeodeticCurveType.Geodesic) ?: return logError(Exception("Failed to create a buffer from the map point"))bufferGraphic.geometry = bufferbufferPointGraphic.geometry = mapPointcalloutPoint = mapPoint}@Composablefun MainScreen() {val coroutineScope = rememberCoroutineScope()Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) { innerPadding ->Column(modifier = Modifier.padding(innerPadding),verticalArrangement = Arrangement.spacedBy(12.dp)) {ProjectionByTypeDropdownMenu(onProjectionSelected = { dropdownSelection ->coroutineScope.launch { changeSpatialReference(dropdownSelection) }})MapView(modifier = Modifier.fillMaxSize().weight(1f),arcGISMap = map,graphicsOverlays = listOf(boundaryGraphicsOverlay, bufferGraphicsOverlay),onSpatialReferenceChanged = { newSpatialReference ->newSpatialReference?.let {coroutineScope.launch {onSpatialReferenceChanged(newSpatialReference)}}},onSingleTapConfirmed = ::createBuffer) {CalloutContainer(this)}6 collapsed lines}}}
Click Run > Run > app to run the app.
You should see an orange feature layer
You can interact with this app in two ways to learn about spatial references:
-
View coordinates of the same point in different projections: When the app launches, examine the callout and the point coordinates it displays. Then select other projections from the drop-down menus. You should see that same point with different coordinates for each spatial reference.
-
View spatial distortions of the buffer: In any spatial reference, tap on the map. The point buffer should display on the map, with the tapped point’s x/y coordinates showing in a callout. Tap around the map and watch how the buffer’s size and shape get distorted. Try other projections and observe how the distortions differ from one spatial reference to another.
Alternatively, you can download the tutorial solution, as follows.
Option 2: Download the solution
-
Click the Download solution link in the right-hand side of this page.
-
Unzip the file to a location on your machine.
-
Run Android Studio.
-
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
You can implement API key authentication or user authentication in this tutorial. Compare the differences below:
API key authentication
- Users are not required to sign in.
- Requires creating an API key credential
API key credentials are an item that contains the parameters used to create and manage long-lived access tokens for API key authentication. They are a type of developer credential. with the correct privileges. - API keys
An API key is a long-lived access token created using API key credentials. They are valid for up to one year and are typically embedded directly into client applications. are long-lived access tokens. - Service usage is billed to the API key owner/developer.
- Simplest authentication method to implement.
- Recommended approach for new ArcGIS developers.
Learn more in API key authentication.
User authentication
- Users are required to sign in with an ArcGIS account
An ArcGIS account is an identity with a user type and set of privileges that can access specific ArcGIS products, tools, APIs, services, and resources. The main account types that can be used for development are an ArcGIS Location Platform account, ArcGIS Online account, and ArcGIS Enterprise account. ArcGIS Location Platform and ArcGIS Online accounts are also associated with a subscription. . - User accounts must have privilege
Privileges are a set of permissions assigned to ArcGIS accounts, developer credentials, and applications that grant access to secure resources and functionality in ArcGIS. to access the ArcGIS servicesA service, also known as an ArcGIS service, is software that supports an ArcGIS REST API and provides geospatial functionality or data. A service can be hosted by Esri or in ArcGIS Enterprise. used in application. - Requires creating OAuth credentials
OAuth credentials are an item that contains parameters required to implement user authentication or app authentication, including a .client_id,client_secret, and redirect URIs. They are a type of developer credential. - Application uses a redirect URL and client ID.
- Service usage is billed to the organization of the user signed into the application.
Learn more in User authentication.
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
-
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. :- Privileges
- Location services > Basemaps
- Privileges
-
Copy and paste the API key access token into a safe location. It will be used in a later step.
Create new OAuth credentials to access the secure resources used in this tutorial.
-
Complete the Create OAuth credentials for user authentication tutorial to obtain a Client ID and Redirect URL.
A
Client IDuniquely identifies your app on the authenticating server. If the server cannot find an app with the provided Client ID, it will not proceed with authentication.The
Redirect URL(also referred to as a callback url) is used to identify a response from the authenticating server when the system returns control back to your app after an OAuth login. Since it does not necessarily represent a valid endpoint that a user could navigate to, the redirect URL can use a custom scheme, such asmy-app://auth. It is important to make sure the redirect URL used in your app’s code matches a redirect URL configured on the authenticating server. -
Copy and paste the Client ID and Redirect URL into a safe location. They will be used in a later step.
All users that access this application need account privileges
Set developer credentials in the solution
To allow your app users to access ArcGIS location services
-
In the Android view of Android Studio, open app > kotlin+java > com.example.app > MainActivity. Set the
AuthenticationModeto.API_KEY.MainActivity.kt14 collapsed linespackage com.example.appimport android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.arcgismaps.httpcore.authentication.OAuthUserConfigurationimport com.arcgismaps.toolkit.authentication.AuthenticatorStateimport com.arcgismaps.toolkit.authentication.DialogAuthenticatorimport com.example.app.screens.MainScreenimport com.example.app.ui.theme.TutorialThemeclass MainActivity : ComponentActivity() {private enum class AuthenticationMode { API_KEY, USER_AUTH }private val authenticationMode = AuthenticationMode.API_KEY42 collapsed linesprivate 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)}}}}} -
Set the
apiKeyproperty with your API key access token.MainActivity.kt22 collapsed linespackage com.example.appimport android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.arcgismaps.httpcore.authentication.OAuthUserConfigurationimport com.arcgismaps.toolkit.authentication.AuthenticatorStateimport com.arcgismaps.toolkit.authentication.DialogAuthenticatorimport com.example.app.screens.MainScreenimport com.example.app.ui.theme.TutorialThemeclass MainActivity : ComponentActivity() {private enum class AuthenticationMode { API_KEY, USER_AUTH }private val authenticationMode = AuthenticationMode.API_KEYprivate 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 linesAuthenticationMode.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.
-
In the Android view of Android Studio, open app > kotlin+java > com.example.app > MainActivity. Set the
AuthenticationModeto.USER_AUTH.MainActivity.ktclass MainActivity : ComponentActivity() {private enum class AuthenticationMode { API_KEY, USER_AUTH }private val authenticationMode = AuthenticationMode.USER_AUTH -
Set your
clientIDandredirectURLvalues. You must use the RedirectURL that you supplied for your app in theuser authenticationpart of the Set up authentication step.MainActivity.ktAuthenticationMode.USER_AUTH -> {authenticatorState.oAuthUserConfigurations = listOf(OAuthUserConfiguration(portalUrl = "https://www.arcgis.com",clientId = "YOUR_CLIENT_ID",redirectUrl = "YOUR_REDIRECT_URL")) -
Open app > manifests > AndroidManifest.xml.
-
Set the
android:schemeandandroid:hostusing the scheme and host from your RedirectURL.A redirectURL is composed of a scheme and a host component. The format for the redirect url is
scheme://host. For example, if the redirect url ismyscheme://myhostthen the scheme ismyschemeand the host ismyhost.AndroidManifest.xml41 collapsed lines<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"><uses-permission android:name="android.permission.INTERNET" /><applicationandroid:allowBackup="true"android:dataExtractionRules="@xml/data_extraction_rules"android:fullBackupContent="@xml/backup_rules"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.Tutorial"tools:targetApi="31"><activityandroid:name=".MainActivity"android:exported="true"android:label="@string/app_name"android:theme="@style/Theme.Tutorial"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><activityandroid:name="com.arcgismaps.toolkit.authentication.AuthenticationActivity"android:configChanges="keyboard|keyboardHidden|orientation|screenSize"android:exported="true"android:launchMode="singleTop" ><intent-filter><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><dataandroid:scheme="your_redirect_url_scheme"android:host="your_redirect_url_host" />6 collapsed lines</intent-filter></activity></application></manifest>
Best Practice: The OAuth credentials are 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 an orange feature layer
You can interact with this app in two ways to learn about spatial references:
-
View coordinates of the same point in different projections: When the app launches, examine the callout and the point coordinates it displays. Then select other projections from the drop-down menus. You should see that same point with different coordinates for each spatial reference.
-
View spatial distortions of the buffer: In any spatial reference, tap on the map. The point buffer should display on the map, with the tapped point’s x/y coordinates showing in a callout. Tap around the map and watch how the buffer’s size and shape get distorted. Try other projections and observe how the distortions differ from one spatial reference to another.
What’s next?
Learn how to use additional API features, ArcGIS location services, and ArcGIS tools in these tutorials: