Create graphics from an XML file with key-value pairs for each graphic, and display the military symbols using a MIL-STD-2525D web style in 3D.

Use case
Use a dictionary renderer on a graphics overlay to display more transient data, such as military messages coming through a local tactical network.
How to use the sample
Run the sample and view the military symbols on the map.
How it works
- Create a new
DictionarySymbolStyleobject with the “mil2525d” specification type and set the style’s draw rule configuration to “ORDERED ANCHOR POINTS”. - Create a new
DictionaryRendererobject with the dictionary symbol style. - Create an instance of
GraphicsOverlay. - Set the dictionary renderer to the graphics overlay.
- Parse through the local XML file creating a map of key/value pairs for each block of attributes.
- Create an instance of
Graphicfor each attribute. - Use the
_wkidkey to get the geometry’s spatial reference. - Use the
_control_pointskey to get the geometry’s shape. - Add the graphic to the graphics overlay.
Relevant API
- DictionaryRenderer
- DictionarySymbolStyle
- GraphicsOverlay
About the data
The sample viewer will load MIL-STD-2525D symbol dictionary web style from ArcGIS Online before loading the sample. This ArcGIS Web Style is for use to build custom applications that incorporate the MIL-STD-2525D symbol dictionary. This style supports a configuration for modeling locations as ordered anchor points or full geometries.
A local XML file containing messages with MIL-STD-2525D fields for military symbology (MIL-STD-2525D Messages 100.13.0) is also used. This is downloaded from ArcGIS Online automatically.
Tags
defense, military, situational awareness, tactical, visualization
Sample Code
/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.applydictionaryrenderertographicsoverlay
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), getString(R.string.apply_dictionary_renderer_to_graphics_overlay_app_name), listOf( "https://www.arcgis.com/home/item.html?id=8776cfc26eed4485a03de6316826384c" ) ) }}/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.applydictionaryrenderertographicsoverlay
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.runtime.Composableimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.applydictionaryrenderertographicsoverlay.screens.ApplyDictionaryRendererToGraphicsOverlayScreen
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // authentication with an API key or named user is // required to access basemaps and other location services ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN)
setContent { SampleAppTheme { ApplyDictionaryRendererToGraphicsOverlayApp() } } }
@Composable private fun ApplyDictionaryRendererToGraphicsOverlayApp() { Surface(color = MaterialTheme.colorScheme.background) { ApplyDictionaryRendererToGraphicsOverlayScreen( sampleName = getString(R.string.apply_dictionary_renderer_to_graphics_overlay_app_name) ) } }}/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.applydictionaryrenderertographicsoverlay.components
import android.app.Applicationimport android.util.Xmlimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.geometry.Multipointimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.symbology.DictionaryRendererimport com.arcgismaps.mapping.symbology.DictionarySymbolStyleimport com.arcgismaps.mapping.view.Cameraimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.esri.arcgismaps.sample.applydictionaryrenderertographicsoverlay.Rimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport kotlinx.coroutines.launchimport org.xmlpull.v1.XmlPullParserimport java.io.File
/** * ViewModel for the "Apply dictionary renderer to graphics overlay" sample. */class ApplyDictionaryRendererToGraphicsOverlayViewModel(private val app: Application) : AndroidViewModel(app) {
// The scene shown in the SceneView composable val arcGISScene = ArcGISScene(basemapStyle = BasemapStyle.ArcGISTopographic)
// Graphics overlay that will hold the message graphics val graphicsOverlay: GraphicsOverlay = GraphicsOverlay()
// SceneViewProxy to enable programmatic viewpoint changes val sceneViewProxy = SceneViewProxy()
// Used to display error messages val messageDialogVM = MessageDialogViewModel()
// Provision path where downloaded sample assets will be placed by the downloader activity private val provisionPath: String by lazy { app.getExternalFilesDir(null)?.path.toString() + File.separator + app.getString(R.string.apply_dictionary_renderer_to_graphics_overlay_app_name) }
init { viewModelScope.launch { // Load the scene first arcGISScene.load().onFailure { throwable -> messageDialogVM.showMessageDialog(throwable) }
// Create and apply dictionary renderer from a web style val dictionaryRenderer = createMil2525dDictionaryRenderer().getOrElse { messageDialogVM.showMessageDialog(it) return@launch }
// Create the point graphics from a local XML file val pointGraphics = makeMessageGraphics().getOrElse { messageDialogVM.showMessageDialog(it) return@launch }
// Set the graphics overlay to use the dictionary renderer and add graphics graphicsOverlay.apply { renderer = dictionaryRenderer graphics.addAll(pointGraphics) }
// Sets the camera to look a the graphics in the graphics overlay graphicsOverlay.extent?.let { extent -> sceneViewProxy.setViewpointCamera( camera = Camera( lookAtPoint = extent.center, distance = 15000.0, heading = 0.0, pitch = 70.0, roll = 0.0 ) ) } } }
/** * Create and load a [DictionarySymbolStyle] from a web style and use it to create a [DictionaryRenderer]. */ private suspend fun createMil2525dDictionaryRenderer(): Result<DictionaryRenderer> { // Creates a dictionary symbol style from a dictionary style portal item. val portalItem = PortalItem( portal = Portal.arcGISOnline(Portal.Connection.Anonymous), itemId = "d815f3bdf6e6452bb8fd153b654c94ca" )
val dictionarySymbolStyle = DictionarySymbolStyle(portalItem = portalItem)
return dictionarySymbolStyle.load().mapCatching { // Uses the "Ordered Anchor Points" for the symbol style draw rule. // Get the model configuration from the style's list of configurations. val modelConfiguration = dictionarySymbolStyle.configurations.firstOrNull { it.name.equals("model", ignoreCase = true) } if (modelConfiguration != null) { // Set the draw rule of the style to "ORDERED ANCHOR POINTS". modelConfiguration.value = "ORDERED ANCHOR POINTS" } DictionaryRenderer(dictionarySymbolStyle = dictionarySymbolStyle) } }
/** * Create point graphics from a local XML file containing `MIL-STD-2525D` message data. */ private fun makeMessageGraphics(): Result<List<Graphic>> { return runCatching { val xmlFile = File(provisionPath, "Mil2525DMessages.xml") val messageXml = xmlFile.readText() val messages = MessageXmlParser().parse(messageXml)
val graphics = messages.map { message -> val wkid = message.wkid val controlPoints = message.controlPoints if (wkid == null || controlPoints.isEmpty()) { throw IllegalArgumentException("Invalid message: missing wkid or control points") } val spatialReference = SpatialReference(wkid = wkid) val points = controlPoints.map { (x, y) -> Point(x = x, y = y, spatialReference = spatialReference) } Graphic(geometry = Multipoint(points), attributes = message.other) } graphics } }}
/** * Simple XML parser for the `MIL-STD-2525D` message XML file. * This is a basic implementation and does not cover all edge cases. */class MessageXmlParser { private val messagesTag = "messages" private val messageTag = "message" private val controlPointsTag = "_control_points" private val wkidTag = "_wkid"
/** * Parses the provided XML string and returns a list of Message objects. */ fun parse(xml: String): List<Message> { val messages = mutableListOf<Message>()
val parser = Xml.newPullParser().apply { setInput(xml.reader()) }
var eventType = parser.eventType var currentControlPoints: List<Pair<Double, Double>> = emptyList() var currentWkid: Int? = null var currentAttrs = mutableMapOf<String, String?>()
while (eventType != XmlPullParser.END_DOCUMENT) { when (eventType) { XmlPullParser.START_TAG -> { when (val tagName = parser.name) { controlPointsTag -> { val controlPointsText = parser.nextText() currentControlPoints = controlPointsText.split(";") .mapNotNull { val coords = it.split(",") if (coords.size == 2) { val x = coords[0].toDoubleOrNull() val y = coords[1].toDoubleOrNull() if (x != null && y != null) Pair(x, y) else null } else null } }
wkidTag -> { currentWkid = parser.nextText().toIntOrNull() }
messageTag, messagesTag -> { /* ignore container tags */ }
else -> { if (tagName != null) { currentAttrs[tagName] = parser.nextText() } } } }
XmlPullParser.END_TAG -> { if (parser.name == messageTag) { messages.add( Message( controlPoints = currentControlPoints, wkid = currentWkid, other = currentAttrs ) ) currentControlPoints = emptyList() currentWkid = null currentAttrs = mutableMapOf() } } }
eventType = parser.next() }
return messages }}
data class Message( val controlPoints: List<Pair<Double, Double>>, val wkid: Int?, val other: Map<String, String?> = emptyMap())/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.applydictionaryrenderertographicsoverlay.screens
import androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.Scaffoldimport androidx.compose.runtime.Composableimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.esri.arcgismaps.sample.applydictionaryrenderertographicsoverlay.components.ApplyDictionaryRendererToGraphicsOverlayViewModelimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBarimport androidx.compose.ui.Modifier
/** * Screen composable that displays a SceneView and binds to the [ApplyDictionaryRendererToGraphicsOverlayViewModel]. * The SceneView shows graphics styled with a DictionaryRenderer loaded from a web style and uses a Camera * provided by the ViewModel to set the initial viewpoint. */@Composablefun ApplyDictionaryRendererToGraphicsOverlayScreen(sampleName: String) { val viewModel: ApplyDictionaryRendererToGraphicsOverlayViewModel = viewModel()
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { padding -> // Composable SceneView that renders graphics styled with DictionaryRenderer // using configured ArcGISObjects from the ViewModel (scene, overlays, proxy) SceneView( modifier = Modifier .fillMaxSize() .padding(padding), arcGISScene = viewModel.arcGISScene, sceneViewProxy = viewModel.sceneViewProxy, graphicsOverlays = listOf(viewModel.graphicsOverlay) )
// Show any error/messages from the view model viewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } )}