Create and save KML file

View on GitHub
Sample viewer app

Construct a KML document and save it as a KMZ file.

KML style settings Sketching a KML

Use case

If you need to create and save data on the fly, you can use KML to create points, lines, and polygons by sketching on the map, customizing the style, and serializing them as KML nodes in a KML Document. Once complete, you can share the KML data with others that are using a KML reading application, such as ArcGIS Earth.

How to use the sample

Tap on the middle button in the bottom toolbar to add a new KML. Select a type of feature and choose its color or icon. Tap on the map to sketch the KML. Tap the bottom middle button to complete the sketch. Tap the button on the right to save the KMZ file. Tap the left button to clear the current KML document.

How it works

  1. Create an AGSKMLDocument.
  2. Create an AGSKMLDataset using the AGSKMLDocument.
  3. Create an AGSKMLLayer using the AGSKMLDataset and add it to the map's operationalLayers array.
  4. Create AGSGeometry using AGSSketchEditor.
  5. Project that AGSGeometry to WGS84 using class AGSGeometryEngine.projectGeometry(_:to:).
  6. Create an AGSKMLGeometry object using that projected AGSGeometry.
  7. Create an AGSKMLPlacemark using the AGSKMLGeometry.
  8. Add the AGSKMLPlacemark to the AGSKMLDocument.
  9. Set the AGSKMLStyle for the AGSKMLPlacemark.
  10. When finished with adding AGSKMLPlacemark nodes to the AGSKMLDocument, save the AGSKMLDocument to a file using the AGSKMLNode.save(toFileURL:completion:) method.

Relevant API

  • AGSGeometryEngine
  • AGSKMLDataset
  • AGSKMLDocument
  • AGSKMLGeometry
  • AGSKMLLayer
  • AGSKMLNode
  • AGSKMLPlacemark
  • AGSKMLStyle
  • AGSSketchEditor

Tags

Keyhole, KML, KMZ, OGC

Sample Code

CreateAndSaveKMLViewController.swift
                                                                                                                                                                                                                                                                                   
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
// Copyright © 2020 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

class CreateAndSaveKMLViewController: UIViewController {
    // Set the map.
    @IBOutlet var mapView: AGSMapView! {
        didSet {
            mapView.map = AGSMap(basemapStyle: .arcGISDarkGrayBase)
            mapView.sketchEditor = AGSSketchEditor()
        }
    }

    @IBOutlet var addButton: UIBarButtonItem!
    @IBOutlet var sketchDoneButton: UIBarButtonItem!
    @IBOutlet var toolbar: UIToolbar!
    @IBOutlet var actionButtonItem: UIBarButtonItem!
    @IBOutlet var resetButtonItem: UIBarButtonItem!

    // Prompt feature selection action sheet.
    @IBAction func addFeature() {
        let alertController = UIAlertController(title: "Select Feature", message: nil, preferredStyle: .actionSheet)
        let pointAction = UIAlertAction(title: "Point", style: .default) { (_) in
            self.addPoint()
        }
        alertController.addAction(pointAction)
        let polylineAction = UIAlertAction(title: "Polyline", style: .default) { (_) in
            self.addPolyline()
        }
        alertController.addAction(polylineAction)
        let polygonAction = UIAlertAction(title: "Polygon", style: .default) { (_) in
            self.addPolygon()
        }
        alertController.addAction(polygonAction)

        // Add "cancel" item.
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
        alertController.addAction(cancelAction)

        alertController.popoverPresentationController?.barButtonItem = addButton
        present(alertController, animated: true)
    }

    // Prompt options to allow the user to save the KMZ file.
    @IBAction func saveKMZ(_ sender: UIBarButtonItem) {
        let kmzProvider = KMZProvider(document: kmlDocument)
        let activityViewController = UIActivityViewController(activityItems: [kmzProvider], applicationActivities: nil)
        activityViewController.popoverPresentationController?.barButtonItem = sender
        present(activityViewController, animated: true)
        activityViewController.completionWithItemsHandler = { _, completed, _, activityError in
            if completed {
                kmzProvider.deleteKMZ()
            } else if let error = activityError {
                self.presentAlert(error: error)
            }
        }
    }

