|
| 1 | +// Copyright 2024 Esri |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +import ArcGIS |
| 16 | +import SwiftUI |
| 17 | + |
| 18 | +struct ApplyDictionaryRendererToGraphicsOverlayView: View { |
| 19 | + /// A scene with a topographic basemap. |
| 20 | + @State private var scene = Scene(basemapStyle: .arcGISTopographic) |
| 21 | + |
| 22 | + /// The graphics overlay for displaying the message graphics on the scene. |
| 23 | + @State private var graphicsOverlay = GraphicsOverlay() |
| 24 | + |
| 25 | + /// The camera for zooming the scene view to the message graphics. |
| 26 | + @State private var camera: Camera? |
| 27 | + |
| 28 | + /// The error shown in the error alert. |
| 29 | + @State private var error: Error? |
| 30 | + |
| 31 | + var body: some View { |
| 32 | + SceneView(scene: scene, camera: $camera, graphicsOverlays: [graphicsOverlay]) |
| 33 | + .task { |
| 34 | + do { |
| 35 | + // Sets up the graphics overlay when the sample opens. |
| 36 | + graphicsOverlay.renderer = try await makeMIL2525DRenderer() |
| 37 | + try graphicsOverlay.addGraphics(makeMessageGraphics()) |
| 38 | + |
| 39 | + // Sets the camera to look at the graphics in the graphics overlay. |
| 40 | + guard let extent = graphicsOverlay.extent else { return } |
| 41 | + camera = Camera( |
| 42 | + lookingAt: extent.center, |
| 43 | + distance: 15_000, |
| 44 | + heading: 0, |
| 45 | + pitch: 70, |
| 46 | + roll: 0 |
| 47 | + ) |
| 48 | + } catch { |
| 49 | + self.error = error |
| 50 | + } |
| 51 | + } |
| 52 | + .errorAlert(presentingError: $error) |
| 53 | + } |
| 54 | + |
| 55 | + /// Creates a dictionary renderer for styling with MIL-STD-2525D symbols. |
| 56 | + /// - Returns: A new `DictionaryRenderer` object. |
| 57 | + private func makeMIL2525DRenderer() async throws -> DictionaryRenderer { |
| 58 | + // Creates a dictionary symbol style from a dictionary style portal item. |
| 59 | + let portalItem = PortalItem( |
| 60 | + portal: .arcGISOnline(connection: .anonymous), |
| 61 | + id: .jointMilitarySymbologyDictionaryStyle |
| 62 | + ) |
| 63 | + let dictionarySymbolStyle = DictionarySymbolStyle(portalItem: portalItem) |
| 64 | + try await dictionarySymbolStyle.load() |
| 65 | + |
| 66 | + // Uses the "Ordered Anchor Points" for the symbol style draw rule. |
| 67 | + let drawRuleConfiguration = dictionarySymbolStyle.configurations.first { $0.name == "model" } |
| 68 | + drawRuleConfiguration?.value = "ORDERED ANCHOR POINTS" |
| 69 | + |
| 70 | + return DictionaryRenderer(dictionarySymbolStyle: dictionarySymbolStyle) |
| 71 | + } |
| 72 | + |
| 73 | + /// Creates graphics from messages in an XML file. |
| 74 | + /// - Returns: An array of new `Graphic` objects. |
| 75 | + private func makeMessageGraphics() throws -> [Graphic] { |
| 76 | + // Gets the data from the local XML file. |
| 77 | + let messagesData = try Data(contentsOf: .mil2525dMessagesXMLFile) |
| 78 | + let parser = MessageParser(data: messagesData) |
| 79 | + |
| 80 | + if parser.parse() { |
| 81 | + // Creates graphics from the parsed messages. |
| 82 | + return parser.messages.compactMap { message in |
| 83 | + guard let messageWKID = message.wkid, |
| 84 | + let wkid = WKID(messageWKID) else { return nil } |
| 85 | + let spatialReference = SpatialReference(wkid: wkid) |
| 86 | + let points = message.controlPoints.map { x, y in |
| 87 | + Point(x: x, y: y, spatialReference: spatialReference) |
| 88 | + } |
| 89 | + return Graphic(geometry: Multipoint(points: points), attributes: message.other) |
| 90 | + } |
| 91 | + } else if let error = parser.parserError { |
| 92 | + throw error |
| 93 | + } else { |
| 94 | + return [] |
| 95 | + } |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +// MARK: Message Parser |
| 100 | + |
| 101 | +private extension ApplyDictionaryRendererToGraphicsOverlayView { |
| 102 | + /// A parser for the XML file containing the MIL-STD-2525D messages. |
| 103 | + final class MessageParser: XMLParser, XMLParserDelegate { |
| 104 | + /// The parsed messages. |
| 105 | + private(set) var messages: [Message] = [] |
| 106 | + |
| 107 | + /// The values of the message element currently being parsed. |
| 108 | + private var currentMessage: Message? |
| 109 | + |
| 110 | + /// The characters of the XML element currently being parsed. |
| 111 | + private var currentElementContents = "" |
| 112 | + |
| 113 | + override init(data: Data) { |
| 114 | + super.init(data: data) |
| 115 | + self.delegate = self |
| 116 | + } |
| 117 | + |
| 118 | + /// Creates a new `currentMessage` when a message start tag is encountered. |
| 119 | + func parser( |
| 120 | + _ parser: XMLParser, |
| 121 | + didStartElement elementName: String, |
| 122 | + namespaceURI: String?, |
| 123 | + qualifiedName qName: String?, |
| 124 | + attributes attributeDict: [String: String] = [:] |
| 125 | + ) { |
| 126 | + if elementName == "message" { |
| 127 | + currentMessage = Message() |
| 128 | + } |
| 129 | + currentElementContents.removeAll() |
| 130 | + } |
| 131 | + |
| 132 | + /// Adds the characters of the current element to `currentElementContents`. |
| 133 | + func parser(_ parser: XMLParser, foundCharacters string: String) { |
| 134 | + currentElementContents.append(contentsOf: string) |
| 135 | + } |
| 136 | + |
| 137 | + /// Adds the contents of the current element to the `currentMessage` when an end tag is encountered. |
| 138 | + func parser( |
| 139 | + _ parser: XMLParser, |
| 140 | + didEndElement elementName: String, |
| 141 | + namespaceURI: String?, |
| 142 | + qualifiedName qName: String? |
| 143 | + ) { |
| 144 | + switch elementName { |
| 145 | + case "_control_points": |
| 146 | + currentMessage?.controlPoints = currentElementContents.split(separator: ";") |
| 147 | + .map { pair in |
| 148 | + let coordinates = pair.split(separator: ",") |
| 149 | + return (x: Double(coordinates.first!)!, y: Double(coordinates.last!)!) |
| 150 | + } |
| 151 | + case "message": |
| 152 | + messages.append(currentMessage!) |
| 153 | + currentMessage = nil |
| 154 | + case "messages": |
| 155 | + break |
| 156 | + case "_wkid": |
| 157 | + currentMessage?.wkid = Int(currentElementContents) |
| 158 | + default: |
| 159 | + currentMessage?.other[elementName] = currentElementContents |
| 160 | + } |
| 161 | + |
| 162 | + currentElementContents.removeAll() |
| 163 | + } |
| 164 | + } |
| 165 | + |
| 166 | + /// The parsed values from an XML message element. |
| 167 | + struct Message { |
| 168 | + /// The x and y values of the control points element. |
| 169 | + var controlPoints: [(x: Double, y: Double)] = [] |
| 170 | + /// The value of the wkid element. |
| 171 | + var wkid: Int? |
| 172 | + /// The other elements and their values. |
| 173 | + var other: [String: any Sendable] = [:] |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +// MARK: Helper Extensions |
| 178 | + |
| 179 | +private extension PortalItem.ID { |
| 180 | + /// The ID for the "Joint Military Symbology MIL-STD-2525D" dictionary style portal item on ArcGIS Online. |
| 181 | + static var jointMilitarySymbologyDictionaryStyle: Self { |
| 182 | + .init("d815f3bdf6e6452bb8fd153b654c94ca")! |
| 183 | + } |
| 184 | +} |
| 185 | + |
| 186 | +private extension URL { |
| 187 | + /// The URL to the local XML file containing messages with MIL-STD-2525D fields. |
| 188 | + static var mil2525dMessagesXMLFile: URL { |
| 189 | + Bundle.main.url(forResource: "Mil2525DMessages", withExtension: "xml")! |
| 190 | + } |
| 191 | +} |
0 commit comments