Create KML multi-track

View on GitHub

Create, save, and preview a KML multi-track captured from a location data source.

Create KML multi-track sample

Use case

When capturing location data for outdoor activities such as hiking or skiing, it can be useful to record and share your path. This sample demonstrates how you can collect individual KML tracks during a navigation session, then combine and export them as a KML multi-track.

How to use the sample

Tap "Record Track" to start recording your current path on the simulated trail. Tap "Stop Recording" to end recording and capture a KML track. Repeat these steps to capture multiple KML tracks in a single session. Tap the save button to convert the recorded tracks into a KML multi-track and save it to a local .kmz file. Then, use the picker to select a track from the saved KML multi-track. Tap the Delete button to remove the local file and reset the sample.

How it works

  1. Create a Map with a basemap style and a GraphicsOverlay to display the path geometry for your navigation route.
  2. Create a SimulatedLocationDataSource to drive the LocationDisplay.
  3. As you receive Location updates, add each point to a list of KMLTrackElement objects while recording.
  4. Once recording stops, create a KMLTrack using one or more KMLTrackElement objects.
  5. Combine one or more KMLTrack objects into a KMLMultiTrack.
  6. Save the KMLMultiTrack inside a KMLDocument, then export the document to a .kmz file.
  7. Load the saved .kmz file into a KMLDataset and locate the KMLDocument in the dataset's rootNodes. From the document's childNodes, get the KMLPlacemark and retrieve the KMLMultiTrack geometry.
  8. Retrieve the geometry of each track in the KMLMultiTrack by iterating through the list of tracks and obtaining the respective KMLTrack.geometry.

Relevant API

  • KMLDataset
  • KMLDocument
  • KMLMultiTrack
  • KMLPlacemark
  • KMLTrack
  • KMLTrackElement
  • LocationDisplay
  • SimulatedLocationDataSource

Tags

export, hiking, KML, KMZ, multi-track, record, track

Sample Code

CreateKMLMultiTrackView.swiftCreateKMLMultiTrackView.swiftCreateKMLMultiTrackView.Model.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
// 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
//
//   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 CreateKMLMultiTrackView: View {
    /// The view model for the sample.
    @StateObject private var model = Model()

    /// The KML multi-track loaded from the KMZ file.
    @State private var multiTrack: KMLMultiTrack?

    /// A Boolean value indicating whether the recenter button is enabled.
    @State private var isRecenterEnabled = false

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

    /// Represents the various states of the sample.
    private enum SampleState {
        /// The sample is navigating without recording.
        case navigating
        /// A KML track is being recorded.
        case recording
        /// The KML multi-track is being saved, loaded, and viewed.
        case viewingMultiTrack
        /// The sample is being reset.
        case reseting
    }

    /// The current state of the sample.
    @State private var state = SampleState.navigating

    /// The text shown in the status bar. This describes the current state of the sample.
    private var statusText: String {
        return switch state {
        case .recording: "Recording KML track. Elements added: \(model.trackElements.count)"
        case .viewingMultiTrack: "Saved KML multi-track to 'HikingTracks.kmz'."
        default: "Tap record to capture KML track elements."
        }
    }

    var body: some View {
        MapViewReader { mapViewProxy in
            MapView(map: model.map, graphicsOverlays: model.graphicsOverlays)
                .locationDisplay(model.locationDisplay)
                .overlay(alignment: .top) {
                    Text(statusText)
                        .multilineTextAlignment(.center)
                        .frame(maxWidth: .infinity, alignment: .center)
                        .padding(8)
                        .background(.regularMaterial, ignoresSafeAreaEdges: .horizontal)
                }
                .toolbar {
                    ToolbarItemGroup(placement: .bottomBar) {
                        if let multiTrack {
                            Button("Delete", systemImage: "trash", role: .destructive) {
                                state = .reseting
                            }

                            Spacer()

                            TrackPicker(tracks: multiTrack.tracks) { geometry in
                                await mapViewProxy.setViewpointGeometry(geometry.extent, padding: 25)
                            }
                        } else {
                            Button("Recenter", systemImage: "location.north.circle") {
                                model.locationDisplay.autoPanMode = .navigation
                            }
                            .disabled(!isRecenterEnabled)

                            Spacer()

                            Toggle(
                                state == .recording ? "Stop Recording" : "Record Track",
                                isOn: .init {
                                    state == .recording
                                } set: { newValue in
                                    state = newValue ? .recording : .navigating
                                }
                            )

                            Spacer()

                            Button("Save", systemImage: "square.and.arrow.down") {
                                state = .viewingMultiTrack
                            }
                            .disabled(model.tracks.isEmpty)
                        }
                    }
                }
                .task(id: state) {
                    // Runs the asynchronous action associated with the sample state.
                    do {
                        switch state {
                        case .navigating:
                            break
                        case .recording:
                            for await location in model.locationDisplay.$location where location != nil {
                                model.addTrackElement(at: location!.position)
                            }

                            model.addTrack()
                        case .viewingMultiTrack:
                            await model.locationDisplay.dataSource.stop()

                            try await model.saveKMLMultiTrack()
                            multiTrack = try await model.loadKMLMultiTrack()
                        case .reseting:
                            model.reset()
                            multiTrack = nil

                            await mapViewProxy.setViewpointScale(model.locationDisplay.initialZoomScale)
                            try await model.startNavigation()
                        }
                    } catch {
                        self.error = error
                    }
                }
                .task {
                    // Starts the navigation when the sample opens.
                    do {
                        try await model.startNavigation()
                    } catch {
                        self.error = error
                    }

                    // Monitors the auto pan mode to determine if recenter button should be enabled.
                    for await autoPanMode in model.locationDisplay.$autoPanMode {
                        isRecenterEnabled = autoPanMode != .navigation
                    }
                }
                .errorAlert(presentingError: $error)
        }
    }
}

private extension CreateKMLMultiTrackView {
    /// A picker for selecting a track from a KML multi-track.
    struct TrackPicker: View {
        /// The KML tracks options shown in the picker.
        let tracks: [KMLTrack]

        /// The closure to perform when the selected track has changed.
        let onSelectionChanged: (Geometry) async -> Void

        /// The track selected by the picker.
        @State private var selectedTrack: KMLTrack?

        var body: some View {
            Picker("Track", selection: $selectedTrack) {
                Text("All Tracks")
                    .tag(nil as KMLTrack?)

                ForEach(Array(tracks.enumerated()), id: \.offset) { offset, track in
                    Text("KML Track #\(offset + 1)")
                        .tag(track)
                }
            }
            .task(id: selectedTrack) {
                guard let geometry = selectedTrack?.geometry
                        ?? GeometryEngine.union(of: tracks.map(\.geometry)) else {
                    return
                }

                await onSelectionChanged(geometry)
            }
        }
    }
}

#Preview {
    CreateKMLMultiTrackView()
}

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