forked from merlos/iOS-Open-GPX-Tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathInterfaceController.swift
460 lines (381 loc) · 18 KB
/
InterfaceController.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
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
//
// InterfaceController.swift
// OpenGpxTracker-Watch Extension
//
// Created by Vincent on 5/2/19.
// Copyright © 2019 TransitBox. All rights reserved.
//
import WatchKit
import MapKit
import CoreLocation
import CoreGPX
//Button colors
let kPurpleButtonBackgroundColor: UIColor = UIColor(red: 146.0/255.0, green: 166.0/255.0, blue: 218.0/255.0, alpha: 0.90)
let kGreenButtonBackgroundColor: UIColor = UIColor(red: 142.0/255.0, green: 224.0/255.0, blue: 102.0/255.0, alpha: 0.90)
let kRedButtonBackgroundColor: UIColor = UIColor(red: 244.0/255.0, green: 94.0/255.0, blue: 94.0/255.0, alpha: 0.90)
let kBlueButtonBackgroundColor: UIColor = UIColor(red: 74.0/255.0, green: 144.0/255.0, blue: 226.0/255.0, alpha: 0.90)
let kDisabledBlueButtonBackgroundColor: UIColor = UIColor(red: 74.0/255.0, green: 144.0/255.0, blue: 226.0/255.0, alpha: 0.10)
let kDisabledRedButtonBackgroundColor: UIColor = UIColor(red: 244.0/255.0, green: 94.0/255.0, blue: 94.0/255.0, alpha: 0.10)
let kWhiteBackgroundColor: UIColor = UIColor(red: 254.0/255.0, green: 254.0/255.0, blue: 254.0/255.0, alpha: 0.90)
//Accesory View buttons tags
let kDeleteWaypointAccesoryButtonTag = 666
let kEditWaypointAccesoryButtonTag = 333
let kNotGettingLocationText = NSLocalizedString("NO_LOCATION", comment: "no comment")
let kUnknownAccuracyText = "±···"
let kUnknownSpeedText = "·.··"
let kUnknownAltitudeText = "···"
/// Size for small buttons
let kButtonSmallSize: CGFloat = 48.0
/// Size for large buttons
let kButtonLargeSize: CGFloat = 96.0
/// Separation between buttons
let kButtonSeparation: CGFloat = 6.0
/// Upper limits threshold (in meters) on signal accuracy.
let kSignalAccuracy6 = 6.0
let kSignalAccuracy5 = 11.0
let kSignalAccuracy4 = 31.0
let kSignalAccuracy3 = 51.0
let kSignalAccuracy2 = 101.0
let kSignalAccuracy1 = 201.0
///
/// Main View Controller of the Watch Application. It is loaded when the application is launched
///
/// Displays a set the buttons to control the tracking, along with additional infomation.
///
///
class InterfaceController: WKInterfaceController {
@IBOutlet var newPinButton: WKInterfaceButton!
@IBOutlet var trackerButton: WKInterfaceButton!
@IBOutlet var saveButton: WKInterfaceButton!
@IBOutlet var resetButton: WKInterfaceButton!
@IBOutlet var timeLabel: WKInterfaceLabel!
@IBOutlet var totalTrackedDistanceLabel: WKInterfaceLabel!
@IBOutlet var signalImageView: WKInterfaceImage!
@IBOutlet var signalAccuracyLabel: WKInterfaceLabel!
@IBOutlet var coordinatesLabel: WKInterfaceLabel!
@IBOutlet var altitudeLabel: WKInterfaceLabel!
@IBOutlet var speedLabel: WKInterfaceLabel!
/// Location Manager
let locationManager: CLLocationManager = {
let manager = CLLocationManager()
manager.requestAlwaysAuthorization()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.distanceFilter = 2 //meters
manager.allowsBackgroundLocationUpdates = true
return manager
}()
/// Preferences loader
let preferences = Preferences.shared
/// Underlying class that handles background stuff
let map = GPXMapView() // not even a map view. Considering renaming
//Status Vars
var stopWatch = StopWatch()
var lastGpxFilename: String = ""
var wasSentToBackground: Bool = false //Was the app sent to background
var isDisplayingLocationServicesDenied: Bool = false
/// Does the 'file' have any waypoint?
var hasWaypoints: Bool = false {
/// Whenever it is updated, if it has waypoints it sets the save and reset button
didSet {
if hasWaypoints {
saveButton.setBackgroundColor(kBlueButtonBackgroundColor)
resetButton.setBackgroundColor(kRedButtonBackgroundColor)
}
}
}
// Signal accuracy images
let signalImage0 = UIImage(named: "signal0")
let signalImage1 = UIImage(named: "signal1")
let signalImage2 = UIImage(named: "signal2")
let signalImage3 = UIImage(named: "signal3")
let signalImage4 = UIImage(named: "signal4")
let signalImage5 = UIImage(named: "signal5")
let signalImage6 = UIImage(named: "signal6")
/// Defines the different statuses regarding tracking current user location.
enum GpxTrackingStatus {
/// Tracking has not started or map was reset
case notStarted
/// Tracking is ongoing
case tracking
/// Tracking is paused (the map has some contents)
case paused
}
/// Tells what is the current status of the Map Instance.
var gpxTrackingStatus: GpxTrackingStatus = GpxTrackingStatus.notStarted {
didSet {
print("gpxTrackingStatus changed to \(gpxTrackingStatus)")
switch gpxTrackingStatus {
case .notStarted:
print("switched to non started")
// set Tracker button to allow Start
trackerButton.setTitle(NSLocalizedString("START_TRACKING", comment: "no comment"))
trackerButton.setBackgroundColor(kGreenButtonBackgroundColor)
//save & reset button to transparent.
saveButton.setBackgroundColor(kDisabledBlueButtonBackgroundColor)
resetButton.setBackgroundColor(kDisabledRedButtonBackgroundColor)
//reset clock
stopWatch.reset()
timeLabel.setText(stopWatch.elapsedTimeString)
map.reset() //reset gpx logging
lastGpxFilename = "" //clear last filename, so when saving it appears an empty field
totalTrackedDistanceLabel.setText(map.totalTrackedDistance.toDistance(useImperial: preferences.useImperial))
//currentSegmentDistanceLabel.distance = (map.currentSegmentDistance)
/*
// XXX Left here for reference
UIView.animateWithDuration(0.2, delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: { () -> Void in
self.trackerButton.hidden = true
self.pauseButton.hidden = false
}, completion: {(f: Bool) -> Void in
println("finished animation start tracking")
})
*/
case .tracking:
print("switched to tracking mode")
// set trackerButton to allow Pause
trackerButton.setTitle(NSLocalizedString("PAUSE", comment: "no comment"))
trackerButton.setBackgroundColor(kPurpleButtonBackgroundColor)
//activate save & reset buttons
saveButton.setBackgroundColor(kBlueButtonBackgroundColor)
resetButton.setBackgroundColor(kRedButtonBackgroundColor)
// start clock
self.stopWatch.start()
case .paused:
print("switched to paused mode")
// set trackerButton to allow Resume
self.trackerButton.setTitle(NSLocalizedString("RESUME", comment: "no comment"))
self.trackerButton.setBackgroundColor(kGreenButtonBackgroundColor)
// activate save & reset (just in case switched from .NotStarted)
saveButton.setBackgroundColor(kBlueButtonBackgroundColor)
resetButton.setBackgroundColor(kRedButtonBackgroundColor)
//pause clock
self.stopWatch.stop()
// start new track segment
self.map.startNewTrackSegment()
}
}
}
/// Editing Waypoint Temporal Reference
var lastLocation: CLLocation? //Last point of current segment.
override func awake(withContext context: Any?) {
print("InterfaceController:: awake")
super.awake(withContext: context)
totalTrackedDistanceLabel.setText( 0.00.toDistance(useImperial: preferences.useImperial))
if gpxTrackingStatus == .notStarted {
trackerButton.setBackgroundColor(kGreenButtonBackgroundColor)
newPinButton.setBackgroundColor(kWhiteBackgroundColor)
saveButton.setBackgroundColor(kDisabledRedButtonBackgroundColor)
resetButton.setBackgroundColor(kDisabledBlueButtonBackgroundColor)
coordinatesLabel.setText(kNotGettingLocationText)
signalAccuracyLabel.setText(kUnknownAccuracyText)
altitudeLabel.setText(kUnknownAltitudeText)
speedLabel.setText(kUnknownSpeedText)
signalImageView.setImage(signalImage0)
}
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
print("InterfaceController:: willActivate")
super.willActivate()
self.setTitle(NSLocalizedString("GPX_TRACKER", comment: "no comment"))
stopWatch.delegate = self
locationManager.delegate = self
checkLocationServicesStatus()
locationManager.startUpdatingLocation()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
print("InterfaceController:: didDeactivate called")
if gpxTrackingStatus != .tracking {
print("InterfaceController:: didDeactivate will stopUpdatingLocation")
locationManager.stopUpdatingLocation()
}
}
///
/// Main Start/Pause Button was tapped.
///
/// It sets the status to tracking or paused.
///
@IBAction func trackerButtonTapped() {
print("startGpxTracking::")
switch gpxTrackingStatus {
case .notStarted:
gpxTrackingStatus = .tracking
case .tracking:
gpxTrackingStatus = .paused
case .paused:
//set to tracking
gpxTrackingStatus = .tracking
}
}
///
/// Add Pin (waypoint) Button was tapped.
///
/// It adds a new waypoint with the current coordinates while tracking is underway.
///
@IBAction func addPinAtMyLocation() {
if let currentCoordinates = locationManager.location?.coordinate {
let altitude = locationManager.location?.altitude
let waypoint = GPXWaypoint(coordinate: currentCoordinates, altitude: altitude)
map.addWaypoint(waypoint)
print("Adding waypoint at \(currentCoordinates)")
self.hasWaypoints = true
}
}
///
/// Save Button was tapped.
///
/// Saves current track and waypoints as a GPX file, with a default filename of date and time.
///
@IBAction func saveButtonTapped() {
print("save Button tapped")
// ignore the save button if there is nothing to save.
if (gpxTrackingStatus == .notStarted) && !self.hasWaypoints {
return
}
let filename = defaultFilename()
let gpxString = self.map.exportToGPXString()
GPXFileManager.save(filename, gpxContents: gpxString)
self.lastGpxFilename = filename
//print(gpxString)
/// Just a 'done' button, without
let action = WKAlertAction(title: "Done", style: .default) {}
presentAlert(withTitle: NSLocalizedString("FILE_SAVED_TITLE", comment: "no comment"),
message: "\(filename).gpx", preferredStyle: .alert, actions: [action])
}
///
/// Triggered when reset button was tapped.
///
/// It sets map to status .notStarted which clears the map.
///
@IBAction func resetButtonTapped() {
self.gpxTrackingStatus = .notStarted
}
/// returns a string with the format of current date dd-MMM-yyyy-HHmm' (20-Jun-2018-1133)
///
func defaultFilename() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd-MMM-yyyy-HHmm"
print("fileName:" + dateFormatter.string(from: Date()))
return dateFormatter.string(from: Date())
}
///
/// Checks the location services status
/// - Are location services enabled (access to location device wide)? If not => displays an alert
/// - Are location services allowed to this app? If not => displays an alert
///
/// - Seealso: displayLocationServicesDisabledAlert, displayLocationServicesDeniedAlert
///
func checkLocationServicesStatus() {
//Are location services enabled?
if !CLLocationManager.locationServicesEnabled() {
displayLocationServicesDisabledAlert()
return
}
//Does the app have permissions to use the location servies?
if !([.authorizedAlways, .authorizedWhenInUse].contains(CLLocationManager.authorizationStatus())) {
displayLocationServicesDeniedAlert()
return
}
}
///
/// Displays an alert that informs the user that location services are disabled.
///
/// When location services are disabled is for all applications, not only this one.
///
func displayLocationServicesDisabledAlert() {
let button = WKAlertAction(title: "Cancel", style: .cancel) {
print("LocationServicesDisabledAlert: cancel pressed")
}
presentAlert(withTitle: NSLocalizedString("LOCATION_SERVICES_DISABLED", comment: "no comment"), message: NSLocalizedString("ENABLE_LOCATION_SERVICES", comment: "no comment"), preferredStyle: .alert, actions: [button])
}
///
/// Displays an alert that informs the user that access to location was denied for this app (other apps may have access).
/// It also dispays a button allows the user to go to settings to activate the location.
///
func displayLocationServicesDeniedAlert() {
if isDisplayingLocationServicesDenied {
return // display it only once.
}
let button = WKAlertAction(title: "Cancel", style: .cancel) {
print("LocationServicesDeniedAlert: cancel pressed")
}
presentAlert(withTitle: NSLocalizedString("ACCESS_TO_LOCATION_DENIED", comment: "no comment"), message: NSLocalizedString("ALLOW_LOCATION", comment: "no comment"), preferredStyle: .alert, actions: [button])
}
}
// MARK: StopWatchDelegate
///
/// Updates the `timeLabel` with the `stopWatch` elapsedTime.
/// In the main ViewController there is a label that holds the elapsed time, that is, the time that
/// user has been tracking his position.
///
///
extension InterfaceController: StopWatchDelegate {
func stopWatch(_ stropWatch: StopWatch, didUpdateElapsedTimeString elapsedTimeString: String) {
timeLabel.setText(elapsedTimeString)
}
}
// MARK: CLLocationManagerDelegate
extension InterfaceController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("didFailWithError \(error)")
coordinatesLabel.setText(kNotGettingLocationText)
signalAccuracyLabel.setText(kUnknownAccuracyText)
altitudeLabel.setText(kUnknownAltitudeText)
signalImageView.setImage(signalImage0)
speedLabel.setText(kUnknownSpeedText)
//signalAccuracyLabel.text = kUnknownAccuracyText
//signalImageView.image = signalImage0
let locationError = error as? CLError
switch locationError?.code {
case CLError.locationUnknown:
print("Location Unknown")
case CLError.denied:
print("Access to location services denied. Display message")
checkLocationServicesStatus()
case CLError.headingFailure:
print("Heading failure")
default:
print("Default error")
}
}
///
/// Updates location accuracy and map information when user is in a new position
///
///
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
//updates signal image accuracy
let newLocation = locations.first!
print("didUpdateLocation: received \(newLocation.coordinate) hAcc: \(newLocation.horizontalAccuracy) vAcc: \(newLocation.verticalAccuracy) floor: \(newLocation.floor?.description ?? "''")")
let hAcc = newLocation.horizontalAccuracy
signalAccuracyLabel.setText(hAcc.toAccuracy(useImperial: preferences.useImperial))
if hAcc < kSignalAccuracy6 {
self.signalImageView.setImage(signalImage6)
} else if hAcc < kSignalAccuracy5 {
self.signalImageView.setImage(signalImage5)
} else if hAcc < kSignalAccuracy4 {
self.signalImageView.setImage(signalImage4)
} else if hAcc < kSignalAccuracy3 {
self.signalImageView.setImage(signalImage3)
} else if hAcc < kSignalAccuracy2 {
self.signalImageView.setImage(signalImage2)
} else if hAcc < kSignalAccuracy1 {
self.signalImageView.setImage(signalImage1)
} else{
self.signalImageView.setImage(signalImage0)
}
// Update coordsLabels
let latFormat = String(format: "%.6f", newLocation.coordinate.latitude)
let lonFormat = String(format: "%.6f", newLocation.coordinate.longitude)
coordinatesLabel.setText("\(latFormat),\(lonFormat)")
altitudeLabel.setText(newLocation.altitude.toAltitude(useImperial: preferences.useImperial))
//Update speed (provided in m/s, but displayed in km/h)
speedLabel.setText(newLocation.speed.toSpeed(useImperial: preferences.useImperial))
if gpxTrackingStatus == .tracking {
print("didUpdateLocation: adding point to track (\(newLocation.coordinate.latitude),\(newLocation.coordinate.longitude))")
map.addPointToCurrentTrackSegmentAtLocation(newLocation)
totalTrackedDistanceLabel.setText(map.totalTrackedDistance.toDistance(useImperial: preferences.useImperial))
//currentSegmentDistanceLabel.distance = map.currentSegmentDistance
}
}
}