Skip to content

Commit f9781c2

Browse files
authored
Merge pull request #560 from Esri/Caleb/New-CreateKMLMultiTrackView
[New] Create KML multi-track
2 parents b7ba609 + a6690c8 commit f9781c2

File tree

6 files changed

+444
-0
lines changed

6 files changed

+444
-0
lines changed

Samples.xcodeproj/project.pbxproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@
250250
D713C6D72CB990600073AA72 /* AddKMLLayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D713C6D12CB990600073AA72 /* AddKMLLayerView.swift */; };
251251
D713C6D82CB990800073AA72 /* AddKMLLayerView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D713C6D12CB990600073AA72 /* AddKMLLayerView.swift */; };
252252
D713C6F72CB9B9A60073AA72 /* US_State_Capitals.kml in Resources */ = {isa = PBXBuildFile; fileRef = D713C6F52CB9B9A60073AA72 /* US_State_Capitals.kml */; settings = {ASSET_TAGS = (AddKmlLayer, ); }; };
253+
D71563E92D5AC2B600D2E948 /* CreateKMLMultiTrackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71563E32D5AC2B600D2E948 /* CreateKMLMultiTrackView.swift */; };
254+
D71563EA2D5AC2D500D2E948 /* CreateKMLMultiTrackView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D71563E32D5AC2B600D2E948 /* CreateKMLMultiTrackView.swift */; };
253255
D718A1E72B570F7500447087 /* OrbitCameraAroundObjectView.Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D718A1E62B570F7500447087 /* OrbitCameraAroundObjectView.Model.swift */; };
254256
D718A1E82B571C9100447087 /* OrbitCameraAroundObjectView.Model.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D718A1E62B570F7500447087 /* OrbitCameraAroundObjectView.Model.swift */; };
255257
D718A1ED2B575FD900447087 /* ManageBookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D718A1EA2B575FD900447087 /* ManageBookmarksView.swift */; };
@@ -416,6 +418,8 @@
416418
D7848F012CBD987B00F6F546 /* AddElevationSourceFromRasterView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D7848EFA2CBD986400F6F546 /* AddElevationSourceFromRasterView.swift */; };
417419
D78666AD2A2161F100C60110 /* FindNearestVertexView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78666AC2A2161F100C60110 /* FindNearestVertexView.swift */; };
418420
D78666AE2A21629200C60110 /* FindNearestVertexView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D78666AC2A2161F100C60110 /* FindNearestVertexView.swift */; };
421+
D789AAAD2D66C718007A8E0E /* CreateKMLMultiTrackView.Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D789AAAC2D66C718007A8E0E /* CreateKMLMultiTrackView.Model.swift */; };
422+
D789AAAE2D66C737007A8E0E /* CreateKMLMultiTrackView.Model.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D789AAAC2D66C718007A8E0E /* CreateKMLMultiTrackView.Model.swift */; };
419423
D78FA4942C3C88880079313E /* CreateDynamicBasemapGalleryView.Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78FA4932C3C88880079313E /* CreateDynamicBasemapGalleryView.Views.swift */; };
420424
D78FA4952C3C8E8A0079313E /* CreateDynamicBasemapGalleryView.Views.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D78FA4932C3C88880079313E /* CreateDynamicBasemapGalleryView.Views.swift */; };
421425
D79482D42C35D872006521CD /* CreateDynamicBasemapGalleryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79482D02C35D872006521CD /* CreateDynamicBasemapGalleryView.swift */; };
@@ -585,6 +589,8 @@
585589
dstPath = "";
586590
dstSubfolderSpec = 7;
587591
files = (
592+
D789AAAE2D66C737007A8E0E /* CreateKMLMultiTrackView.Model.swift in Copy Source Code Files */,
593+
D71563EA2D5AC2D500D2E948 /* CreateKMLMultiTrackView.swift in Copy Source Code Files */,
588594
D74F6C452D0CD54200D4FB15 /* ConfigureElectronicNavigationalChartsView.swift in Copy Source Code Files */,
589595
D7A85A092CD5AC0B009DC68A /* QueryWithCQLFiltersView.swift in Copy Source Code Files */,
590596
D771D0C92CD5522A004C13CB /* ApplyRasterRenderingRuleView.swift in Copy Source Code Files */,
@@ -953,6 +959,7 @@
953959
D71371752BD88ECC00EB2F86 /* MonitorChangesToLayerViewStateView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MonitorChangesToLayerViewStateView.swift; sourceTree = "<group>"; };
954960
D713C6D12CB990600073AA72 /* AddKMLLayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddKMLLayerView.swift; sourceTree = "<group>"; };
955961
D713C6F52CB9B9A60073AA72 /* US_State_Capitals.kml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = US_State_Capitals.kml; sourceTree = "<group>"; };
962+
D71563E32D5AC2B600D2E948 /* CreateKMLMultiTrackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateKMLMultiTrackView.swift; sourceTree = "<group>"; };
956963
D718A1E62B570F7500447087 /* OrbitCameraAroundObjectView.Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrbitCameraAroundObjectView.Model.swift; sourceTree = "<group>"; };
957964
D718A1EA2B575FD900447087 /* ManageBookmarksView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageBookmarksView.swift; sourceTree = "<group>"; };
958965
D71C5F632AAA7A88006599FD /* CreateSymbolStylesFromWebStylesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateSymbolStylesFromWebStylesView.swift; sourceTree = "<group>"; };
@@ -1045,6 +1052,7 @@
10451052
D7848ED42CBD85A300F6F546 /* AddPointSceneLayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPointSceneLayerView.swift; sourceTree = "<group>"; };
10461053
D7848EFA2CBD986400F6F546 /* AddElevationSourceFromRasterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddElevationSourceFromRasterView.swift; sourceTree = "<group>"; };
10471054
D78666AC2A2161F100C60110 /* FindNearestVertexView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindNearestVertexView.swift; sourceTree = "<group>"; };
1055+
D789AAAC2D66C718007A8E0E /* CreateKMLMultiTrackView.Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateKMLMultiTrackView.Model.swift; sourceTree = "<group>"; };
10481056
D78FA4932C3C88880079313E /* CreateDynamicBasemapGalleryView.Views.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateDynamicBasemapGalleryView.Views.swift; sourceTree = "<group>"; };
10491057
D79482D02C35D872006521CD /* CreateDynamicBasemapGalleryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateDynamicBasemapGalleryView.swift; sourceTree = "<group>"; };
10501058
D79EE76D2A4CEA5D005A52AE /* SetUpLocationDrivenGeotriggersView.Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetUpLocationDrivenGeotriggersView.Model.swift; sourceTree = "<group>"; };
@@ -1290,6 +1298,7 @@
12901298
D7B3C5C02A43B71E001DA4D8 /* Create convex hull around geometries */,
12911299
D744FD132A2112360084A66C /* Create convex hull around points */,
12921300
D79482D32C35D872006521CD /* Create dynamic basemap gallery */,
1301+
D71563E62D5AC2B600D2E948 /* Create KML multi-track */,
12931302
1C19B4EA2A578E46001D2506 /* Create load report */,
12941303
D73FABE82AD4A0370048EC70 /* Create mobile geodatabase */,
12951304
E004A6EB28495538002A1FE6 /* Create planar and geodetic buffers */,
@@ -2111,6 +2120,15 @@
21112120
path = 324e4742820e46cfbe5029ff2c32cb1f;
21122121
sourceTree = "<group>";
21132122
};
2123+
D71563E62D5AC2B600D2E948 /* Create KML multi-track */ = {
2124+
isa = PBXGroup;
2125+
children = (
2126+
D71563E32D5AC2B600D2E948 /* CreateKMLMultiTrackView.swift */,
2127+
D789AAAC2D66C718007A8E0E /* CreateKMLMultiTrackView.Model.swift */,
2128+
);
2129+
path = "Create KML multi-track";
2130+
sourceTree = "<group>";
2131+
};
21142132
D718A1E92B575FD900447087 /* Manage bookmarks */ = {
21152133
isa = PBXGroup;
21162134
children = (
@@ -3508,6 +3526,7 @@
35083526
00CCB8A5285BAF8700BBAB70 /* OnDemandResource.swift in Sources */,
35093527
D7635FFE2B9277DC0044AB97 /* ConfigureClustersView.swift in Sources */,
35103528
1C19B4F32A578E46001D2506 /* CreateLoadReportView.swift in Sources */,
3529+
D71563E92D5AC2B600D2E948 /* CreateKMLMultiTrackView.swift in Sources */,
35113530
7900C5F62A83FC3F002D430F /* AddCustomDynamicEntityDataSourceView.Vessel.swift in Sources */,
35123531
D71099702A2802FA0065A1C1 /* DensifyAndGeneralizeGeometryView.SettingsView.swift in Sources */,
35133532
D7201D042CC6D3B5004BDB7D /* AddVectorTiledLayerFromCustomStyleView.swift in Sources */,
@@ -3547,6 +3566,7 @@
35473566
D7497F3C2AC4B4C100167AD2 /* DisplayDimensionsView.swift in Sources */,
35483567
D7C97B562B75C10C0097CDA1 /* ValidateUtilityNetworkTopologyView.Views.swift in Sources */,
35493568
D73FCFF72B02A3AA0006360D /* FindAddressWithReverseGeocodeView.swift in Sources */,
3569+
D789AAAD2D66C718007A8E0E /* CreateKMLMultiTrackView.Model.swift in Sources */,
35503570
0005580A2817C51E00224BC6 /* SampleDetailView.swift in Sources */,
35513571
D75F66362B48EABC00434974 /* SearchForWebMapView.swift in Sources */,
35523572
D7058B102B59E44B000A888A /* StylePointWithSceneSymbolView.swift in Sources */,

Shared/Samples/Create KML multi-track/CreateKMLMultiTrackView.Model.swift

Lines changed: 158 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Copyright 2025 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 CreateKMLMultiTrackView: View {
19+
/// The view model for the sample.
20+
@StateObject private var model = Model()
21+
22+
/// The KML multi-track loaded from the KMZ file.
23+
@State private var multiTrack: KMLMultiTrack?
24+
25+
/// A Boolean value indicating whether the recenter button is enabled.
26+
@State private var isRecenterEnabled = false
27+
28+
/// The error shown in the error alert.
29+
@State private var error: Error?
30+
31+
/// Represents the various states of the sample.
32+
private enum SampleState {
33+
/// The sample is navigating without recording.
34+
case navigating
35+
/// A KML track is being recorded.
36+
case recording
37+
/// The KML multi-track is being saved, loaded, and viewed.
38+
case viewingMultiTrack
39+
/// The sample is being reset.
40+
case reseting
41+
}
42+
43+
/// The current state of the sample.
44+
@State private var state = SampleState.navigating
45+
46+
/// The text shown in the status bar. This describes the current state of the sample.
47+
private var statusText: String {
48+
return switch state {
49+
case .recording: "Recording KML track. Elements added: \(model.trackElements.count)"
50+
case .viewingMultiTrack: "Saved KML multi-track to 'HikingTracks.kmz'."
51+
default: "Tap record to capture KML track elements."
52+
}
53+
}
54+
55+
var body: some View {
56+
MapViewReader { mapViewProxy in
57+
MapView(map: model.map, graphicsOverlays: model.graphicsOverlays)
58+
.locationDisplay(model.locationDisplay)
59+
.overlay(alignment: .top) {
60+
Text(statusText)
61+
.multilineTextAlignment(.center)
62+
.frame(maxWidth: .infinity, alignment: .center)
63+
.padding(8)
64+
.background(.regularMaterial, ignoresSafeAreaEdges: .horizontal)
65+
}
66+
.toolbar {
67+
ToolbarItemGroup(placement: .bottomBar) {
68+
if let multiTrack {
69+
Button("Delete", systemImage: "trash", role: .destructive) {
70+
state = .reseting
71+
}
72+
73+
Spacer()
74+
75+
TrackPicker(tracks: multiTrack.tracks) { geometry in
76+
await mapViewProxy.setViewpointGeometry(geometry.extent, padding: 25)
77+
}
78+
} else {
79+
Button("Recenter", systemImage: "location.north.circle") {
80+
model.locationDisplay.autoPanMode = .navigation
81+
}
82+
.disabled(!isRecenterEnabled)
83+
84+
Spacer()
85+
86+
Toggle(
87+
state == .recording ? "Stop Recording" : "Record Track",
88+
isOn: .init {
89+
state == .recording
90+
} set: { newValue in
91+
state = newValue ? .recording : .navigating
92+
}
93+
)
94+
95+
Spacer()
96+
97+
Button("Save", systemImage: "square.and.arrow.down") {
98+
state = .viewingMultiTrack
99+
}
100+
.disabled(model.tracks.isEmpty)
101+
}
102+
}
103+
}
104+
.task(id: state) {
105+
// Runs the asynchronous action associated with the sample state.
106+
do {
107+
switch state {
108+
case .navigating:
109+
break
110+
case .recording:
111+
for await location in model.locationDisplay.$location where location != nil {
112+
model.addTrackElement(at: location!.position)
113+
}
114+
115+
model.addTrack()
116+
case .viewingMultiTrack:
117+
await model.locationDisplay.dataSource.stop()
118+
119+
try await model.saveKMLMultiTrack()
120+
multiTrack = try await model.loadKMLMultiTrack()
121+
case .reseting:
122+
model.reset()
123+
multiTrack = nil
124+
125+
await mapViewProxy.setViewpointScale(model.locationDisplay.initialZoomScale)
126+
try await model.startNavigation()
127+
}
128+
} catch {
129+
self.error = error
130+
}
131+
}
132+
.task {
133+
// Starts the navigation when the sample opens.
134+
do {
135+
try await model.startNavigation()
136+
} catch {
137+
self.error = error
138+
}
139+
140+
// Monitors the auto pan mode to determine if recenter button should be enabled.
141+
for await autoPanMode in model.locationDisplay.$autoPanMode {
142+
isRecenterEnabled = autoPanMode != .navigation
143+
}
144+
}
145+
.errorAlert(presentingError: $error)
146+
}
147+
}
148+
}
149+
150+
private extension CreateKMLMultiTrackView {
151+
/// A picker for selecting a track from a KML multi-track.
152+
struct TrackPicker: View {
153+
/// The KML tracks options shown in the picker.
154+
let tracks: [KMLTrack]
155+
156+
/// The closure to perform when the selected track has changed.
157+
let onSelectionChanged: (Geometry) async -> Void
158+
159+
/// The track selected by the picker.
160+
@State private var selectedTrack: KMLTrack?
161+
162+
var body: some View {
163+
Picker("Track", selection: $selectedTrack) {
164+
Text("All Tracks")
165+
.tag(nil as KMLTrack?)
166+
167+
ForEach(Array(tracks.enumerated()), id: \.offset) { offset, track in
168+
Text("KML Track #\(offset + 1)")
169+
.tag(track)
170+
}
171+
}
172+
.task(id: selectedTrack) {
173+
guard let geometry = selectedTrack?.geometry
174+
?? GeometryEngine.union(of: tracks.map(\.geometry)) else {
175+
return
176+
}
177+
178+
await onSelectionChanged(geometry)
179+
}
180+
}
181+
}
182+
}
183+
184+
#Preview {
185+
CreateKMLMultiTrackView()
186+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Create KML multi-track
2+
3+
Create, save, and preview a KML multi-track captured from a location data source.
4+
5+
![Create KML multi-track sample](create-kml-multi-track.png)
6+
7+
## Use case
8+
9+
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.
10+
11+
## How to use the sample
12+
13+
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.
14+
15+
## How it works
16+
17+
1. Create a `Map` with a basemap style and a `GraphicsOverlay` to display the path geometry for your navigation route.
18+
2. Create a `SimulatedLocationDataSource` to drive the `LocationDisplay`.
19+
3. As you receive `Location` updates, add each point to a list of `KMLTrackElement` objects while recording.
20+
4. Once recording stops, create a `KMLTrack` using one or more `KMLTrackElement` objects.
21+
5. Combine one or more `KMLTrack` objects into a `KMLMultiTrack`.
22+
6. Save the `KMLMultiTrack` inside a `KMLDocument`, then export the document to a `.kmz` file.
23+
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.
24+
8. Retrieve the geometry of each track in the `KMLMultiTrack` by iterating through the list of tracks and obtaining the respective `KMLTrack.geometry`.
25+
26+
## Relevant API
27+
28+
* KMLDataset
29+
* KMLDocument
30+
* KMLMultiTrack
31+
* KMLPlacemark
32+
* KMLTrack
33+
* KMLTrackElement
34+
* LocationDisplay
35+
* SimulatedLocationDataSource
36+
37+
## Tags
38+
39+
export, hiking, KML, KMZ, multi-track, record, track
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"category": "Edit and Manage Data",
3+
"description": "Create, save, and preview a KML multi-track captured from a location data source.",
4+
"ignore": false,
5+
"images": [
6+
"create-kml-multi-track.png"
7+
],
8+
"keywords": [
9+
"KML",
10+
"KMZ",
11+
"export",
12+
"hiking",
13+
"multi-track",
14+
"record",
15+
"track",
16+
"KMLDataset",
17+
"KMLDocument",
18+
"KMLMultiTrack",
19+
"KMLPlacemark",
20+
"KMLTrack",
21+
"KMLTrackElement",
22+
"LocationDisplay",
23+
"SimulatedLocationDataSource"
24+
],
25+
"redirect_from": [],
26+
"relevant_apis": [
27+
"KMLDataset",
28+
"KMLDocument",
29+
"KMLMultiTrack",
30+
"KMLPlacemark",
31+
"KMLTrack",
32+
"KMLTrackElement",
33+
"LocationDisplay",
34+
"SimulatedLocationDataSource"
35+
],
36+
"snippets": [
37+
"CreateKMLMultiTrackView.swift",
38+
"CreateKMLMultiTrackView.Model.swift"
39+
],
40+
"title": "Create KML multi-track"
41+
}
114 KB
Loading

0 commit comments

Comments
 (0)