Skip to content

Commit 5463184

Browse files
authored
Merge pull request #482 from Esri/destiny/Refactor-augment-reality-to-navigate-route
[Fix] Refactor `Augment reality to navigate route`
2 parents 498b5b7 + e820436 commit 5463184

File tree

5 files changed

+478
-431
lines changed

5 files changed

+478
-431
lines changed

Samples.xcodeproj/project.pbxproj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@
106106
1C19B4F72A578E69001D2506 /* CreateLoadReportView.Model.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C19B4EF2A578E46001D2506 /* CreateLoadReportView.Model.swift */; };
107107
1C19B4F82A578E69001D2506 /* CreateLoadReportView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C19B4ED2A578E46001D2506 /* CreateLoadReportView.swift */; };
108108
1C19B4F92A578E69001D2506 /* CreateLoadReportView.Views.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C19B4EB2A578E46001D2506 /* CreateLoadReportView.Views.swift */; };
109-
1C2538542BABACB100337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C2538522BABACB100337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift */; };
109+
1C2538542BABACB100337307 /* AugmentRealityToNavigateRouteView.ARSceneView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C2538522BABACB100337307 /* AugmentRealityToNavigateRouteView.ARSceneView.swift */; };
110110
1C2538552BABACB100337307 /* AugmentRealityToNavigateRouteView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C2538532BABACB100337307 /* AugmentRealityToNavigateRouteView.swift */; };
111111
1C2538562BABACFD00337307 /* AugmentRealityToNavigateRouteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C2538532BABACB100337307 /* AugmentRealityToNavigateRouteView.swift */; };
112-
1C2538572BABACFD00337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C2538522BABACB100337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift */; };
112+
1C2538572BABACFD00337307 /* AugmentRealityToNavigateRouteView.ARSceneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C2538522BABACB100337307 /* AugmentRealityToNavigateRouteView.ARSceneView.swift */; };
113113
1C26ED192A859525009B7721 /* FilterFeaturesInSceneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C26ED152A859525009B7721 /* FilterFeaturesInSceneView.swift */; };
114114
1C26ED202A8BEC63009B7721 /* FilterFeaturesInSceneView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C26ED152A859525009B7721 /* FilterFeaturesInSceneView.swift */; };
115115
1C3B7DC82A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3B7DC32A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.Model.swift */; };
@@ -569,7 +569,7 @@
569569
0000FB712BBDC01400845921 /* Add3DTilesLayerView.swift in Copy Source Code Files */,
570570
D77D9C012BB2439400B38A6C /* AugmentRealityToShowHiddenInfrastructureView.ARSceneView.swift in Copy Source Code Files */,
571571
D7A737E32BABBA2200B7C7FC /* AugmentRealityToShowHiddenInfrastructureView.swift in Copy Source Code Files */,
572-
1C2538542BABACB100337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift in Copy Source Code Files */,
572+
1C2538542BABACB100337307 /* AugmentRealityToNavigateRouteView.ARSceneView.swift in Copy Source Code Files */,
573573
1C2538552BABACB100337307 /* AugmentRealityToNavigateRouteView.swift in Copy Source Code Files */,
574574
1C8EC74B2BAE28A9001A6929 /* AugmentRealityToCollectDataView.swift in Copy Source Code Files */,
575575
000D43182B993A030003D3C2 /* ConfigureBasemapStyleParametersView.swift in Copy Source Code Files */,
@@ -795,7 +795,7 @@
795795
1C19B4EB2A578E46001D2506 /* CreateLoadReportView.Views.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateLoadReportView.Views.swift; sourceTree = "<group>"; };
796796
1C19B4ED2A578E46001D2506 /* CreateLoadReportView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateLoadReportView.swift; sourceTree = "<group>"; };
797797
1C19B4EF2A578E46001D2506 /* CreateLoadReportView.Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateLoadReportView.Model.swift; sourceTree = "<group>"; };
798-
1C2538522BABACB100337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AugmentRealityToNavigateRouteView.RoutePlannerView.swift; sourceTree = "<group>"; };
798+
1C2538522BABACB100337307 /* AugmentRealityToNavigateRouteView.ARSceneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AugmentRealityToNavigateRouteView.ARSceneView.swift; sourceTree = "<group>"; };
799799
1C2538532BABACB100337307 /* AugmentRealityToNavigateRouteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AugmentRealityToNavigateRouteView.swift; sourceTree = "<group>"; };
800800
1C26ED152A859525009B7721 /* FilterFeaturesInSceneView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterFeaturesInSceneView.swift; sourceTree = "<group>"; };
801801
1C3B7DC32A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyzeNetworkWithSubnetworkTraceView.Model.swift; sourceTree = "<group>"; };
@@ -1537,7 +1537,7 @@
15371537
isa = PBXGroup;
15381538
children = (
15391539
1C2538532BABACB100337307 /* AugmentRealityToNavigateRouteView.swift */,
1540-
1C2538522BABACB100337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift */,
1540+
1C2538522BABACB100337307 /* AugmentRealityToNavigateRouteView.ARSceneView.swift */,
15411541
);
15421542
path = "Augment reality to navigate route";
15431543
sourceTree = "<group>";
@@ -3027,7 +3027,7 @@
30273027
buildActionMask = 2147483647;
30283028
files = (
30293029
1C2538562BABACFD00337307 /* AugmentRealityToNavigateRouteView.swift in Sources */,
3030-
1C2538572BABACFD00337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift in Sources */,
3030+
1C2538572BABACFD00337307 /* AugmentRealityToNavigateRouteView.ARSceneView.swift in Sources */,
30313031
D76929FA2B4F79540047205E /* OrbitCameraAroundObjectView.swift in Sources */,
30323032
79D84D132A81711A00F45262 /* AddCustomDynamicEntityDataSourceView.swift in Sources */,
30333033
102B6A372BFD5B55009F763C /* IdentifyFeaturesInWMSLayerView.swift in Sources */,
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
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 ArcGISToolkit
17+
import AVFoundation
18+
import SwiftUI
19+
20+
extension AugmentRealityToNavigateRouteView {
21+
/// A world scale scene view displaying route graphics from a given model.
22+
struct ARRouteSceneView: View {
23+
/// The view model for scene view in the sample.
24+
@ObservedObject var model: SceneModel
25+
26+
/// A Boolean value indicating whether the use is navigating the route.
27+
@State private var isNavigating = false
28+
29+
/// The error shown in the error alert.
30+
@State private var error: Error?
31+
32+
var body: some View {
33+
VStack(spacing: 0) {
34+
WorldScaleSceneView { _ in
35+
SceneView(
36+
scene: model.scene,
37+
graphicsOverlays: [model.routeGraphicsOverlay]
38+
)
39+
}
40+
.calibrationButtonAlignment(.bottomLeading)
41+
.onCalibratingChanged { isPresented in
42+
model.scene.baseSurface.opacity = isPresented ? 0.6 : 0
43+
}
44+
.task {
45+
do {
46+
try await model.startTrackingLocation()
47+
} catch {
48+
self.error = error
49+
}
50+
}
51+
.overlay(alignment: .top) {
52+
Text(model.statusText)
53+
.multilineTextAlignment(.center)
54+
.frame(maxWidth: .infinity, alignment: .center)
55+
.padding(8)
56+
.background(.regularMaterial, ignoresSafeAreaEdges: .horizontal)
57+
}
58+
.onAppear {
59+
model.statusText = "Adjust calibration before starting."
60+
}
61+
.onDisappear {
62+
model.stopNavigation()
63+
}
64+
Divider()
65+
}
66+
.toolbar {
67+
ToolbarItemGroup(placement: .bottomBar) {
68+
Button("Start") {
69+
isNavigating = true
70+
}
71+
.disabled(isNavigating)
72+
.task(id: isNavigating) {
73+
guard isNavigating else { return }
74+
do {
75+
try await model.startNavigation()
76+
} catch {
77+
self.error = error
78+
}
79+
}
80+
}
81+
}
82+
.errorAlert(presentingError: $error)
83+
}
84+
}
85+
}
86+
87+
extension AugmentRealityToNavigateRouteView {
88+
// MARK: Scene Model
89+
90+
/// The view model for scene view in the sample.
91+
@MainActor
92+
class SceneModel: ObservableObject {
93+
/// A scene with an imagery basemap.
94+
let scene = Scene(basemapStyle: .arcGISImagery)
95+
96+
/// The graphics overlay containing a graphic for the route.
97+
private(set) var routeGraphicsOverlay = GraphicsOverlay()
98+
99+
/// The elevation surface set to the base surface of the scene.
100+
private let elevationSurface: Surface = {
101+
let elevationSurface = Surface()
102+
elevationSurface.navigationConstraint = .unconstrained
103+
elevationSurface.opacity = 0
104+
elevationSurface.backgroundGrid.isVisible = false
105+
return elevationSurface
106+
}()
107+
108+
/// The elevation source with elevation service URL.
109+
private var elevationSource = ArcGISTiledElevationSource(url: .worldElevationService)
110+
111+
/// The route tracker.
112+
private(set) var routeTracker: RouteTracker?
113+
114+
/// The route result.
115+
var routeResult: RouteResult?
116+
117+
/// An AVSpeechSynthesizer for text to speech.
118+
private let speechSynthesizer = AVSpeechSynthesizer()
119+
120+
/// The route task that solves the route using the online routing service, using API key authentication.
121+
let routeTask = RouteTask(url: URL(string: "https://route-api.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World")!)
122+
123+
/// The parameters for route task to solve a route.
124+
var routeParameters = RouteParameters()
125+
126+
/// The data source to track device location and provide updates to route tracker.
127+
let locationDataSource = SystemLocationDataSource()
128+
129+
/// The status text displayed to the user.
130+
@Published var statusText = ""
131+
132+
init() {
133+
elevationSurface.addElevationSource(elevationSource)
134+
scene.baseSurface = elevationSurface
135+
}
136+
137+
/// Loads the scene elevation source.
138+
func loadElevationSource() async throws {
139+
await scene.baseSurface.elevationSources.load()
140+
}
141+
142+
/// Tracks the location datasource locations.
143+
func startTrackingLocation() async throws {
144+
for await location in locationDataSource.locations {
145+
try await routeTracker?.track(location)
146+
}
147+
}
148+
149+
/// Creates a graphics overlay and adds a graphic (with solid yellow 3D tube symbol)
150+
/// to represent the route.
151+
func makeRouteOverlay() async throws {
152+
let graphicsOverlay = GraphicsOverlay()
153+
graphicsOverlay.sceneProperties.surfacePlacement = .absolute
154+
let strokeSymbolLayer = SolidStrokeSymbolLayer(
155+
width: 1.0,
156+
color: .yellow,
157+
lineStyle3D: .tube
158+
)
159+
let polylineSymbol = MultilayerPolylineSymbol(symbolLayers: [strokeSymbolLayer])
160+
let polylineRenderer = SimpleRenderer(symbol: polylineSymbol)
161+
graphicsOverlay.renderer = polylineRenderer
162+
163+
if let routeResult,
164+
let originalPolyline = routeResult.routes.first?.geometry,
165+
let elevatedPolyline = try await addingElevation(3, to: originalPolyline) {
166+
let routeGraphic = Graphic(geometry: elevatedPolyline)
167+
graphicsOverlay.addGraphic(routeGraphic)
168+
}
169+
170+
routeGraphicsOverlay = graphicsOverlay
171+
}
172+
173+
/// Densifies the polyline geometry so the elevation can be adjusted every 0.3 meters and adds
174+
/// an elevation to the geometry.
175+
/// - Parameters:
176+
/// - z: The z elevation.
177+
/// - polyline: The polyline geometry of the route.
178+
/// - Returns: A polyline with adjusted elevation.
179+
private func addingElevation(_ z: Double, to polyline: Polyline) async throws -> Polyline? {
180+
guard let densifiedPolyline = GeometryEngine.densify(polyline, maxSegmentLength: 0.3) as? Polyline else { return nil }
181+
let polylineBuilder = PolylineBuilder(spatialReference: densifiedPolyline.spatialReference)
182+
for part in densifiedPolyline.parts {
183+
for point in part.points {
184+
let elevation = try await elevationSurface.elevation(at: point)
185+
let newPoint = GeometryEngine.makeGeometry(from: point, z: elevation + z)
186+
// Put the new point 3 meters above the ground elevation.
187+
polylineBuilder.add(newPoint)
188+
}
189+
}
190+
return polylineBuilder.toGeometry()
191+
}
192+
193+
/// Starts navigating the route.
194+
func startNavigation() async throws {
195+
guard let routeResult else { return }
196+
routeTracker = RouteTracker(
197+
routeResult: routeResult,
198+
routeIndex: 0,
199+
skipsCoincidentStops: true
200+
)
201+
guard let routeTracker else { return }
202+
203+
routeTracker.voiceGuidanceUnitSystem = Locale.current.measurementSystem == .us
204+
? .imperial
205+
: .metric
206+
207+
try await routeTask.load()
208+
209+
if routeTask.info.supportsRerouting,
210+
let reroutingParameters = ReroutingParameters(
211+
routeTask: routeTask,
212+
routeParameters: routeParameters
213+
) {
214+
try await routeTracker.enableRerouting(using: reroutingParameters)
215+
}
216+
217+
statusText = "Navigation will start."
218+
await startTracking()
219+
}
220+
221+
/// Stops navigating the route.
222+
func stopNavigation() {
223+
speechSynthesizer.stopSpeaking(at: .word)
224+
routeTracker = nil
225+
}
226+
227+
/// Starts monitoring multiple asynchronous streams of information.
228+
private func startTracking() async {
229+
await withTaskGroup(of: Void.self) { group in
230+
group.addTask { await self.trackStatus() }
231+
group.addTask { await self.trackVoiceGuidance() }
232+
}
233+
}
234+
235+
/// Monitors the asynchronous stream of voice guidances.
236+
private func trackVoiceGuidance() async {
237+
guard let routeTracker else { return }
238+
for try await voiceGuidance in routeTracker.voiceGuidances {
239+
speechSynthesizer.stopSpeaking(at: .word)
240+
speechSynthesizer.speak(AVSpeechUtterance(string: voiceGuidance.text))
241+
}
242+
}
243+
244+
/// Monitors the asynchronous stream of tracking statuses.
245+
///
246+
/// Updates the route's traversed and remaining graphics when new statuses are delivered.
247+
private func trackStatus() async {
248+
guard let routeTracker else { return }
249+
for await status in routeTracker.$trackingStatus {
250+
guard let status else { continue }
251+
switch status.destinationStatus {
252+
case .notReached, .approaching:
253+
if let route = routeResult?.routes.first {
254+
let currentManeuver = route.directionManeuvers[status.currentManeuverIndex]
255+
statusText = currentManeuver.text
256+
}
257+
case .reached:
258+
statusText = "You have arrived!"
259+
@unknown default:
260+
break
261+
}
262+
}
263+
}
264+
}
265+
}
266+
267+
private extension URL {
268+
/// The URL of the Terrain 3D ArcGIS REST Service.
269+
static var worldElevationService: URL {
270+
URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!
271+
}
272+
}

0 commit comments

Comments
 (0)