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
AGSDictionarySymbolStyle
object with the "mil2525d" specification type and set the style's draw rule configuration to "ORDERED ANCHOR POINTS". - Create a new
AGSDictionaryRenderer
object with the dictionary symbol style. - Create an instance of
AGSGraphicsOverlay
. - 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
AGSGraphic
for each attribute. - Use the
_wkid
key to get the geometry's spatial reference. - Use the
_control_points
key to get the geometry's shape. - Add the graphic to the graphics overlay.
Relevant API
- AGSDictionaryRenderer
- AGSDictionarySymbolStyle
- AGSGraphicsOverlay
About the data
The sample viewer will load MIL-STD-2525D symbol dictionary web style from ArcGIS Online before loading the sample.
Tags
defense, military, situational awareness, tactical, visualization
Sample Code
GraphicsOverlayDictionaryRenderer3DViewController.swift
//
// Copyright © 2019 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.
//
import UIKit
import ArcGIS
/// A view controller that manages the interface of the Graphics Overlay
/// (Dictionary Renderer) 3D sample.
class GraphicsOverlayDictionaryRenderer3DViewController: UIViewController {
/// The scene view managed by the view controller.
@IBOutlet weak var sceneView: AGSSceneView! {
didSet {
sceneView.scene = AGSScene(basemapStyle: .arcGISTopographic)
sceneView.graphicsOverlays.add(makeGraphicsOverlay())
}
}
/// Creates a graphics overlay configured to display MIL-STD-2525D
/// symbology.
/// - Returns: A new `AGSGraphicsOverlay` object.
func makeGraphicsOverlay() -> AGSGraphicsOverlay {
let graphicsOverlay = AGSGraphicsOverlay()
// Create the style from Joint Military Symbology MIL-STD-2525D
// portal item.
let dictionarySymbolStyle = AGSDictionarySymbolStyle(
portalItem: AGSPortalItem(
portal: .arcGISOnline(withLoginRequired: false),
itemID: "d815f3bdf6e6452bb8fd153b654c94ca"
)
)
dictionarySymbolStyle.load { [weak self, weak graphicsOverlay] error in
guard let self = self, let graphicsOverlay = graphicsOverlay else {
return
}
if let error = error {
self.presentAlert(error: error)
} else {
let camera = AGSCamera(lookAt: graphicsOverlay.extent.center, distance: 15_000, heading: 0, pitch: 70, roll: 0)
self.sceneView.setViewpointCamera(camera)
// Use Ordered Anchor Points for the symbol style draw rule.
if let drawRuleConfiguration = dictionarySymbolStyle.configurations.first(where: { $0.name == "model" }) {
drawRuleConfiguration.value = "ORDERED ANCHOR POINTS"
}
graphicsOverlay.renderer = AGSDictionaryRenderer(dictionarySymbolStyle: dictionarySymbolStyle)
}
}
// Read the messages and add a graphic to the overlay for each messages.
if let messagesURL = Bundle.main.url(forResource: "Mil2525DMessages", withExtension: "xml") {
do {
let messagesData = try Data(contentsOf: messagesURL)
let messages = try MessageParser().parseMessages(from: messagesData)
let graphics = messages.map { AGSGraphic(geometry: AGSMultipoint(points: $0.points), symbol: nil, attributes: $0.attributes) }
graphicsOverlay.graphics.addObjects(from: graphics)
} catch {
print("Error reading or decoding messages: \(error)")
}
} else {
preconditionFailure("Missing Mil2525DMessages.xml")
}
return graphicsOverlay
}
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
// Add the source code button item to the right of navigation bar.
(navigationItem.rightBarButtonItem as? SourceCodeBarButtonItem)?.filenames = ["GraphicsOverlayDictionaryRenderer3DViewController"]
}
}
struct Message {
var points: [AGSPoint]
var attributes: [String: Any]
}
class MessageParser: NSObject {
struct ControlPoint {
var x: Double
var y: Double
}
private var controlPoints = [ControlPoint]()
private var wkid: Int?
private var attributes = [String: Any]()
private var contentsOfCurrentElement = ""
private var parsedMessages = [Message]()
func didFinishParsingMessage() {
let spatialReference = AGSSpatialReference(wkid: wkid!)
let points = controlPoints.map { AGSPoint(x: $0.x, y: $0.y, spatialReference: spatialReference) }
let message = Message(points: points, attributes: attributes)
parsedMessages.append(message)
wkid = nil
controlPoints.removeAll()
attributes.removeAll()
}
func parseMessages(from data: Data) throws -> [Message] {
defer { parsedMessages.removeAll() }
let parser = XMLParser(data: data)
parser.delegate = self
let parsingSucceeded = parser.parse()
if parsingSucceeded {
return parsedMessages
} else if let error = parser.parserError {
throw error
} else {
return []
}
}
}
extension MessageParser: XMLParserDelegate {
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String: String] = [:]) {
contentsOfCurrentElement.removeAll()
}
func parser(_ parser: XMLParser, foundCharacters string: String) {
contentsOfCurrentElement.append(contentsOf: string)
}
enum Element: String {
case controlPoints = "_control_points"
case message
case messages
case wkid = "_wkid"
}
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
if let element = Element(rawValue: elementName) {
switch element {
case .controlPoints:
controlPoints = contentsOfCurrentElement.split(separator: ";").map { (pair) in
let coordinates = pair.split(separator: ",")
return ControlPoint(x: Double(coordinates.first!)!, y: Double(coordinates.last!)!)
}
case .message:
didFinishParsingMessage()
case .messages:
break
case .wkid:
wkid = Int(contentsOfCurrentElement)
}
} else {
attributes[elementName] = contentsOfCurrentElement
}
contentsOfCurrentElement.removeAll()
}
}