forked from AndrewHartAR/ARKit-CoreLocation
/
POIViewController.swift
341 lines (277 loc) · 13.1 KB
/
POIViewController.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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
//
// POIViewController.swift
// ARKit+CoreLocation
//
// Created by Andrew Hart on 02/07/2017.
// Copyright © 2017 Project Dent. All rights reserved.
//
import UIKit
import SceneKit
import MapKit
import ARCL
@available(iOS 11.0, *)
/// Displays Points of Interest in ARCL
class POIViewController: UIViewController {
@IBOutlet var mapView: MKMapView!
@IBOutlet var infoLabel: UILabel!
@IBOutlet var contentView: UIView!
let sceneLocationView = SceneLocationView()
var userAnnotation: MKPointAnnotation?
var locationEstimateAnnotation: MKPointAnnotation?
var updateUserLocationTimer: Timer?
var updateInfoLabelTimer: Timer?
var centerMapOnUserLocation: Bool = true
var routes: [MKRoute]?
var showMap = false {
didSet {
guard let mapView = mapView else {
return
}
mapView.isHidden = !showMap
}
}
/// Whether to display some debugging data
/// This currently displays the coordinate of the best location estimate
/// The initial value is respected
let displayDebugging = false
let adjustNorthByTappingSidesOfScreen = false
class func loadFromStoryboard() -> POIViewController {
return UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "ARCLViewController") as! POIViewController
// swiftlint:disable:previous force_cast
}
override func viewDidLoad() {
super.viewDidLoad()
updateInfoLabelTimer = Timer.scheduledTimer(timeInterval: 0.1,
target: self,
selector: #selector(POIViewController.updateInfoLabel),
userInfo: nil,
repeats: true)
// Set to true to display an arrow which points north.
// Checkout the comments in the property description and on the readme on this.
// sceneLocationView.orientToTrueNorth = false
// sceneLocationView.locationEstimateMethod = .coreLocationDataOnly
sceneLocationView.showAxesNode = true
sceneLocationView.showFeaturePoints = displayDebugging
// Now add the route or location annotations as appropriate
addSceneModels()
contentView.addSubview(sceneLocationView)
sceneLocationView.frame = contentView.bounds
mapView.isHidden = !showMap
if showMap {
updateUserLocationTimer = Timer.scheduledTimer(
timeInterval: 0.5,
target: self,
selector: #selector(POIViewController.updateUserLocation),
userInfo: nil,
repeats: true)
routes?.forEach { mapView.addOverlay($0.polyline) }
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
print("run")
sceneLocationView.run()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("pause")
// Pause the view's session
sceneLocationView.pause()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
sceneLocationView.frame = contentView.bounds
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first,
let view = touch.view else { return }
if mapView == view || mapView.recursiveSubviews().contains(view) {
centerMapOnUserLocation = false
} else {
let location = touch.location(in: self.view)
if location.x <= 40 && adjustNorthByTappingSidesOfScreen {
print("left side of the screen")
sceneLocationView.moveSceneHeadingAntiClockwise()
} else if location.x >= view.frame.size.width - 40 && adjustNorthByTappingSidesOfScreen {
print("right side of the screen")
sceneLocationView.moveSceneHeadingClockwise()
} else {
let image = UIImage(named: "pin")!
let annotationNode = LocationAnnotationNode(location: nil, image: image)
annotationNode.scaleRelativeToDistance = false
annotationNode.scalingScheme = .normal
sceneLocationView.addLocationNodeForCurrentPosition(locationNode: annotationNode)
}
}
}
}
// MARK: - MKMapViewDelegate
@available(iOS 11.0, *)
extension POIViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.lineWidth = 3
renderer.strokeColor = UIColor.blue.withAlphaComponent(0.5)
return renderer
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !(annotation is MKUserLocation),
let pointAnnotation = annotation as? MKPointAnnotation else { return nil }
let marker = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: nil)
if pointAnnotation == self.userAnnotation {
marker.displayPriority = .required
marker.glyphImage = UIImage(named: "user")
} else {
marker.displayPriority = .required
marker.markerTintColor = UIColor(hue: 0.267, saturation: 0.67, brightness: 0.77, alpha: 1.0)
marker.glyphImage = UIImage(named: "compass")
}
return marker
}
}
// MARK: - Implementation
@available(iOS 11.0, *)
extension POIViewController {
/// Adds the appropriate ARKit models to the scene. Note: that this won't
/// do anything until the scene has a `currentLocation`. It "polls" on that
/// and when a location is finally discovered, the models are added.
func addSceneModels() {
// 1. Don't try to add the models to the scene until we have a current location
guard sceneLocationView.sceneLocationManager.currentLocation != nil else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.addSceneModels()
}
return
}
let box = SCNBox(width: 1, height: 0.2, length: 5, chamferRadius: 0.25)
box.firstMaterial?.diffuse.contents = UIColor.gray.withAlphaComponent(0.5)
// 2. If there is a route, show that
if let routes = routes {
sceneLocationView.addRoutes(routes: routes) { distance -> SCNBox in
let box = SCNBox(width: 1.75, height: 0.5, length: distance, chamferRadius: 0.25)
// // Option 1: An absolutely terrible box material set (that demonstrates what you can do):
// box.materials = ["box0", "box1", "box2", "box3", "box4", "box5"].map {
// let material = SCNMaterial()
// material.diffuse.contents = UIImage(named: $0)
// return material
// }
// Option 2: Something more typical
box.firstMaterial?.diffuse.contents = UIColor.blue.withAlphaComponent(0.7)
return box
}
} else {
// 3. If not, then show the
buildDemoData().forEach {
sceneLocationView.addLocationNodeWithConfirmedLocation(locationNode: $0)
}
}
}
/// Builds the location annotations for a few random objects, scattered across the country
///
/// - Returns: an array of annotation nodes.
func buildDemoData() -> [LocationAnnotationNode] {
var nodes: [LocationAnnotationNode] = []
let spaceNeedle = buildNode(latitude: 47.6205, longitude: -122.3493, altitude: 225, imageName: "pin")
nodes.append(spaceNeedle)
let empireStateBuilding = buildNode(latitude: 40.7484, longitude: -73.9857, altitude: 14.3, imageName: "pin")
nodes.append(empireStateBuilding)
let canaryWharf = buildNode(latitude: 51.504607, longitude: -0.019592, altitude: 236, imageName: "pin")
nodes.append(canaryWharf)
let applePark = buildViewNode(latitude: 37.334807, longitude: -122.009076, altitude: 100, text: "Apple Park")
nodes.append(applePark)
return nodes
}
@objc
func updateUserLocation() {
guard let currentLocation = sceneLocationView.sceneLocationManager.currentLocation else {
return
}
DispatchQueue.main.async { [weak self ] in
guard let self = self else {
return
}
if self.userAnnotation == nil {
self.userAnnotation = MKPointAnnotation()
self.mapView.addAnnotation(self.userAnnotation!)
}
UIView.animate(withDuration: 0.5, delay: 0, options: .allowUserInteraction, animations: {
self.userAnnotation?.coordinate = currentLocation.coordinate
}, completion: nil)
if self.centerMapOnUserLocation {
UIView.animate(withDuration: 0.45,
delay: 0,
options: .allowUserInteraction,
animations: {
self.mapView.setCenter(self.userAnnotation!.coordinate, animated: false)
}, completion: { _ in
self.mapView.region.span = MKCoordinateSpan(latitudeDelta: 0.0005, longitudeDelta: 0.0005)
})
}
if self.displayDebugging {
if let bestLocationEstimate = self.sceneLocationView.sceneLocationManager.bestLocationEstimate {
if self.locationEstimateAnnotation == nil {
self.locationEstimateAnnotation = MKPointAnnotation()
self.mapView.addAnnotation(self.locationEstimateAnnotation!)
}
self.locationEstimateAnnotation?.coordinate = bestLocationEstimate.location.coordinate
} else if self.locationEstimateAnnotation != nil {
self.mapView.removeAnnotation(self.locationEstimateAnnotation!)
self.locationEstimateAnnotation = nil
}
}
}
}
@objc
func updateInfoLabel() {
if let position = sceneLocationView.currentScenePosition {
infoLabel.text = "x: \(position.x.short), y: \(position.y.short), z: \(position.z.short)\n"
}
if let eulerAngles = sceneLocationView.currentEulerAngles {
infoLabel.text!.append("Euler x: \(eulerAngles.x.short), y: \(eulerAngles.y.short), z: \(eulerAngles.z.short)\n")
}
if let eulerAngles = sceneLocationView.currentEulerAngles,
let heading = sceneLocationView.sceneLocationManager.locationManager.heading,
let headingAccuracy = sceneLocationView.sceneLocationManager.locationManager.headingAccuracy {
infoLabel.text!.append("Heading: \((((0 - eulerAngles.y.radiansToDegrees) + 360).truncatingRemainder(dividingBy: 360) ).short)° • \(Float(heading).short)° • \(headingAccuracy)°\n")
}
let comp = Calendar.current.dateComponents([.hour, .minute, .second, .nanosecond], from: Date())
if let hour = comp.hour, let minute = comp.minute, let second = comp.second, let nanosecond = comp.nanosecond {
infoLabel.text!.append("\(hour.short):\(minute.short):\(second.short):\(nanosecond.short3)")
}
}
func buildNode(latitude: CLLocationDegrees, longitude: CLLocationDegrees,
altitude: CLLocationDistance, imageName: String) -> LocationAnnotationNode {
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
let location = CLLocation(coordinate: coordinate, altitude: altitude)
let image = UIImage(named: imageName)!
return LocationAnnotationNode(location: location, image: image)
}
func buildViewNode(latitude: CLLocationDegrees, longitude: CLLocationDegrees,
altitude: CLLocationDistance, text: String) -> LocationAnnotationNode {
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
let location = CLLocation(coordinate: coordinate, altitude: altitude)
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
label.text = text
label.backgroundColor = .green
label.textAlignment = .center
return LocationAnnotationNode(location: location, view: label)
}
}
// MARK: - Helpers
extension DispatchQueue {
func asyncAfter(timeInterval: TimeInterval, execute: @escaping () -> Void) {
self.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(timeInterval * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC),
execute: execute)
}
}
extension UIView {
func recursiveSubviews() -> [UIView] {
var recursiveSubviews = self.subviews
subviews.forEach { recursiveSubviews.append(contentsOf: $0.recursiveSubviews()) }
return recursiveSubviews
}
}