Apply dictionary renderer to graphics overlay

View on GitHub

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.

Image of Apply dictionary renderer to graphics overlay sample

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

Open the sample and view the military symbols on the scene.

How it works

  1. Create a new DictionarySymbolStyle object with a protal item and set the style's draw rule configuration to "ORDERED ANCHOR POINTS".
  2. Create a new DictionaryRenderer object with the dictionary symbol style.
  3. Create an instance of GraphicsOverlay.
  4. Set the dictionary renderer to the graphics overlay's renderer.
  5. Parse the local XML file, creating a map of key/value pairs for each block of attributes.
  6. Create an instance of Graphic for each attribute.
  7. Use the _wkid key to get the geometry's spatial reference.
  8. Use the _control_points key to get the geometry's shape.
  9. Add the graphics to the graphics overlay.

Relevant API

  • DictionaryRenderer
  • DictionarySymbolStyle
  • GraphicsOverlay

About the data

The sample uses the Joint Military Symbology MIL-STD-2525D dictionary style from ArcGIS Online. 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

ApplyDictionaryRendererToGraphicsOverlayView.swift
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// 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
//
//   https://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.

import ArcGIS
import SwiftUI

struct ApplyDictionaryRendererToGraphicsOverlayView: View {
    /// A scene with a topographic basemap.
    @State private var scene = Scene(basemapStyle: .arcGISTopographic)

    /// The graphics overlay for displaying the message graphics on the scene.
    @State private var graphicsOverlay = GraphicsOverlay()

    /// The camera for zooming the scene view to the message graphics.
    @State private var camera: Camera?

    /// The error shown in the error alert.
    @State private var error: Error?

    var body: some View {
        SceneView(scene: scene, camera: $camera, graphicsOverlays: [graphicsOverlay])
            .task {
                do {
                    // Sets up the graphics overlay when the sample opens.
                    graphicsOverlay.renderer = try await makeMIL2525DRenderer()
                    try graphicsOverlay.addGraphics(makeMessageGraphics())

                    // Sets the camera to look at the graphics in the graphics overlay.
                    guard let extent = graphicsOverlay.extent else { return }
                    camera = Camera(
                        lookingAt: extent.center,
                        distance: 15_000,
                        heading: 0,
                        pitch: 70,
                        roll: 0
                    )
                } catch {
                    self.error = error
                }
            }
            .errorAlert(presentingError: $error)
    }

    /// Creates a dictionary renderer for styling with MIL-STD-2525D symbols.
    /// - Returns: A new `DictionaryRenderer` object.
    private func makeMIL2525DRenderer() async throws -> DictionaryRenderer {
        // Creates a dictionary symbol style from a dictionary style portal item.
        let portalItem = PortalItem(
            portal: .arcGISOnline(connection: .anonymous),
            id: .jointMilitarySymbologyDictionaryStyle
        )
        let dictionarySymbolStyle = DictionarySymbolStyle(portalItem: portalItem)
        try await dictionarySymbolStyle.load()

        // Uses the "Ordered Anchor Points" for the symbol style draw rule.
        let drawRuleConfiguration = dictionarySymbolStyle.configurations.first { $0.name == "model" }
        drawRuleConfiguration?.value = "ORDERED ANCHOR POINTS"

        return DictionaryRenderer(dictionarySymbolStyle: dictionarySymbolStyle)
    }

    /// Creates graphics from messages in an XML file.
    /// - Returns: An array of new `Graphic` objects.
    private func makeMessageGraphics() throws -> [Graphic] {
        // Gets the data from the local XML file.
        let messagesData = try Data(contentsOf: .mil2525dMessagesXMLFile)
        let parser = MessageParser(data: messagesData)

        if parser.parse() {
            // Creates graphics from the parsed messages.
            return parser.messages.compactMap { message in
                guard let messageWKID = message.wkid,
                      let wkid = WKID(messageWKID) else { return nil }
                let spatialReference = SpatialReference(wkid: wkid)
                let points = message.controlPoints.map { x, y in
                    Point(x: x, y: y, spatialReference: spatialReference)
                }
                return Graphic(geometry: Multipoint(points: points), attributes: message.other)
            }
        } else if let error = parser.parserError {
            throw error
        } else {
            return []
        }
    }
}

// MARK: Message Parser

private extension ApplyDictionaryRendererToGraphicsOverlayView {
    /// A parser for the XML file containing the MIL-STD-2525D messages.
    final class MessageParser: XMLParser, XMLParserDelegate {
        /// The parsed messages.
        private(set) var messages: [Message] = []

        /// The values of the message element currently being parsed.
        private var currentMessage: Message?

        /// The characters of the XML element currently being parsed.
        private var currentElementContents = ""

        override init(data: Data) {
            super.init(data: data)
            self.delegate = self
        }

        /// Creates a new `currentMessage` when a message start tag is encountered.
        func parser(
            _ parser: XMLParser,
            didStartElement elementName: String,
            namespaceURI: String?,
            qualifiedName qName: String?,
            attributes attributeDict: [String: String] = [:]
        ) {
            if elementName == "message" {
                currentMessage = Message()
            }
            currentElementContents.removeAll()
        }

        /// Adds the characters of the current element to `currentElementContents`.
        func parser(_ parser: XMLParser, foundCharacters string: String) {
            currentElementContents.append(contentsOf: string)
        }

        /// Adds the contents of the current element to the `currentMessage` when an end tag is encountered.
        func parser(
            _ parser: XMLParser,
            didEndElement elementName: String,
            namespaceURI: String?,
            qualifiedName qName: String?
        ) {
            switch elementName {
            case "_control_points":
                currentMessage?.controlPoints = currentElementContents.split(separator: ";")
                    .map { pair in
                        let coordinates = pair.split(separator: ",")
                        return (x: Double(coordinates.first!)!, y: Double(coordinates.last!)!)
                    }
            case "message":
                messages.append(currentMessage!)
                currentMessage = nil
            case "messages":
                break
            case "_wkid":
                currentMessage?.wkid = Int(currentElementContents)
            default:
                currentMessage?.other[elementName] = currentElementContents
            }

            currentElementContents.removeAll()
        }
    }

    /// The parsed values from an XML message element.
    struct Message {
        /// The x and y values of the control points element.
        var controlPoints: [(x: Double, y: Double)] = []
        /// The value of the wkid element.
        var wkid: Int?
        /// The other elements and their values.
        var other: [String: any Sendable] = [:]
    }
}

// MARK: Helper Extensions

private extension PortalItem.ID {
    /// The ID for the "Joint Military Symbology MIL-STD-2525D" dictionary style portal item on ArcGIS Online.
    static var jointMilitarySymbologyDictionaryStyle: Self {
        .init("d815f3bdf6e6452bb8fd153b654c94ca")!
    }
}

private extension URL {
    /// The URL to the local XML file containing messages with MIL-STD-2525D fields.
    static var mil2525dMessagesXMLFile: URL {
        Bundle.main.url(forResource: "Mil2525DMessages", withExtension: "xml")!
    }
}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.