    // Complete the current sketch and add it to the KML document.
    @IBAction func completeSketch() {
        let geometry = mapView.sketchEditor?.geometry
        let projectedGeometry = AGSGeometryEngine.projectGeometry(geometry!, to: .wgs84())
        let kmlGeometry = AGSKMLGeometry(geometry: projectedGeometry!, altitudeMode: .clampToGround)
        let currentPlacemark = AGSKMLPlacemark(geometry: kmlGeometry!)
        currentPlacemark.style = kmlStyle
        kmlDocument.addChildNode(currentPlacemark)
        mapView.sketchEditor?.stop()
        kmlStyle = nil
        updateToolbarItems()
        actionButtonItem?.isEnabled = true
    }

    // Reset the KML.
    @IBAction func resetKML() {
        actionButtonItem.isEnabled = false
        mapView.map?.operationalLayers.removeAllObjects()
        kmlDocument = AGSKMLDocument()
        let kmlDataset = AGSKMLDataset(rootNode: kmlDocument)
        let kmlLayer = AGSKMLLayer(kmlDataset: kmlDataset)
        mapView.map?.operationalLayers.add(kmlLayer)
    }

    var kmlDocument = AGSKMLDocument()
    var kmlStyle: AGSKMLStyle?
    let colors: [(String, UIColor)] = [
        ("Red", .red),
        ("Yellow", .yellow),
        ("White", .white),
        ("Purple", .purple),
        ("Orange", .orange),
        ("Magenta", .magenta),
        ("Light gray", .lightGray),
        ("Gray", .gray),
        ("Dark gray", .darkGray),
        ("Green", .green),
        ("Cyan", .cyan),
        ("Brown", .brown),
        ("Blue", .blue),
        ("Black", .black)
    ]

    // Prompt icon selection action sheet.
    func addPoint() {
        let alertController = UIAlertController(title: "Select Icon", message: "This icon will be used for the new feature", preferredStyle: .actionSheet)
        let icons: [(String, URL)] = [
            ("No style", URL(string: "http://resources.esri.com/help/900/arcgisexplorer/sdk/doc/bitmaps/148cca9a-87a8-42bd-9da4-5fe427b6fb7b127.png")!),
            ("Star", URL(string: "https://static.arcgis.com/images/Symbols/Shapes/BlueStarLargeB.png")!),
            ("Diamond", URL(string: "https://static.arcgis.com/images/Symbols/Shapes/BlueDiamondLargeB.png")!),
            ("Circle", URL(string: "https://static.arcgis.com/images/Symbols/Shapes/BlueCircleLargeB.png")!),
            ("Square", URL(string: "https://static.arcgis.com/images/Symbols/Shapes/BlueSquareLargeB.png")!),
            ("Round pin", URL(string: "https://static.arcgis.com/images/Symbols/Shapes/BluePin1LargeB.png")!),
            ("Square pin", URL(string: "https://static.arcgis.com/images/Symbols/Shapes/BluePin2LargeB.png")!)
        ]
        icons.forEach { (title, url) in
            let pointAction = UIAlertAction(title: title, style: .default) { (_) in
                self.kmlStyle = self.makeKMLStyleWithPointStyle(iconURL: url)
                self.startSketch(creationMode: .point)
            }
            alertController.addAction(pointAction)
        }
        // Add "cancel" item.
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
        alertController.addAction(cancelAction)

        alertController.popoverPresentationController?.barButtonItem = addButton
        present(alertController, animated: true)
    }

    // Prompt color selection action sheet for polyline feature.
    func addPolyline() {
        let alertController = UIAlertController(title: "Select Color", message: "This color will be used for the polyline", preferredStyle: .actionSheet)
        colors.forEach { (colorTitle, colorValue) in
            let colorAction = UIAlertAction(title: colorTitle, style: .default) { (_) in
                self.kmlStyle = self.makeKMLStyleWithLineStyle(color: colorValue)
                self.startSketch(creationMode: .polyline)
            }
            alertController.addAction(colorAction)
        }
        // Add "cancel" item.
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
        alertController.addAction(cancelAction)

        alertController.popoverPresentationController?.barButtonItem = addButton
        present(alertController, animated: true)
    }

