Format coordinates in a variety of common notations.

Use case
The coordinate formatter can format a map location in WGS84 in a number of common coordinate notations. Parsing one of these formats to a location is also supported. Formats include decimal degrees; degrees, minutes, seconds; Universal Transverse Mercator (UTM), and United States National Grid (USNG).
How to use the sample
Tap on the map to see a marker with the tapped location’s coordinate formatted in 4 different ways. You can also put a coordinate string in any of these formats in the text field. Hit Enter and the coordinate string will be parsed to a map location which the marker will move to.
How it works
- Get or create a map
Pointwith a spatial reference. - Use one of the static “to” methods on
CoordinateFormattersuch asCoordinateFormatter.toLatitudeLongitudeOrNull(point = newLocation, format = LatitudeLongitudeFormat.DecimalDegrees, decimalPlaces = 4)to get the formatted string. - To go from a formatted string to a
Point, use one of the “from” static methods likeCoordinateFormatter.fromUtmOrNull(coordinates = coordinateNotation, utmConversionMode = UtmConversionMode.LatitudeBandIndicators, spatialReference = null).
Relevant API
- CoordinateFormatter
- CoordinateFormatter.LatitudeLongitudeFormat
- CoordinateFormatter.UtmConversionMode
Additional information
This sample uses the GeoView-Compose Toolkit module to be able to implement a composable MapView.
Tags
convert, coordinate, decimal degrees, degree minutes seconds, format, geoview-compose, latitude, longitude, toolkit, USNG, UTM
Sample Code
/* Copyright 2023 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.showcoordinatesinmultipleformats
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.runtime.Composableimport androidx.core.view.WindowCompatimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.showcoordinatesinmultipleformats.screens.MainScreen
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // authentication with an API key or named user is // required to access basemaps and other location services ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN) // remove focus from text fields when keyboard closes WindowCompat.setDecorFitsSystemWindows(window, false) // set compose content enableEdgeToEdge() setContent { SampleAppTheme { ShowCoordinatesInMultipleFormatsApp() } } }
@Composable private fun ShowCoordinatesInMultipleFormatsApp() { Surface(color = MaterialTheme.colorScheme.background) { MainScreen(sampleName = getString(R.string.show_coordinates_in_multiple_formats_app_name)) } }}/* Copyright 2023 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.showcoordinatesinmultipleformats.components
import android.app.Applicationimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.lifecycle.AndroidViewModelimport com.arcgismaps.geometry.CoordinateFormatterimport com.arcgismaps.geometry.LatitudeLongitudeFormatimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.geometry.UtmConversionModeimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
class MapViewModel(application: Application) : AndroidViewModel(application) {
var decimalDegrees by mutableStateOf("") private set var degreesMinutesSeconds by mutableStateOf("") private set
var utm by mutableStateOf("") private set
var usng by mutableStateOf("") private set
// create a ViewModel to handle dialog interactions val messageDialogVM: MessageDialogViewModel = MessageDialogViewModel()
// set up a graphic to indicate where the coordinates relate to, with an initial location val initialPoint = Point(0.0, 0.0, SpatialReference.wgs84())
/** * Uses CoordinateFormatter to update the UI with coordinate notation strings based on the * given [newLocation] point to convert to coordinate notations */ fun toCoordinateNotationFromPoint(newLocation: Point) { // use CoordinateFormatter to convert to Latitude Longitude, formatted as Decimal Degrees decimalDegrees = CoordinateFormatter.toLatitudeLongitudeOrNull( point = newLocation, format = LatitudeLongitudeFormat.DecimalDegrees, decimalPlaces = 4 ) ?: return messageDialogVM.showMessageDialog("Failed to convert from point DD coordinate")
// use CoordinateFormatter to convert to Latitude Longitude, formatted as Degrees, Minutes, Seconds degreesMinutesSeconds = CoordinateFormatter.toLatitudeLongitudeOrNull( point = newLocation, format = LatitudeLongitudeFormat.DegreesMinutesSeconds, decimalPlaces = 4 ) ?: return messageDialogVM.showMessageDialog("Failed to convert from point DMS coordinate")
// use CoordinateFormatter to convert to Universal Transverse Mercator, using latitudinal bands indicator utm = CoordinateFormatter.toUtmOrNull( point = newLocation, utmConversionMode = UtmConversionMode.LatitudeBandIndicators, addSpaces = true ) ?: return messageDialogVM.showMessageDialog("Failed to convert from point UTM coordinate")
// use CoordinateFormatter to convert to United States National Grid (USNG) usng = CoordinateFormatter.toUsngOrNull( point = newLocation, precision = 4, addSpaces = true, ) ?: return messageDialogVM.showMessageDialog("Failed to convert from point USNG coordinate") }
/** * Uses CoordinateFormatter to update the graphic in the map from the given [coordinateNotation] * string entered by the user. Also calls corresponding method to update all the remaining * [coordinateNotation] strings using the notation [notationType]. */ fun fromCoordinateNotationToPoint(notationType: NotationType, coordinateNotation: String) { // ignore empty input coordinate notation strings, do not update UI if (coordinateNotation.isEmpty()) return val convertedPoint: Point = when (notationType) { NotationType.DMS, NotationType.DD -> { // use CoordinateFormatter to parse Latitude Longitude - different numeric notations (Decimal Degrees; // Degrees, Minutes, Seconds; Degrees, Decimal Minutes) can all be passed to this same method CoordinateFormatter.fromLatitudeLongitudeOrNull( coordinates = coordinateNotation, spatialReference = null ) ?: return messageDialogVM.showMessageDialog("Failed to convert DMS/DD coordinate to point") }
NotationType.UTM -> { // use CoordinateFormatter to parse UTM coordinates CoordinateFormatter.fromUtmOrNull( coordinates = coordinateNotation, utmConversionMode = UtmConversionMode.LatitudeBandIndicators, spatialReference = null ) ?: return messageDialogVM.showMessageDialog("Failed to convert UTM coordinate to point") }
NotationType.USNG -> { // use CoordinateFormatter to parse US National Grid coordinates CoordinateFormatter.fromUsngOrNull( coordinates = coordinateNotation, spatialReference = null ) ?: return messageDialogVM.showMessageDialog("Failed to convert USNG coordinate to point") } }
// update the location shown in the map toCoordinateNotationFromPoint(convertedPoint) }
/** * Coordinate notations supported by this sample */ enum class NotationType { DMS, DD, UTM, USNG }
/** * Set's [decimalDegrees] entered in the text field to the [inputString] */ fun setDecimalDegreesCoordinate(inputString: String) { decimalDegrees = inputString }
/** * Set's [degreesMinutesSeconds] entered in the text field to the [inputString] */ fun degreesMinutesSecondsCoordinate(inputString: String) { degreesMinutesSeconds = inputString }
/** * Set's [utm] entered in the text field to the [inputString] */ fun setUTMCoordinate(inputString: String) { utm = inputString
}
/** * Set's [usng] entered in the text field to the [inputString] */ fun setUSNGDegreesCoordinate(inputString: String) { usng = inputString }}/* Copyright 2023 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.showcoordinatesinmultipleformats.screens
import android.content.res.Configurationimport android.view.KeyEventimport androidx.compose.foundation.layout.WindowInsetsimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.imeimport androidx.compose.foundation.text.KeyboardActionsimport androidx.compose.foundation.text.KeyboardOptionsimport androidx.compose.material3.OutlinedTextFieldimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.Stateimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberUpdatedStateimport androidx.compose.ui.Modifierimport androidx.compose.ui.focus.FocusRequesterimport androidx.compose.ui.focus.focusRequesterimport androidx.compose.ui.input.key.onKeyEventimport androidx.compose.ui.platform.LocalDensityimport androidx.compose.ui.platform.LocalFocusManagerimport androidx.compose.ui.text.input.ImeActionimport androidx.compose.ui.text.input.KeyboardTypeimport androidx.compose.ui.tooling.preview.Previewimport androidx.lifecycle.viewmodel.compose.viewModelimport com.esri.arcgismaps.sample.showcoordinatesinmultipleformats.components.MapViewModel
@Composablefun CoordinatesLayout(mapViewModel: MapViewModel) { CoordinateField( notationType = MapViewModel.NotationType.DD, text = mapViewModel.decimalDegrees, labelText = "Decimal Degrees", onTextChanged = mapViewModel::setDecimalDegreesCoordinate, onQuerySubmit = mapViewModel::fromCoordinateNotationToPoint, )
CoordinateField( notationType = MapViewModel.NotationType.DMS, text = mapViewModel.degreesMinutesSeconds, labelText = "Degrees, Minutes, Seconds", onTextChanged = mapViewModel::degreesMinutesSecondsCoordinate, onQuerySubmit = mapViewModel::fromCoordinateNotationToPoint, )
CoordinateField( notationType = MapViewModel.NotationType.UTM, text = mapViewModel.utm, labelText = "UTM", onTextChanged = mapViewModel::setUTMCoordinate, onQuerySubmit = mapViewModel::fromCoordinateNotationToPoint, )
CoordinateField( notationType = MapViewModel.NotationType.USNG, text = mapViewModel.usng, labelText = "USNG", onTextChanged = mapViewModel::setUSNGDegreesCoordinate, onQuerySubmit = mapViewModel::fromCoordinateNotationToPoint, )}
@Composablefun CoordinateField( modifier: Modifier = Modifier, notationType: MapViewModel.NotationType, text: String = "", onTextChanged: (String) -> Unit, onQuerySubmit: (MapViewModel.NotationType, String) -> Unit, labelText: String) { // remember the OutlinedTextField's focus requester to change focus on search val focusRequester = remember { FocusRequester() } // focus manager is used to clear focus from OutlinedTextField on search val focusManager = LocalFocusManager.current // set the keyboard options for all field to search val keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, imeAction = ImeAction.Search )
OutlinedTextField( modifier = modifier.fillMaxWidth().focusRequester(focusRequester).onKeyEvent { // submit query when enter is tapped if (it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) { onQuerySubmit(notationType, text) focusManager.clearFocus() } false }, value = text, maxLines = 1, singleLine = true, onValueChange = onTextChanged, label = { Text(labelText) }, keyboardOptions = keyboardOptions, keyboardActions = KeyboardActions( onSearch = { // submit query when search is tapped onQuerySubmit(notationType, text) focusManager.clearFocus() }, ), )
// if keyboard is closed, remove focus from text fields if (!keyboardAsState().value) { focusManager.clearFocus() }}
/** * Composable function that returns a false state when keyboard is closed. */@Composablefun keyboardAsState(): State<Boolean> { val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0 return rememberUpdatedState(isImeVisible)}
@Preview(showBackground = true)@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)@Composablefun PreviewCoordinatesLayout() { CoordinatesLayout(mapViewModel = viewModel())}/* Copyright 2023 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.showcoordinatesinmultipleformats.screens
import androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.EditLocationAltimport androidx.compose.material3.FloatingActionButtonimport androidx.compose.material3.Iconimport androidx.compose.material3.Scaffoldimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.unit.dpimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.Colorimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.Basemapimport com.arcgismaps.mapping.layers.ArcGISTiledLayerimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.esri.arcgismaps.sample.sampleslib.components.BottomSheetimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBarimport com.esri.arcgismaps.sample.showcoordinatesinmultipleformats.Rimport com.esri.arcgismaps.sample.showcoordinatesinmultipleformats.components.MapViewModel
/** * Main screen layout for the sample app */@Composablefun MainScreen(sampleName: String) { // create a ViewModel to handle MapView interactions val mapViewModel: MapViewModel = viewModel() // create a map that has the WGS 84 coordinate system and set this into the map val basemapLayer = ArcGISTiledLayer(LocalContext.current.applicationContext.getString(R.string.basemap_url)) val arcGISMap = ArcGISMap(Basemap(basemapLayer)) val coordinateLocationGraphic = Graphic( geometry = mapViewModel.initialPoint, symbol = SimpleMarkerSymbol( style = SimpleMarkerSymbolStyle.Cross, color = Color.fromRgba(255, 255, 0, 255), size = 20f ) ) // graphics overlay to display a graphics of the coordinate location val graphicsOverlay = GraphicsOverlay().apply { graphics.add(coordinateLocationGraphic) } // the collection of graphics overlays used by the MapView val graphicsOverlays = remember { listOf(graphicsOverlay) } // update the coordinate notations using the initial point LaunchedEffect(Unit) { mapViewModel.toCoordinateNotationFromPoint(mapViewModel.initialPoint) } // track the bottom sheet visibility var isBottomSheetVisible by remember { mutableStateOf(true) }
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, floatingActionButton = { if (!isBottomSheetVisible) { FloatingActionButton( modifier = Modifier.padding(bottom = 36.dp, end = 12.dp), onClick = { isBottomSheetVisible = true }) { Icon( imageVector = Icons.Filled.EditLocationAlt, contentDescription = "Show coordinate formats" ) } } }, content = { Column( modifier = Modifier .fillMaxSize() .padding(it) ) { MapView( modifier = Modifier.fillMaxSize(), arcGISMap = arcGISMap, graphicsOverlays = graphicsOverlays, onSingleTapConfirmed = { singleTapConfirmedEvent -> // retrieve the map point on MapView tapped val tappedPoint = singleTapConfirmedEvent.mapPoint if (tappedPoint != null) { // update the tapped location graphic coordinateLocationGraphic.geometry = tappedPoint // update the coordinate notations using the tapped point mapViewModel.toCoordinateNotationFromPoint(tappedPoint) } } )
// display a dialog if the sample encounters an error mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, onDismissRequest = ::dismissDialog ) } } } BottomSheet( sheetTitle = "Set Coordinates", isVisible = isBottomSheetVisible, onDismissRequest = { isBottomSheetVisible = false } ) { // layout to display the coordinate text fields. CoordinatesLayout(mapViewModel = mapViewModel) } } )}