Skip to content

Commit 2172506

Browse files
authored
Merge pull request #353 from Esri/destiny/Navigate-in-AR-sample
[New] Augment reality to navigate route
2 parents 4fb115c + e6de766 commit 2172506

File tree

14 files changed

+646
-0
lines changed

14 files changed

+646
-0
lines changed

Samples.xcodeproj/project.pbxproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@
8080
1C19B4F72A578E69001D2506 /* CreateLoadReportView.Model.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C19B4EF2A578E46001D2506 /* CreateLoadReportView.Model.swift */; };
8181
1C19B4F82A578E69001D2506 /* CreateLoadReportView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C19B4ED2A578E46001D2506 /* CreateLoadReportView.swift */; };
8282
1C19B4F92A578E69001D2506 /* CreateLoadReportView.Views.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C19B4EB2A578E46001D2506 /* CreateLoadReportView.Views.swift */; };
83+
1C2538542BABACB100337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C2538522BABACB100337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift */; };
84+
1C2538552BABACB100337307 /* AugmentRealityToNavigateRouteView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C2538532BABACB100337307 /* AugmentRealityToNavigateRouteView.swift */; };
85+
1C2538562BABACFD00337307 /* AugmentRealityToNavigateRouteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C2538532BABACB100337307 /* AugmentRealityToNavigateRouteView.swift */; };
86+
1C2538572BABACFD00337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C2538522BABACB100337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift */; };
8387
1C26ED192A859525009B7721 /* FilterFeaturesInSceneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C26ED152A859525009B7721 /* FilterFeaturesInSceneView.swift */; };
8488
1C26ED202A8BEC63009B7721 /* FilterFeaturesInSceneView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 1C26ED152A859525009B7721 /* FilterFeaturesInSceneView.swift */; };
8589
1C3B7DC82A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3B7DC32A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.Model.swift */; };
@@ -433,6 +437,8 @@
433437
dstPath = "";
434438
dstSubfolderSpec = 7;
435439
files = (
440+
1C2538542BABACB100337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift in Copy Source Code Files */,
441+
1C2538552BABACB100337307 /* AugmentRealityToNavigateRouteView.swift in Copy Source Code Files */,
436442
000D43182B993A030003D3C2 /* ConfigureBasemapStyleParametersView.swift in Copy Source Code Files */,
437443
D76360032B9296580044AB97 /* DisplayClustersView.swift in Copy Source Code Files */,
438444
D76360022B9296520044AB97 /* ConfigureClustersView.SettingsView.swift in Copy Source Code Files */,
@@ -643,6 +649,8 @@
643649
1C19B4EB2A578E46001D2506 /* CreateLoadReportView.Views.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateLoadReportView.Views.swift; sourceTree = "<group>"; };
644650
1C19B4ED2A578E46001D2506 /* CreateLoadReportView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateLoadReportView.swift; sourceTree = "<group>"; };
645651
1C19B4EF2A578E46001D2506 /* CreateLoadReportView.Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateLoadReportView.Model.swift; sourceTree = "<group>"; };
652+
1C2538522BABACB100337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AugmentRealityToNavigateRouteView.RoutePlannerView.swift; sourceTree = "<group>"; };
653+
1C2538532BABACB100337307 /* AugmentRealityToNavigateRouteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AugmentRealityToNavigateRouteView.swift; sourceTree = "<group>"; };
646654
1C26ED152A859525009B7721 /* FilterFeaturesInSceneView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterFeaturesInSceneView.swift; sourceTree = "<group>"; };
647655
1C3B7DC32A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyzeNetworkWithSubnetworkTraceView.Model.swift; sourceTree = "<group>"; };
648656
1C3B7DC62A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyzeNetworkWithSubnetworkTraceView.swift; sourceTree = "<group>"; };
@@ -927,6 +935,7 @@
927935
D77570BC2A29427200F490CD /* Animate images with image overlay */,
928936
D75362CC2A1E862B00D83028 /* Apply unique value renderer */,
929937
D7084FA42AD771AA00EC7F4F /* Augment reality to fly over scene */,
938+
1C2538472BABAC7B00337307 /* Augment reality to navigate route */,
930939
D72F27292ADA1E4400F906DA /* Augment reality to show tabletop scene */,
931940
218F35B229C28F4A00502022 /* Authenticate with OAuth */,
932941
E0FE32E528747762002C6ACA /* Browse building floors */,
@@ -1233,6 +1242,15 @@
12331242
path = "Create load report";
12341243
sourceTree = "<group>";
12351244
};
1245+
1C2538472BABAC7B00337307 /* Augment reality to navigate route */ = {
1246+
isa = PBXGroup;
1247+
children = (
1248+
1C2538532BABACB100337307 /* AugmentRealityToNavigateRouteView.swift */,
1249+
1C2538522BABACB100337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift */,
1250+
);
1251+
path = "Augment reality to navigate route";
1252+
sourceTree = "<group>";
1253+
};
12361254
1C26ED122A859525009B7721 /* Filter features in scene */ = {
12371255
isa = PBXGroup;
12381256
children = (
@@ -2482,6 +2500,8 @@
24822500
isa = PBXSourcesBuildPhase;
24832501
buildActionMask = 2147483647;
24842502
files = (
2503+
1C2538562BABACFD00337307 /* AugmentRealityToNavigateRouteView.swift in Sources */,
2504+
1C2538572BABACFD00337307 /* AugmentRealityToNavigateRouteView.RoutePlannerView.swift in Sources */,
24852505
D76929FA2B4F79540047205E /* OrbitCameraAroundObjectView.swift in Sources */,
24862506
79D84D132A81711A00F45262 /* AddCustomDynamicEntityDataSourceView.swift in Sources */,
24872507
E000E7602869E33D005D87C5 /* ClipGeometryView.swift in Sources */,
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "stopA36.png",
5+
"idiom" : "universal",
6+
"scale" : "1x"
7+
},
8+
{
9+
"filename" : "stopA72.png",
10+
"idiom" : "universal",
11+
"scale" : "2x"
12+
},
13+
{
14+
"filename" : "stopA108.png",
15+
"idiom" : "universal",
16+
"scale" : "3x"
17+
}
18+
],
19+
"info" : {
20+
"author" : "xcode",
21+
"version" : 1
22+
}
23+
}
5.33 KB
Loading
2.12 KB
Loading
3.48 KB
Loading
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "StopB36.png",
5+
"idiom" : "universal",
6+
"scale" : "1x"
7+
},
8+
{
9+
"filename" : "StopB72.png",
10+
"idiom" : "universal",
11+
"scale" : "2x"
12+
},
13+
{
14+
"filename" : "StopB108.png",
15+
"idiom" : "universal",
16+
"scale" : "3x"
17+
}
18+
],
19+
"info" : {
20+
"author" : "xcode",
21+
"version" : 1
22+
}
23+
}
5.17 KB
Loading
2.08 KB
Loading
3.46 KB
Loading
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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 CoreLocation
18+
import SwiftUI
19+
20+
extension AugmentRealityToNavigateRouteView {
21+
@MainActor
22+
struct RoutePlannerView: View {
23+
/// The view model for this sample.
24+
@StateObject private var model = Model()
25+
/// A Boolean value indicating whether the view is showing.
26+
@Binding var isShowing: Bool
27+
/// The status text displayed to the user.
28+
@State private var statusText = ""
29+
/// User defined action to be performed when the slider delta value changes.
30+
var selectRouteAction: ((Graphic, RouteResult) -> Void)?
31+
/// A Boolean value indicating whether a route stop is selected.
32+
var didSelectRouteStop: Bool {
33+
model.startPoint != nil || model.endPoint != nil
34+
}
35+
/// A Boolean value indicating whether a route is selected.
36+
@State private var didSelectRoute = false
37+
/// The error shown in the error alert.
38+
@State private var error: Error?
39+
40+
var body: some View {
41+
MapView(
42+
map: model.map,
43+
graphicsOverlays: model.graphicsOverlays
44+
)
45+
.onSingleTapGesture { _, mapPoint in
46+
if model.startPoint == nil {
47+
model.startPoint = mapPoint
48+
statusText = "Tap to place destination."
49+
} else if model.endPoint == nil {
50+
model.endPoint = mapPoint
51+
model.routeDataModel.routeParameters.setStops(model.makeStops())
52+
Task {
53+
let routeResult = try await model.routeDataModel.routeTask.solveRoute(
54+
using: model.routeDataModel.routeParameters
55+
)
56+
if let firstRoute = routeResult.routes.first {
57+
let routeGraphic = Graphic(geometry: firstRoute.geometry)
58+
model.routeGraphicsOverlay.addGraphic(routeGraphic)
59+
model.routeDataModel.routeResult = routeResult
60+
didSelectRoute = true
61+
statusText = "Tap camera to start navigation."
62+
} else {
63+
self.error = error
64+
}
65+
}
66+
}
67+
}
68+
.locationDisplay(model.locationDisplay)
69+
.overlay(alignment: .top) {
70+
Text(statusText)
71+
.multilineTextAlignment(.center)
72+
.frame(maxWidth: .infinity, alignment: .center)
73+
.padding(8)
74+
.background(.regularMaterial, ignoresSafeAreaEdges: .horizontal)
75+
}
76+
.onChange(of: didSelectRoute) { didSelectRoute in
77+
guard didSelectRoute else { return }
78+
if let onDidSelectRoute = selectRouteAction,
79+
let routeResult = model.routeDataModel.routeResult {
80+
onDidSelectRoute(model.routeGraphic, routeResult)
81+
}
82+
}
83+
.toolbar {
84+
ToolbarItemGroup(placement: .bottomBar) {
85+
Spacer()
86+
Button {
87+
isShowing = false
88+
} label: {
89+
Image(systemName: "camera")
90+
.imageScale(.large)
91+
}
92+
.disabled(!didSelectRoute)
93+
Spacer()
94+
Button {
95+
model.reset()
96+
statusText = "Tap to place a start point."
97+
} label: {
98+
Image(systemName: "trash")
99+
.imageScale(.large)
100+
}
101+
.disabled(!didSelectRouteStop && !didSelectRoute)
102+
}
103+
}
104+
.onAppear {
105+
statusText = "Tap to place a start point."
106+
}
107+
.onDisappear {
108+
Task { await model.locationDataSource.stop() }
109+
}
110+
}
111+
112+
/// Sets an action to perform when the route is selected
113+
/// - Parameter action: The action to perform when the route is selected.
114+
func onDidSelectRoute(
115+
perform action: @escaping (Graphic, RouteResult) -> Void
116+
) -> RoutePlannerView {
117+
var copy = self
118+
copy.selectRouteAction = action
119+
return copy
120+
}
121+
}
122+
}
123+
124+
private extension AugmentRealityToNavigateRouteView.RoutePlannerView {
125+
/// A view model for this example.
126+
@MainActor
127+
class Model: ObservableObject {
128+
/// The data model for the selected route.
129+
@ObservedObject var routeDataModel = AugmentRealityToNavigateRouteView.RouteDataModel()
130+
/// A map with an imagery basemap style.
131+
let map = Map(basemapStyle: .arcGISImagery)
132+
/// The data source to track device location and provide updates to route tracker.
133+
let locationDataSource = SystemLocationDataSource()
134+
/// The graphic (with solid yellow 3D tube symbol) to represent the route.
135+
let routeGraphic = Graphic()
136+
/// The map's location display.
137+
let locationDisplay: LocationDisplay = {
138+
let locationDisplay = LocationDisplay()
139+
locationDisplay.autoPanMode = .recenter
140+
return locationDisplay
141+
}()
142+
/// The graphics overlay for the stops.
143+
let stopGraphicsOverlay = GraphicsOverlay()
144+
/// A graphic overlay for route graphics.
145+
let routeGraphicsOverlay: GraphicsOverlay = {
146+
let overlay = GraphicsOverlay()
147+
overlay.renderer = SimpleRenderer(
148+
symbol: SimpleLineSymbol(style: .solid, color: .yellow, width: 5)
149+
)
150+
return overlay
151+
}()
152+
/// The map's graphics overlays.
153+
var graphicsOverlays: [GraphicsOverlay] {
154+
return [stopGraphicsOverlay, routeGraphicsOverlay]
155+
}
156+
/// A point representing the start of navigation.
157+
var startPoint: Point? {
158+
didSet {
159+
let stopSymbol = PictureMarkerSymbol(image: UIImage(named: "StopA")!)
160+
let startStopGraphic = Graphic(geometry: startPoint, symbol: stopSymbol)
161+
stopGraphicsOverlay.addGraphic(startStopGraphic)
162+
}
163+
}
164+
/// A point representing the destination of navigation.
165+
var endPoint: Point? {
166+
didSet {
167+
let stopSymbol = PictureMarkerSymbol(image: UIImage(named: "StopB")!)
168+
let endStopGraphic = Graphic(geometry: endPoint, symbol: stopSymbol)
169+
stopGraphicsOverlay.addGraphic(endStopGraphic)
170+
}
171+
}
172+
173+
init() {
174+
// Request when-in-use location authorization.
175+
let locationManager = CLLocationManager()
176+
if locationManager.authorizationStatus == .notDetermined {
177+
locationManager.requestWhenInUseAuthorization()
178+
}
179+
180+
locationDisplay.dataSource = locationDataSource
181+
182+
Task {
183+
try await locationDataSource.start()
184+
185+
let parameters = try await routeDataModel.routeTask.makeDefaultParameters()
186+
187+
if let walkMode = routeDataModel.routeTask.info.travelModes.first(where: { $0.name.contains("Walking") }) {
188+
parameters.travelMode = walkMode
189+
parameters.returnsStops = true
190+
parameters.returnsDirections = true
191+
parameters.returnsRoutes = true
192+
routeDataModel.routeParameters = parameters
193+
}
194+
}
195+
}
196+
197+
/// Creates the start and destination stops for the navigation.
198+
func makeStops() -> [Stop] {
199+
guard let startPoint, let endPoint else { return [] }
200+
let stop1 = Stop(point: startPoint)
201+
stop1.name = "Start"
202+
let stop2 = Stop(point: endPoint)
203+
stop2.name = "Destination"
204+
return [stop1, stop2]
205+
}
206+
207+
/// Resets the start and destination stops for the navigation.
208+
func reset() {
209+
routeGraphicsOverlay.removeAllGraphics()
210+
stopGraphicsOverlay.removeAllGraphics()
211+
routeDataModel.routeParameters.clearStops()
212+
startPoint = nil
213+
endPoint = nil
214+
}
215+
}
216+
}

0 commit comments

Comments
 (0)