    // Prompt color selection action sheet for polygon feature.
    func addPolygon() {
        let alertController = UIAlertController(title: "Select Color", message: "This color will be used to fill the polygon", preferredStyle: .actionSheet)
        colors.forEach { (colorTitle, colorValue) in
            let colorAction = UIAlertAction(title: colorTitle, style: .default) { (_) in
                self.kmlStyle = self.makeKMLStyleWithPolygonStyle(color: colorValue)
                self.startSketch(creationMode: .polygon)
            }
            alertController.addAction(colorAction)
        }
        // Add "cancel" item.
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
        alertController.addAction(cancelAction)

        alertController.popoverPresentationController?.barButtonItem = addButton
        present(alertController, animated: true)
    }

    // Make KML with a point style.
    func makeKMLStyleWithPointStyle(iconURL: URL) -> AGSKMLStyle {
        let icon = AGSKMLIcon(url: iconURL)
        let iconStyle = AGSKMLIconStyle(icon: icon, scale: 1.0)
        let kmlStyle = AGSKMLStyle()
        kmlStyle.iconStyle = iconStyle
        return kmlStyle
    }

    // Make KML with a line style.
    func makeKMLStyleWithLineStyle(color: UIColor) -> AGSKMLStyle {
        let kmlStyle = AGSKMLStyle()
        kmlStyle.lineStyle = AGSKMLLineStyle(color: color, width: 2.0)
        return kmlStyle
    }

    // Make KML with a polygon style.
    func makeKMLStyleWithPolygonStyle(color: UIColor) -> AGSKMLStyle {
        let polygonStyle = AGSKMLPolygonStyle(color: color)
        polygonStyle.isFilled = true
        polygonStyle.isOutlined = false
        let kmlStyle = AGSKMLStyle()
        kmlStyle.polygonStyle = polygonStyle
        return kmlStyle
    }

    // Update the bottom toolbar button.
    func updateToolbarItems() {
        guard let sketchEditor = mapView.sketchEditor else {
            return
        }
        let middleButtonItem: UIBarButtonItem
        if sketchEditor.isStarted {
            resetButtonItem.isEnabled = false
            middleButtonItem = sketchDoneButton
        } else {
            resetButtonItem.isEnabled = true
            middleButtonItem = addButton
        }
        let flexibleSpace1 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        let flexibleSpace2 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        toolbar.items = [resetButtonItem, flexibleSpace1, middleButtonItem, flexibleSpace2, actionButtonItem]
    }

    // Start a new sketch mode.
    func startSketch(creationMode: AGSSketchCreationMode) {
        mapView.sketchEditor?.start(with: creationMode)
        updateToolbarItems()
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        resetKML()
        updateToolbarItems()
        // Add the source code button item to the right of navigation bar.
        (navigationItem.rightBarButtonItem as? SourceCodeBarButtonItem)?.filenames = [
            "CreateAndSaveKMLViewController"
        ]
    }
}

// Handles saving a KMZ file.
private class KMZProvider: UIActivityItemProvider {
    private let document: AGSKMLDocument
    private var temporaryDirectoryURL: URL?

    init(document: AGSKMLDocument) {
        self.document = document
        if document.name.isEmpty {
            document.name = "Untitled"
        }
        super.init(placeholderItem: URL(fileURLWithPath: "\(document.name).kmz"))
    }

    override var item: Any {
        temporaryDirectoryURL = try? FileManager.default.url(
            for: .itemReplacementDirectory,
            in: .userDomainMask,
            appropriateFor: Bundle.main.bundleURL,
            create: true
        )
        let documentURL = temporaryDirectoryURL?.appendingPathComponent("\(document.name).kmz")
        let semaphore = DispatchSemaphore(value: 0)
        document.save(toFileURL: documentURL!) { _ in
            semaphore.signal()
        }
        semaphore.wait()
        return documentURL!
    }

    // Deletes the temporary directory.
    func deleteKMZ() {
        guard let url = temporaryDirectoryURL else { return }
        try? FileManager.default.removeItem(at: url)
    }
}

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