Display and configure electronic navigational charts per ENC specification.

Use case
The S-52 standard defines how Electronic Navigational Chart (ENC) content should be displayed to ensure that data is presented consistently across every charting system. S-52 defines several display options, including variations on symbology to ensure that charts are readable both at night and in direct sunlight.
How to use the sample
When opened, the sample displays an electronic navigational chart. Tap on the map to select ENC features and view the feature’s acronyms and descriptions shown in a callout. Tap “Display Settings” and use the options to adjust some of the ENC mariner display settings, such as the colors and symbology.
How it works
- To display ENC content:
- On
EncEnvironmentSettings, setresourcePathto the local hydrography data directory andsencDataPathto a temporary directory. - Create an
EncExchangeSetusing URLs to the local ENC exchange set files and load it. - Make an
EncCellfor each of theEncExchangeSet.datasetsand then make anEncLayerfrom each cell. - Add the layers to the map using
ArcGISMap.operationalLayers.add(encLayer)to display the map.
- On
- To select ENC features:
- Use
onSingleTapConfirmedon the map view to get the screen point from the tapped location. - Create a
MapViewProxyand use it to identify nearby features to the tapped location withidentifyLayers. - From the resulting
IdentifyLayerResult, get theEncLayerfromlayerContentand theEncFeature(s) fromgeoElements. - Use
EncLayer.selectFeatureto select the ENC feature(s).
- Use
- To set ENC display settings:
- Get the
EncDisplaySettingsinstance fromEncEnvironmentSettings.displaySettings. - Use
marinerSettings,textGroupVisibilitySettings, andviewingGroupSettingsto access the settings instances and set their properties. - Reset the display settings using
resetToDefaults()on the settings instances.
- Get the
Relevant API
- EncCell
- EncDataset
- EncDisplaySettings
- EncEnvironmentSettings
- EncExchangeSet
- EncLayer
- EncMarinerSettings
- EncTextGroupVisibilitySettings
- IdentifyLayerResult
Offline data
This sample downloads the ENC Exchange Set without updates item from ArcGIS Online automatically.
The latest Hydrography Data can be downloaded from the Esri Developer downloads. The S57DataDictionary.xml file is contained there.
Additional information
Read more about displaying and deploying electronic navigational charts on Esri Developer.
Tags
ENC, hydrography, identify, IHO, layers, maritime, nautical chart, S-52, S-57, select, settings, symbology
Sample code
/* Copyright 2024 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.configureelectronicnavigationalcharts.components
import android.app.Applicationimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.compose.ui.unit.dpimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.hydrography.EncAreaSymbolizationTypeimport com.arcgismaps.hydrography.EncCellimport com.arcgismaps.hydrography.EncColorSchemeimport com.arcgismaps.hydrography.EncEnvironmentSettingsimport com.arcgismaps.hydrography.EncExchangeSetimport com.arcgismaps.hydrography.EncFeatureimport com.arcgismaps.hydrography.EncPointSymbolizationTypeimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.layers.EncLayerimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.esri.arcgismaps.sample.configureelectronicnavigationalcharts.Rimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport com.arcgismaps.toolkit.geoviewcompose.MapViewProxyimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.asStateFlowimport kotlinx.coroutines.launchimport java.io.File
class ConfigureElectronicNavigationalChartsScreenViewModel(application: Application) : AndroidViewModel(application) { private val provisionPath: String by lazy { application.getExternalFilesDir(null)?.path.toString() + File.separator + application.getString(R.string.configure_electronic_navigational_charts_app_name) }
// Paths to ENC data and hydrology resources private val encResourcesPath = provisionPath + application.getString(R.string.enc_res_dir) private val encDataPath = provisionPath + application.getString(R.string.enc_data_dir)
// Create an ENC exchange set from the local ENC data private val encExchangeSet = EncExchangeSet(listOf(encDataPath)) private val encEnvironmentSettings: EncEnvironmentSettings = EncEnvironmentSettings private val encMarinerSettings = encEnvironmentSettings.displaySettings.marinerSettings
// Create an empty map, to be updated once ENC data is loaded var arcGISMap by mutableStateOf(ArcGISMap())
// Passed to the composable MapView to support identify operations. val mapViewProxy = MapViewProxy()
private val _selectedEncFeature = MutableStateFlow<EncFeature?>(null) val selectedEncFeature = _selectedEncFeature.asStateFlow()
var currentColorScheme by mutableStateOf(encMarinerSettings.colorScheme) private set
var currentAreaSymbolizationType by mutableStateOf(encMarinerSettings.areaSymbolizationType) private set
var currentPointSymbolizationType by mutableStateOf(encMarinerSettings.pointSymbolizationType) private set
// Create a message dialog view model for handling error messages val messageDialogVM = MessageDialogViewModel()
init { // Provide ENC environment with location of ENC resources and configure SENC caching location encEnvironmentSettings.resourcePath = encResourcesPath encEnvironmentSettings.sencDataPath = application.externalCacheDir?.path configureEncDisplaySettings()
viewModelScope.launch { encExchangeSet.load().onSuccess {
// Set the map to the oceans basemap style, and initial viewpoint arcGISMap = ArcGISMap(BasemapStyle.ArcGISOceans).apply { initialViewpoint = Viewpoint(-32.5, 60.95, 67e3) }
encExchangeSet.datasets.forEach { encDataset -> // Create a layer for each ENC dataset and add it to the map val encCell = EncCell(encDataset) val encLayer = EncLayer(encCell) arcGISMap.operationalLayers.add(encLayer)
encLayer.load().onFailure { error -> messageDialogVM.showMessageDialog(error) } } }.onFailure { error -> messageDialogVM.showMessageDialog(error) } } }
fun updateColorScheme(colorScheme: EncColorScheme) { encMarinerSettings.colorScheme = colorScheme currentColorScheme = colorScheme }
fun updateAreaSymbolizationType(areaSymbolizationType: EncAreaSymbolizationType) { encMarinerSettings.areaSymbolizationType = areaSymbolizationType currentAreaSymbolizationType = areaSymbolizationType }
fun updatePointSymbolizationType(pointSymbolizationType: EncPointSymbolizationType) { encMarinerSettings.pointSymbolizationType = pointSymbolizationType currentPointSymbolizationType = pointSymbolizationType }
/** * Identifies the ENC feature at the tapped screen coordinate and selects it for display. */ fun identify(singleTapConfirmedEvent: SingleTapConfirmedEvent) { arcGISMap.operationalLayers.filterIsInstance<EncLayer>().forEach { encLayer -> encLayer.clearSelection() } viewModelScope.launch { mapViewProxy.identifyLayers(singleTapConfirmedEvent.screenCoordinate, 10.dp) .onSuccess { identifyResults -> val encIdentifyResult = identifyResults.firstOrNull { it.geoElements.filterIsInstance<EncFeature>().isNotEmpty() } val encLayer = encIdentifyResult?.layerContent as? EncLayer val encFeature = encIdentifyResult?.geoElements ?.filterIsInstance<EncFeature>() ?.firstOrNull() if (encLayer != null && encFeature != null) { encLayer.selectFeature(encFeature) _selectedEncFeature.value = encFeature } else { _selectedEncFeature.value = null } }.onFailure { error -> _selectedEncFeature.value = null messageDialogVM.showMessageDialog(error) } } }
override fun onCleared() { super.onCleared() encEnvironmentSettings.resourcePath = null encEnvironmentSettings.sencDataPath = null encEnvironmentSettings.displaySettings.marinerSettings.resetToDefaults() encEnvironmentSettings.displaySettings.textGroupVisibilitySettings.resetToDefaults() encEnvironmentSettings.displaySettings.viewingGroupSettings.resetToDefaults() }
/** * Disables a subset of text and viewing groups so the charts start less cluttered. */ private fun configureEncDisplaySettings() { encEnvironmentSettings.displaySettings.textGroupVisibilitySettings.apply { includeGeographicNames = false includeNatureOfSeabed = false }
encEnvironmentSettings.displaySettings.viewingGroupSettings.apply { includeDepthContours = false includeLights = false includeSpotSoundings = false }
currentColorScheme = encMarinerSettings.colorScheme currentAreaSymbolizationType = encMarinerSettings.areaSymbolizationType currentPointSymbolizationType = encMarinerSettings.pointSymbolizationType }}
val colorSchemes: List<EncColorScheme> = listOf( EncColorScheme.Day, EncColorScheme.Dusk, EncColorScheme.Night)val areaSymbolizationTypes: List<EncAreaSymbolizationType> = listOf( EncAreaSymbolizationType.Plain, EncAreaSymbolizationType.Symbolized)val pointSymbolizationTypes: List<EncPointSymbolizationType> = listOf( EncPointSymbolizationType.PaperChart, EncPointSymbolizationType.Simplified)/* Copyright 2024 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.configureelectronicnavigationalcharts
import android.content.Intentimport android.os.Bundleimport com.esri.arcgismaps.sample.sampleslib.DownloaderActivity
class DownloadActivity : DownloaderActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) downloadAndStartSample( Intent(this, MainActivity::class.java), // get the app name of the sample getString(R.string.configure_electronic_navigational_charts_app_name), listOf( // ArcGIS Portal item containing ENC hydrography resources "https://www.arcgis.com/home/item.html?id=5028bf3513ff4c38b28822d010a4937c", // ArcGIS Portal item containing the ENC dataset "https://www.arcgis.com/home/item.html?id=9d2987a825c646468b3ce7512fb76e2d" ) ) }}/* Copyright 2024 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.configureelectronicnavigationalcharts
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 com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.configureelectronicnavigationalcharts.screens.ConfigureElectronicNavigationalChartsScreen
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
enableEdgeToEdge() setContent { SampleAppTheme { SampleApp() } } }
@Composable private fun SampleApp() { Surface( color = MaterialTheme.colorScheme.background ) { ConfigureElectronicNavigationalChartsScreen( sampleName = getString(R.string.configure_electronic_navigational_charts_app_name) ) } }}/* Copyright 2024 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.configureelectronicnavigationalcharts.screens
import androidx.compose.animation.AnimatedVisibilityimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.OutlinedButtonimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dpimport androidx.lifecycle.compose.collectAsStateWithLifecycleimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.geometry.Pointimport com.arcgismaps.hydrography.EncAreaSymbolizationTypeimport com.arcgismaps.hydrography.EncColorSchemeimport com.arcgismaps.hydrography.EncPointSymbolizationTypeimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.esri.arcgismaps.sample.configureelectronicnavigationalcharts.components.ConfigureElectronicNavigationalChartsScreenViewModelimport com.esri.arcgismaps.sample.configureelectronicnavigationalcharts.components.areaSymbolizationTypesimport com.esri.arcgismaps.sample.configureelectronicnavigationalcharts.components.colorSchemesimport com.esri.arcgismaps.sample.configureelectronicnavigationalcharts.components.pointSymbolizationTypesimport com.esri.arcgismaps.sample.sampleslib.components.DropDownMenuBoximport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
/** * Main screen layout for the sample app */@Composablefun ConfigureElectronicNavigationalChartsScreen(sampleName: String) { // create a ViewModel to handle MapView interactions val mapViewModel: ConfigureElectronicNavigationalChartsScreenViewModel = viewModel() val selectedEncFeature by mapViewModel.selectedEncFeature.collectAsStateWithLifecycle() var isSettingsDialogVisible by remember { mutableStateOf(false) } var tapLocation by remember { mutableStateOf<Point?>(null) }
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { Column( modifier = Modifier .fillMaxSize() .padding(it), horizontalAlignment = Alignment.CenterHorizontally ) { MapView( modifier = Modifier .fillMaxSize() .weight(1f), arcGISMap = mapViewModel.arcGISMap, mapViewProxy = mapViewModel.mapViewProxy, onSingleTapConfirmed = { tapEvent -> tapLocation = tapEvent.mapPoint mapViewModel.identify(tapEvent) } ) { selectedEncFeature?.let { encFeature -> tapLocation?.let { location -> Callout(location = location) { Column { Text(encFeature.acronym) Text(encFeature.description) } } } } } OutlinedButton( modifier = Modifier.padding(12.dp), onClick = { isSettingsDialogVisible = true } ) { Text("Display Settings") }
DisplaySettingsContent( isSettingsDialogVisible = isSettingsDialogVisible, currentColorScheme = mapViewModel.currentColorScheme, currentAreaSymbolizationType = mapViewModel.currentAreaSymbolizationType, currentPointSymbolizationType = mapViewModel.currentPointSymbolizationType, onColorSchemeSelected = mapViewModel::updateColorScheme, onAreaSymbolizationSelected = mapViewModel::updateAreaSymbolizationType, onPointSymbolizationSelected = mapViewModel::updatePointSymbolizationType, onDismiss = { isSettingsDialogVisible = false } ) }
mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } )}
@Composablefun DisplaySettingsContent( isSettingsDialogVisible: Boolean, currentColorScheme: EncColorScheme, currentAreaSymbolizationType: EncAreaSymbolizationType, currentPointSymbolizationType: EncPointSymbolizationType, onColorSchemeSelected: (EncColorScheme) -> Unit, onAreaSymbolizationSelected: (EncAreaSymbolizationType) -> Unit, onPointSymbolizationSelected: (EncPointSymbolizationType) -> Unit, onDismiss: () -> Unit,) { AnimatedVisibility(isSettingsDialogVisible) { SampleDialog(onDismissRequest = onDismiss) { Text("Display Settings", style = MaterialTheme.typography.titleMedium) DropDownMenuBox( modifier = Modifier.fillMaxWidth(), textFieldLabel = "Color Scheme", textFieldValue = currentColorScheme.javaClass.simpleName, dropDownItemList = colorSchemes.map { it.javaClass.simpleName }, onIndexSelected = { index -> onColorSchemeSelected(colorSchemes[index]) } ) DropDownMenuBox( modifier = Modifier.fillMaxWidth(), textFieldLabel = "Area Symbolization Type", textFieldValue = currentAreaSymbolizationType.javaClass.simpleName, dropDownItemList = areaSymbolizationTypes.map { it.javaClass.simpleName }, onIndexSelected = { index -> onAreaSymbolizationSelected(areaSymbolizationTypes[index]) } ) DropDownMenuBox( modifier = Modifier.fillMaxWidth(), textFieldLabel = "Point Symbolization Type", textFieldValue = currentPointSymbolizationType.javaClass.simpleName, dropDownItemList = pointSymbolizationTypes.map { it.javaClass.simpleName }, onIndexSelected = { index -> onPointSymbolizationSelected(pointSymbolizationTypes[index]) } ) } }}